Files
gart/code/fcpxml/writer/markers.py
T
João HenriqueandClaude Opus 5 4f5cf94443 refactor: writer.py vira pacote, um módulo por assunto
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>
2026-08-19 21:38:49 -04:00

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