"""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 resource with a nested containing the specified clips, then replaces the originals in the main spine with a single . Args: clip_ids: IDs of clips in the spine to group. name: Name for the compound clip. Returns: The created element. """ spine = self._get_spine() resources = self.root.find('.//resources') if resources is None: raise ValueError("No 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 # ========================================================================