"""O que existe numa timeline: clipes, marcadores, lanes, projeto. Extraído de models.py — ver fcpxml/models/__init__.py. """ from dataclasses import dataclass, field from typing import List, Optional from .enums import MarkerColor, MarkerType from .timing import Timecode @dataclass class Keyword: """Represents a keyword/tag applied to a clip.""" value: str start: Optional[Timecode] = None duration: Optional[Timecode] = None @dataclass class ParametroEfeito: """Um parâmetro de um filtro de efeito (```` dentro do filtro).""" nome: str valor: str chave: str = "" metadado: str = "" @dataclass class EfeitoAjuste: """Um efeito aplicado por uma camada de ajuste (adjustment layer). ``uid`` é o UUID do efeito interno do Final Cut (ver ``FCP_EFFECTS`` em ``fcpxml/writer/helpers.py`` para os efeitos built-in). ``tipo`` é ``"video"`` ou ``"audio"`` — decide se vira ```` ou ````, filho direto do ```` da camada de ajuste (o DTD não define wrapper ````). """ nome: str uid: str tipo: str = "video" parametros: List[ParametroEfeito] = field(default_factory=list) @dataclass class Marker: """Represents a marker in the timeline.""" name: str start: Timecode duration: Optional[Timecode] = None marker_type: MarkerType = MarkerType.STANDARD note: str = "" color: Optional[MarkerColor] = None def to_youtube_timestamp(self) -> str: """Format as YouTube chapter timestamp.""" total_seconds = int(self.start.seconds) hours = total_seconds // 3600 minutes = (total_seconds % 3600) // 60 secs = total_seconds % 60 if hours > 0: return f"{hours}:{minutes:02d}:{secs:02d}" return f"{minutes}:{secs:02d}" @dataclass class Clip: """Represents a clip in the timeline.""" name: str start: Timecode duration: Timecode source_start: Optional[Timecode] = None source_end: Optional[Timecode] = None media_path: str = "" markers: List[Marker] = field(default_factory=list) keywords: List[Keyword] = field(default_factory=list) # Extended metadata rating: int = 0 # 0=unrated, 1-5 stars is_favorite: bool = False is_rejected: bool = False # Roles (FCP audio/video role assignments) audio_role: str = "" video_role: str = "" # Connected clips (B-roll, titles, audio attached to this clip) connected_clips: List['ConnectedClip'] = field(default_factory=list) # Edit-time correction, in degrees, from a Transform filter on the clip # (e.g. straightening a tilted phone shot) — not the camera's own # recorded orientation, which lives in the media file itself. rotation: float = 0.0 @property def end(self) -> Timecode: return Timecode( frames=self.start.frames + self.duration.frames, frame_rate=self.start.frame_rate ) @property def duration_seconds(self) -> float: return self.duration.seconds @property def keyword_values(self) -> List[str]: """Get list of keyword strings.""" return [k.value for k in self.keywords] @dataclass class AudioClip(Clip): """Audio-specific clip.""" channels: int = 2 sample_rate: int = 48000 role: str = "dialogue" @dataclass class VideoClip(Clip): """Video-specific clip.""" width: int = 1920 height: int = 1080 has_audio: bool = True @dataclass class ConnectedClip: """A clip connected to a primary storyline clip (B-roll, titles, audio). In FCP's magnetic timeline, connected clips hang off spine clips via lanes. Positive lanes are above (video overlays), negative lanes are below (audio). """ name: str start: Timecode duration: Timecode lane: int = 1 offset: Optional[Timecode] = None source_start: Optional[Timecode] = None media_path: str = "" clip_type: str = "asset-clip" role: str = "" ref_id: str = "" parent_clip_name: str = "" markers: List[Marker] = field(default_factory=list) keywords: List[Keyword] = field(default_factory=list) rotation: float = 0.0 @property def duration_seconds(self) -> float: return self.duration.seconds @dataclass class CompoundClip: """A compound clip (ref-clip) containing a nested timeline.""" name: str ref_id: str duration: Timecode start: Timecode clips: List[Clip] = field(default_factory=list) connected_clips: List[ConnectedClip] = field(default_factory=list) @property def duration_seconds(self) -> float: return self.duration.seconds @dataclass class SilenceCandidate: """A potential silence region detected by timeline heuristics.""" start_timecode: str duration_seconds: float reason: str # "gap", "ultra_short", "name_match", "duration_anomaly" confidence: float = 0.5 # 0.0 to 1.0 clip_name: Optional[str] = None clip_index: Optional[int] = None @dataclass class Transition: """Represents a transition between clips.""" name: str duration: Timecode start: Timecode transition_type: str = "cross-dissolve" @dataclass class Timeline: """Represents a Final Cut Pro timeline/sequence.""" name: str duration: Timecode frame_rate: float = 24.0 width: int = 1920 height: int = 1080 clips: List[Clip] = field(default_factory=list) audio_clips: List[AudioClip] = field(default_factory=list) transitions: List[Transition] = field(default_factory=list) markers: List[Marker] = field(default_factory=list) connected_clips: List[ConnectedClip] = field(default_factory=list) compound_clips: List[CompoundClip] = field(default_factory=list) @property def total_clips(self) -> int: return len(self.clips) @property def total_cuts(self) -> int: return max(0, len(self.clips) - 1) @property def average_clip_duration(self) -> float: if not self.clips: return 0.0 return sum(c.duration_seconds for c in self.clips) / len(self.clips) @property def cuts_per_minute(self) -> float: """Average cuts per minute.""" if self.duration.seconds <= 0: return 0.0 return (self.total_cuts / self.duration.seconds) * 60 def get_clips_shorter_than(self, seconds: float) -> List[Clip]: """Find clips shorter than threshold (flash frame detection).""" return [c for c in self.clips if c.duration_seconds < seconds] def get_clips_longer_than(self, seconds: float) -> List[Clip]: """Find clips longer than threshold.""" return [c for c in self.clips if c.duration_seconds > seconds] def get_clip_at(self, timecode: float) -> Optional[Clip]: """Find the clip at a specific timecode (seconds).""" for clip in self.clips: start_sec = clip.start.seconds end_sec = clip.end.seconds if start_sec <= timecode < end_sec: return clip return None def get_clips_by_keyword(self, keyword: str) -> List[Clip]: """Find all clips with a specific keyword.""" return [c for c in self.clips if keyword in c.keyword_values] @dataclass class Project: """Represents a Final Cut Pro project/library.""" name: str timelines: List[Timeline] = field(default_factory=list) fcpxml_version: str = "1.13" @property def primary_timeline(self) -> Optional[Timeline]: return self.timelines[0] if self.timelines else None