"""Aparência das legendas dinâmicas: paleta, look por palavra, configuração. Extraído de models.py — ver fcpxml/models/__init__.py. """ from dataclasses import dataclass, field from typing import Optional from ..text_layout import REFERENCE_BLOCK_LINE_GAP, TEXT_TEMPLATE_FONT_SCALE # The palette and type treatment of the calibration export # ("Exemplo Letra.fcpxmld", sentence "Toda a minha vida, assim,"), copied # verbatim from what the user set in Final Cut's Inspector. COLOR_INDIGO = "0.156863 0 0.596079 1" COLOR_YELLOW = "0.997808 0.882664 0.0388632 1" COLOR_GREY = "0.7 0.7 0.7 1" COLOR_WHITE = "1 1 1 1" @dataclass class WordLook: """How one word is set: size, colour and type treatment. A sentence cycles through a tuple of these, so its typography reads with a deliberate rhythm rather than a uniform block. """ font_size: int color: str font: str = "Helvetica Neue" face: Optional[str] = None # Final Cut's fontFace, e.g. "Light Italic" kerning: float = 2.048 @property def italic(self) -> bool: return bool(self.face) and "italic" in self.face.lower() # One entry per word of the reference sentence, in order: # Toda(170, indigo, Helvetica Light) a(128, yellow) minha(151, grey) # vida,(128, white) assim,(128, grey, Light Italic) REFERENCE_RHYTHM = ( WordLook(170, COLOR_INDIGO, font="Helvetica", face="Light", kerning=2.72), WordLook(128, COLOR_YELLOW), WordLook(151, COLOR_GREY, kerning=2.416), WordLook(128, COLOR_WHITE), WordLook(128, COLOR_GREY, face="Light Italic"), ) # The progressive-composition look (reference: the reel the user sent, # 2026-08-17). Supporting text in a small grotesque, the sentence's key word # large in a display italic, everything white — the two-font contrast IS the # style. Playfair Display ships in the user's ~/Library/Fonts and its real # advance widths are embedded in font_metrics, so the lines can be measured # rather than guessed. Both are plain WordLooks: swap them for any installed # family (a script/calligraphic face for the emphasis, say) and layout follows. EDITORIAL_EMPHASIS_LOOK = WordLook( 230, COLOR_WHITE, font="Playfair Display", face="Medium Italic", kerning=0.0, ) EDITORIAL_BODY_LOOK = WordLook( 88, COLOR_WHITE, font="Helvetica Neue", face="Bold", kerning=1.2, ) @dataclass class WordStyle: """Per-word text styling for dynamic (karaoke-style) subtitles. ``rhythm`` drives size, colour and face, cycling by the word's index within its sentence — deterministic, so regenerating a transcript twice yields the same look. ``font``/``font_size`` are the fallback when ``rhythm`` is empty. """ font: str = "Helvetica Neue" font_size: int = 128 active_color: str = COLOR_WHITE inactive_color: str = COLOR_GREY bold: bool = False kerning: float = 2.048 rhythm: tuple = REFERENCE_RHYTHM # Progressive composition only (granularity="phrase"). emphasis_look: Optional[WordLook] = None body_look: Optional[WordLook] = None def look_for(self, index: int) -> WordLook: """The look for the word at *index* within its sentence.""" if not self.rhythm: return WordLook( self.font_size, self.active_color, font=self.font, kerning=self.kerning, ) return self.rhythm[index % len(self.rhythm)] def look_for_emphasis(self) -> WordLook: """The look for a composition's key word (progressive composition).""" return self.emphasis_look or EDITORIAL_EMPHASIS_LOOK def look_for_body(self) -> WordLook: """The look for a composition's supporting lines.""" return self.body_look or EDITORIAL_BODY_LOOK @dataclass class SubtitlePosition: """Screen position for generated title clips, in FCP title coordinate space.""" x: float = 0.0 y: float = -300.0 alignment: str = "center" # left | center | right @dataclass class DynamicSubtitleConfig: """Options for FCPXMLWriter.generate_dynamic_subtitles(). Dynamic subtitles are animated TITLES, not captions. Both templates below render on the video title lane and never carry a ``subtitles.*`` role — a ``role="subtitles.*"`` would make Final Cut treat them as captions and hide them behind the caption-display toggle. They DO carry a ``titles.*`` sub-role (``role``), which groups them in Final Cut's role index and lanes them with a distinct colour, without ever being mistaken for closed captions. ``animated`` picks the template: True uses "Essencial - Título" (Essential Title), which animates on its own Motion defaults; False uses the static "Título Básico" (Basic Title). Default is True — the animated reveal is the feature's purpose. Words are grouped into sentences and laid out as a compact typographic block: each word becomes its own positioned ````, appearing as it is spoken and accumulating on screen, with every word of a block clearing at the same instant so the sentence vanishes as a whole. ``band_height`` is the fraction of frame height the block may occupy, and ``block_center_y`` its centre in canvas points (negative is below frame centre). The defaults reproduce the calibration export the user built by hand: a block of at most three lines sitting just below centre. A sentence taller than the band splits into successive blocks. """ style: WordStyle = field(default_factory=WordStyle) position: SubtitlePosition = field(default_factory=SubtitlePosition) animated: bool = True band_height: float = 0.22 block_center_y: float = -167.0 # "phrase": one title per LINE of the composition — supporting words # grouped, the key word alone and large (the reference look). "word": one # title per word, the earlier rhythm. granularity: str = "phrase" # Ratio between the template's fontSize space and the canvas-point space # its Position uses. See text_layout.TEXT_TEMPLATE_FONT_SCALE: the "Text" # (Text.moti) template sizes type in frame pixels, so a size chosen in # points renders half as large unless it is converted on the way out. text_scale: float = TEXT_TEMPLATE_FONT_SCALE # Vertical air between stacked lines, in canvas points. Negative values # deliberately overlap the lines — the display italic tucking under the # line above is a real editorial look, and the stacking arithmetic places # ink boxes edge to edge, so a negative gap moves them by exactly that # much rather than colliding unpredictably. line_gap: float = REFERENCE_BLOCK_LINE_GAP # Final Cut role for every title this generator emits. A ``titles.*`` # sub-role (NOT ``subtitles.*``) groups the clips in the role index and # tints their lane, keeping dynamic captions distinct from plain # ones and from Final Cut's own closed-caption toggle. role: str = "titles.dinamicas" # Run the post-generation collision validation (collision.validate_titles) # and refuse to emit when it reports a blocking overlap. Off by default so # generation stays byte-identical to before this flag existed; flip it on # for a guaranteed no-collision export. validate: bool = False