"""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 ``