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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1bebee4359
commit
4f5cf94443
@@ -0,0 +1,196 @@
|
||||
"""Compound clips: criar e achatar.
|
||||
|
||||
Extraído de writer.py — ver fcpxml/writer/__init__.py para o conjunto.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import uuid
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import List
|
||||
|
||||
from ..models import (
|
||||
TimeValue,
|
||||
)
|
||||
from .helpers import (
|
||||
_sanitize_xml_value,
|
||||
)
|
||||
|
||||
|
||||
class CompoundMixin:
|
||||
"""Compound clips: criar e achatar."""
|
||||
|
||||
# COMPOUND CLIP OPERATIONS (v0.6.0)
|
||||
# ========================================================================
|
||||
|
||||
def create_compound_clip(
|
||||
self,
|
||||
clip_ids: List[str],
|
||||
name: str = "Compound Clip",
|
||||
) -> ET.Element:
|
||||
"""Group spine clips into a compound clip.
|
||||
|
||||
Creates a <media> resource with a nested <sequence><spine> containing
|
||||
the specified clips, then replaces the originals in the main spine
|
||||
with a single <ref-clip>.
|
||||
|
||||
Args:
|
||||
clip_ids: IDs of clips in the spine to group.
|
||||
name: Name for the compound clip.
|
||||
|
||||
Returns:
|
||||
The created <ref-clip> element.
|
||||
"""
|
||||
spine = self._get_spine()
|
||||
resources = self.root.find('.//resources')
|
||||
if resources is None:
|
||||
raise ValueError("No <resources> element found in FCPXML")
|
||||
|
||||
# Collect clips and validate they're in spine
|
||||
spine_children = list(spine)
|
||||
clips_to_group = []
|
||||
for cid in clip_ids:
|
||||
clip = self._require_clip(cid)
|
||||
if clip not in spine_children:
|
||||
raise ValueError(f"Clip not in spine: {cid}")
|
||||
clips_to_group.append((cid, clip))
|
||||
|
||||
if not clips_to_group:
|
||||
raise ValueError("No valid clips to group")
|
||||
|
||||
# Sort by offset so the compound maintains order
|
||||
clips_to_group.sort(
|
||||
key=lambda c: self._parse_time(c[1].get('offset', '0s'))
|
||||
)
|
||||
|
||||
# Calculate compound duration and starting offset
|
||||
first_offset = self._parse_time(clips_to_group[0][1].get('offset', '0s'))
|
||||
total_duration = TimeValue.zero()
|
||||
for _, clip in clips_to_group:
|
||||
total_duration = total_duration + self._parse_time(clip.get('duration', '0s'))
|
||||
|
||||
# Get format ref
|
||||
format_id = None
|
||||
for fmt_id in self.formats:
|
||||
format_id = fmt_id
|
||||
break
|
||||
|
||||
# Create media resource with nested sequence
|
||||
media_id = self._unique_resource_id(resources, 'r_compound1')
|
||||
|
||||
media = ET.SubElement(resources, 'media')
|
||||
media.set('id', media_id)
|
||||
media.set('name', _sanitize_xml_value(name, 512))
|
||||
media.set('uid', str(uuid.uuid4()).upper())
|
||||
|
||||
seq = ET.SubElement(media, 'sequence')
|
||||
seq.set('format', format_id or 'r1')
|
||||
seq.set('duration', total_duration.to_fcpxml())
|
||||
seq.set('tcStart', '0s')
|
||||
seq.set('tcFormat', 'NDF')
|
||||
|
||||
inner_spine = ET.SubElement(seq, 'spine')
|
||||
|
||||
# Move clips into the compound's inner spine
|
||||
inner_offset = TimeValue.zero()
|
||||
for _, clip in clips_to_group:
|
||||
new_clip = copy.deepcopy(clip)
|
||||
new_clip.set('offset', inner_offset.to_fcpxml())
|
||||
inner_spine.append(new_clip)
|
||||
inner_offset = inner_offset + self._parse_time(clip.get('duration', '0s'))
|
||||
|
||||
# Get the insert position (where first clip was)
|
||||
spine_children = list(spine)
|
||||
insert_idx = spine_children.index(clips_to_group[0][1])
|
||||
|
||||
# Remove originals from spine
|
||||
for cid, clip in clips_to_group:
|
||||
spine.remove(clip)
|
||||
if cid in self.clips:
|
||||
del self.clips[cid]
|
||||
|
||||
# Create ref-clip in main spine
|
||||
ref_clip = ET.Element('ref-clip')
|
||||
ref_clip.set('ref', media_id)
|
||||
ref_clip.set('offset', first_offset.to_fcpxml())
|
||||
ref_clip.set('name', _sanitize_xml_value(name, 512))
|
||||
ref_clip.set('duration', total_duration.to_fcpxml())
|
||||
spine.insert(insert_idx, ref_clip)
|
||||
|
||||
# Index the new ref-clip
|
||||
compound_id = f"compound_{name}"
|
||||
self.clips[compound_id] = ref_clip
|
||||
|
||||
return ref_clip
|
||||
|
||||
def flatten_compound_clip(
|
||||
self,
|
||||
ref_clip_id: str,
|
||||
) -> List[ET.Element]:
|
||||
"""Flatten a compound clip back into individual spine clips.
|
||||
|
||||
Extracts clips from the compound's inner sequence and places them
|
||||
back in the main spine at the ref-clip's position.
|
||||
|
||||
Args:
|
||||
ref_clip_id: ID of the ref-clip to flatten.
|
||||
|
||||
Returns:
|
||||
List of extracted clip elements now in the main spine.
|
||||
"""
|
||||
spine = self._get_spine()
|
||||
ref_clip = self._require_clip(ref_clip_id)
|
||||
if ref_clip.tag != 'ref-clip':
|
||||
raise ValueError(f"Element is not a ref-clip: {ref_clip_id}")
|
||||
|
||||
media_ref = ref_clip.get('ref', '')
|
||||
ref_offset = self._parse_time(ref_clip.get('offset', '0s'))
|
||||
|
||||
# Find the media resource
|
||||
resources = self.root.find('.//resources')
|
||||
media_elem = None
|
||||
if resources is not None:
|
||||
for m in resources.findall('media'):
|
||||
if m.get('id') == media_ref:
|
||||
media_elem = m
|
||||
break
|
||||
|
||||
if media_elem is None:
|
||||
raise ValueError(f"Media resource not found for ref: {media_ref}")
|
||||
|
||||
inner_spine = media_elem.find('.//spine')
|
||||
if inner_spine is None:
|
||||
raise ValueError("No spine found in compound clip media")
|
||||
|
||||
# Get insert position
|
||||
spine_children = list(spine)
|
||||
insert_idx = spine_children.index(ref_clip)
|
||||
|
||||
# Remove ref-clip from spine
|
||||
spine.remove(ref_clip)
|
||||
if ref_clip_id in self.clips:
|
||||
del self.clips[ref_clip_id]
|
||||
|
||||
# Extract clips from inner spine into main spine
|
||||
extracted = []
|
||||
current_offset = ref_offset
|
||||
for child in list(inner_spine):
|
||||
new_clip = copy.deepcopy(child)
|
||||
new_clip.set('offset', current_offset.to_fcpxml())
|
||||
spine.insert(insert_idx, new_clip)
|
||||
insert_idx += 1
|
||||
extracted.append(new_clip)
|
||||
current_offset = current_offset + self._parse_time(
|
||||
child.get('duration', '0s')
|
||||
)
|
||||
|
||||
# Index the extracted clip
|
||||
clip_name = new_clip.get('name') or new_clip.get('id') or f"flat_{len(self.clips)}"
|
||||
self.clips[clip_name] = new_clip
|
||||
|
||||
# Clean up media resource
|
||||
if resources is not None:
|
||||
resources.remove(media_elem)
|
||||
|
||||
return extracted
|
||||
|
||||
# ========================================================================
|
||||
Reference in New Issue
Block a user