"""FCPXMLWriter: gera um documento novo a partir de objetos Python. Extraído de writer.py — ver fcpxml/writer/__init__.py para o conjunto. """ import uuid import xml.etree.ElementTree as ET from datetime import datetime from ..models import ( Marker, Project, Timecode, ) from .document import write_fcpxml from .helpers import build_marker_element # ============================================================================ # FCPXML GENERATOR - Create from Python objects # ============================================================================ class FCPXMLWriter: """Generate a new FCPXML document from Python dataclass objects. Converts a ``Project`` (containing ``Timeline`` → ``Clip`` → ``Marker`` hierarchies) into a spec-compliant FCPXML v1.11 element tree and writes it to disk. Used by ``RoughCutGenerator`` and the ``generate_*`` MCP tools to create fresh timelines from scratch. Unlike ``FCPXMLModifier`` (which mutates existing XML), this class *creates* XML from structured Python objects. Example:: from fcpxml.models import Project, Timeline, Clip, Timecode project = Project(name="My Edit", timelines=[...]) writer = FCPXMLWriter() writer.write_project(project, "output.fcpxml") """ def __init__(self, version: str = "1.13"): """Initialize writer targeting the given FCPXML version.""" self.version = version self.resource_counter = 1 def _next_resource_id(self) -> str: """Return an auto-incrementing resource ID (r1, r2, ...).""" rid = f"r{self.resource_counter}" self.resource_counter += 1 return rid def _generate_uid(self) -> str: """Generate a unique identifier for FCPXML elements.""" return str(uuid.uuid4()).upper() def _tc_to_rational(self, tc: Timecode) -> str: """Convert a Timecode to FCPXML rational time string (e.g. '48/24s').""" return f"{tc.frames}/{int(tc.frame_rate)}s" def write_project(self, project: Project, filepath: str): """Write a project to an FCPXML file.""" root = self._build_fcpxml(project) write_fcpxml(root, filepath) def _build_fcpxml(self, project: Project) -> ET.Element: """Build the full FCPXML element tree: fcpxml > resources + library > event > project.""" root = ET.Element('fcpxml', version=self.version) resources = ET.SubElement(root, 'resources') resource_map = {} if project.timelines: timeline = project.timelines[0] format_id = self._next_resource_id() ET.SubElement(resources, 'format', id=format_id, name=f"FFVideoFormat{timeline.height}p{int(timeline.frame_rate)}", frameDuration=f"1/{int(timeline.frame_rate)}s", width=str(timeline.width), height=str(timeline.height)) resource_map['_format'] = format_id library = ET.SubElement(root, 'library', location=f"file:///Users/editor/Movies/{project.name}.fcpbundle/") event = ET.SubElement(library, 'event', name=project.name, uid=self._generate_uid()) for timeline in project.timelines: self._add_timeline(event, timeline, resources, resource_map) return root def _add_timeline(self, event, timeline, resources, resource_map): """Add a timeline as a project > sequence > spine structure under the event.""" project_elem = ET.SubElement(event, 'project', name=timeline.name, uid=self._generate_uid(), modDate=datetime.now().strftime("%Y-%m-%d %H:%M:%S -0500")) format_id = resource_map.get('_format', 'r1') sequence = ET.SubElement(project_elem, 'sequence', format=format_id, duration=self._tc_to_rational(timeline.duration), tcStart="0s", tcFormat="NDF", audioLayout="stereo", audioRate="48k") spine = ET.SubElement(sequence, 'spine') for clip in timeline.clips: self._add_clip(spine, clip, resources, resource_map) for marker in timeline.markers: self._add_marker(sequence, marker) def _add_clip(self, spine, clip, resources, resource_map): """Add a clip as an asset-clip element, creating its asset resource if needed.""" if clip.media_path and clip.media_path not in resource_map: asset_id = self._next_resource_id() ET.SubElement(resources, 'asset', id=asset_id, name=clip.name, uid=self._generate_uid(), src=clip.media_path, start="0s", duration=self._tc_to_rational(clip.duration), hasVideo="1", hasAudio="1") resource_map[clip.media_path] = asset_id asset_id = resource_map.get(clip.media_path, 'r1') format_id = resource_map.get('_format', 'r1') clip_elem = ET.SubElement(spine, 'asset-clip', ref=asset_id, offset=self._tc_to_rational(clip.start), name=clip.name, start=self._tc_to_rational(clip.source_start) if clip.source_start else "0s", duration=self._tc_to_rational(clip.duration), format=format_id, tcFormat="NDF") for marker in clip.markers: self._add_marker(clip_elem, marker) for keyword in clip.keywords: self._add_keyword(clip_elem, keyword) def _add_marker(self, parent: ET.Element, marker: Marker): """Add a marker or chapter-marker element to a parent clip or sequence.""" build_marker_element( parent=parent, marker_type=marker.marker_type, start=self._tc_to_rational(marker.start), duration=self._tc_to_rational(marker.duration) if marker.duration else "1/24s", name=marker.name, note=marker.note or None, ) def _add_keyword(self, parent, keyword): """Add a keyword element with optional start/duration range to a parent clip.""" attrs = {'value': keyword.value} if keyword.start: attrs['start'] = self._tc_to_rational(keyword.start) if keyword.duration: attrs['duration'] = self._tc_to_rational(keyword.duration) ET.SubElement(parent, 'keyword', **attrs)