O writer tinha 4.199 linhas, das quais 3.300 numa única classe com dezoito
assuntos dentro. Achar o trecho de zoom exigia rolar por marcadores,
velocidade e legendas.
Agora é o pacote fcpxml/writer/, com um arquivo por assunto e o
FCPXMLModifier montado por composição de mixins. Mixins, e não objetos
separados, porque todas essas operações mexem no mesmo documento e nos
mesmos índices — separá-las em objetos independentes transformaria toda
chamada interna em travessia de fronteira sem nada em troca. A divisão que
importa aqui é de leitura, não de estado.
Nenhuma mudança de comportamento e nenhuma alteração nos ~50 pontos que
importam do writer: o __init__ re-exporta tudo, inclusive os nomes com
underscore que a suíte já usava.
core 723 carga, índices, navegação na spine, save
titles 600 títulos e legendas dinâmicas
cut 333 dividir, cortar faixas, apagar
speed 297 velocidade e zoom
(+ 20 módulos menores)
Único ajuste de chamada: quatro testes faziam patch em
fcpxml.writer.subprocess, que agora mora em writer.document (ver
Engine/docs/05_EXPERIENCIAS.md #23).
Lint zerado, 1441 testes passando.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
166 lines
5.5 KiB
Python
166 lines
5.5 KiB
Python
"""Marcadores: um, por timecode, e em lote.
|
|
|
|
Extraído de writer.py — ver fcpxml/writer/__init__.py para o conjunto.
|
|
"""
|
|
|
|
import xml.etree.ElementTree as ET
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from ..models import (
|
|
MarkerColor,
|
|
MarkerType,
|
|
TimeValue,
|
|
)
|
|
from .helpers import build_marker_element
|
|
|
|
|
|
class MarkersMixin:
|
|
"""Marcadores: um, por timecode, e em lote."""
|
|
|
|
# ========================================================================
|
|
# MARKER OPERATIONS
|
|
# ========================================================================
|
|
|
|
def add_marker(
|
|
self,
|
|
clip_id: 'str | ET.Element',
|
|
timecode: str,
|
|
name: str,
|
|
marker_type: "MarkerType | str" = MarkerType.STANDARD,
|
|
color: Optional[MarkerColor] = None,
|
|
note: Optional[str] = None
|
|
) -> ET.Element:
|
|
"""
|
|
Add a marker to a clip.
|
|
|
|
Args:
|
|
clip_id: Target clip identifier (name or ID)
|
|
timecode: Position within clip (relative to clip start)
|
|
name: Marker label
|
|
marker_type: STANDARD, TODO, COMPLETED, or CHAPTER (enum or string)
|
|
color: Optional marker color
|
|
note: Optional marker note
|
|
|
|
Returns:
|
|
The created marker element
|
|
"""
|
|
clip = self._require_clip(clip_id)
|
|
|
|
if isinstance(marker_type, str):
|
|
marker_type = MarkerType.from_string(marker_type)
|
|
|
|
time_value = self._parse_time(timecode)
|
|
|
|
return build_marker_element(
|
|
parent=clip,
|
|
marker_type=marker_type,
|
|
start=time_value.to_fcpxml(),
|
|
duration=f"1/{int(self.fps)}s",
|
|
name=name,
|
|
note=note,
|
|
)
|
|
|
|
def add_marker_at_timeline(
|
|
self,
|
|
timecode: str,
|
|
name: str,
|
|
marker_type: "MarkerType | str" = MarkerType.STANDARD,
|
|
color: Optional[MarkerColor] = None,
|
|
note: Optional[str] = None
|
|
) -> ET.Element:
|
|
"""Add a marker at a timeline position (finds the containing clip).
|
|
|
|
Uses ``_find_spine_clip_at_seconds`` to walk the spine directly,
|
|
avoiding the name-indexed ``self.clips`` dict which silently drops
|
|
duplicate-named clips.
|
|
"""
|
|
if isinstance(marker_type, str):
|
|
marker_type = MarkerType.from_string(marker_type)
|
|
time_value = self._parse_time(timecode)
|
|
target_seconds = time_value.to_seconds()
|
|
|
|
clip, relative_seconds = self._find_spine_clip_at_seconds(target_seconds)
|
|
relative_tc = TimeValue.from_seconds(relative_seconds, self.fps)
|
|
|
|
return build_marker_element(
|
|
parent=clip,
|
|
marker_type=marker_type,
|
|
start=relative_tc.to_fcpxml(),
|
|
duration=f"1/{int(self.fps)}s",
|
|
name=name,
|
|
note=note,
|
|
)
|
|
|
|
def batch_add_markers(
|
|
self,
|
|
markers: List[Dict[str, Any]],
|
|
auto_at_cuts: bool = False,
|
|
auto_at_intervals: Optional[str] = None
|
|
) -> List[ET.Element]:
|
|
"""
|
|
Add multiple markers at once.
|
|
|
|
Args:
|
|
markers: List of marker specs [{timecode, name, marker_type, color}]
|
|
auto_at_cuts: Add marker at every cut point
|
|
auto_at_intervals: Add markers at regular intervals (e.g., "00:00:30:00")
|
|
|
|
Returns:
|
|
List of created marker elements
|
|
"""
|
|
created = []
|
|
|
|
# Handle explicit markers
|
|
for m in markers:
|
|
marker = self.add_marker_at_timeline(
|
|
timecode=m['timecode'],
|
|
name=m['name'],
|
|
marker_type=MarkerType.from_string(m.get('marker_type', 'standard')),
|
|
color=MarkerColor[m['color'].upper()] if m.get('color') else None,
|
|
note=m.get('note')
|
|
)
|
|
created.append(marker)
|
|
|
|
# Auto-detect at cuts — add a marker at the start of every spine clip.
|
|
if auto_at_cuts:
|
|
for i, clip in self._iter_spine_clips():
|
|
clip_start = clip.get('start', '0s')
|
|
marker = build_marker_element(
|
|
parent=clip,
|
|
marker_type=MarkerType.STANDARD,
|
|
start=clip_start,
|
|
duration=f"1/{int(self.fps)}s",
|
|
name=f"Cut {i+1}",
|
|
)
|
|
created.append(marker)
|
|
|
|
# Auto-detect at intervals — place markers at regular time steps.
|
|
if auto_at_intervals:
|
|
interval = self._parse_time(auto_at_intervals).to_seconds()
|
|
total_duration = self._timeline_duration().to_seconds()
|
|
if total_duration > 0:
|
|
|
|
current = interval
|
|
count = 1
|
|
while current < total_duration:
|
|
try:
|
|
clip, relative = self._find_spine_clip_at_seconds(current)
|
|
except ValueError:
|
|
current += interval
|
|
count += 1
|
|
continue
|
|
rel_tv = TimeValue.from_seconds(relative, self.fps)
|
|
marker = build_marker_element(
|
|
parent=clip,
|
|
marker_type=MarkerType.STANDARD,
|
|
start=rel_tv.to_fcpxml(),
|
|
duration=f"1/{int(self.fps)}s",
|
|
name=f"Marker {count}",
|
|
)
|
|
created.append(marker)
|
|
current += interval
|
|
count += 1
|
|
|
|
return created
|
|
|