# 02 — Módulos do Engine (`fcpxml/`) > **Escopo:** Mapa do engine `fcpxml/`: qual módulo faz o quê e onde mexer. > **Não cobre:** Camadas e regras gerais (→ 01) · handlers MCP (→ 03) · o que está aberto (→ 09) Mapa módulo a módulo do núcleo Python: onde cada coisa mora e o que ela faz. A API pública é reexportada em `fcpxml/__init__.py` — essa é a fonte da verdade do `__all__`. Versão: `0.6.35` · Última varredura: 2026-08-19 > **Por que existem pacotes aqui.** `writer.py` tinha 4.199 linhas e `models.py` > 1.091, cada um com muitos assuntos dentro. Viraram pacotes com um módulo por > assunto. Do lado de fora **nada mudou**: `from .writer import FCPXMLModifier` > e `from .models import TimeValue` seguem valendo, porque os `__init__.py` > reexportam tudo — inclusive os nomes com underscore que a suíte usa. --- ## Visão geral | Módulo / pacote | Linhas | Papel | |-----------------|-------:|-------| | `writer/` | 4.687 | **Edição e escrita de FCPXML** — o coração | | `models/` | 1.195 | Data classes e enums | | `text_layout.py` | 901 | Diagramação das legendas dinâmicas | | `rough_cut.py` | 798 | Geração de timelines novas | | `model_manager.py` | 748 | Modelos Whisper: catálogo, download, config | | `voice_timeline.py` | 600 | O JSON de voz que a IA lê | | `phrase_review.py` | 547 | Revisão de frases (etapa 5 do assistente) | | `collision.py` | 472 | Colisão entre títulos na tela | | `font_metrics.py` | 445 | Largura real de glifos por fonte | | `templates.py` | 387 | Templates de timeline | | `parser.py` | 367 | FCPXML → objetos Python | | `transcribe.py` | 332 | Transcrição Whisper e corte por texto | | `forced_align.py` | 181 | Alinhamento forçado opcional (whisperx/wav2vec2) que corrige o viés de ~0,4s no início das palavras | | `live.py` | 273 | Modo Live (push_to_fcp) | | `diff.py` | 269 | Comparação de timelines | | `voice_actions.py` | 263 | Decisões de edição (cut/zoom/text/marker) | | `export.py` | 226 | Export Resolve v1.9 + FCP7 XMEML v5 | | `voice_features.py` | 220 | Pitch, energia, ritmo, pausas | | `diarize.py` | 180 | Quem falou (pyannote) | | `media_intel.py` | 177 | Silêncio (ffmpeg) e beats (librosa) | | `emphasis.py` | 133 | Índice de ênfase por palavra | | `safe_xml.py` | 113 | `defusedxml` + `serialize_xml()` | | `dtd.py` | 112 | Validação contra os DTDs da Apple | --- ## `writer/` — edição e escrita O `FCPXMLModifier` é montado por **composição de mixins**: um mixin por assunto editorial, todos operando sobre o mesmo documento e os mesmos índices. | Módulo | Linhas | Conteúdo | |--------|-------:|----------| | `core.py` | 723 | `ModifierCore`: carga, índices, navegação na spine, `save` | | `titles.py` | 600 | Títulos de texto e legendas dinâmicas | | `cut.py` | 333 | Dividir, cortar faixas, apagar | | `speed.py` | 297 | Velocidade e zoom (punch-in) | | `helpers.py` | 279 | Sanitização, escalas, construtores de elemento | | `rapid.py` | 240 | Flash frames, rapid trim, preencher buracos | | `validation.py` | 232 | Verificações estruturais antes de salvar | | `compound.py` | 196 | Compound clips: criar e achatar | | `silence.py` | 185 | Detectar e remover silêncio | | `document.py` | 170 | Assets de vídeo, timebases, `write_fcpxml` | | `markers.py` | 165 | Marcadores: um, por timecode, em lote | | `audio.py` | 162 | Clipes de áudio e cama musical | | `generator.py` | 147 | `FCPXMLWriter` — cria documento do zero | | `reorder.py` | 126 | Reordenar e recalcular offsets | | `trim.py` | 125 | Aparar e propagar o ripple | | `transitions.py` | 94 | Transições entre vizinhos | | `relink.py` | 94 | Repontar mídia | | `insert.py` | 78 | Inserir clipes na spine | | `modifier.py` | 64 | Monta a classe a partir dos mixins | | `selection.py` | 57 | Selecionar por palavra-chave | | `api.py` | 55 | Atalhos de uma linha | | `connected.py` | 49 | Clipes conectados (lanes) | | `roles.py` | 43 | Atribuir roles | | `reformat.py` | 43 | Reenquadrar resolução | **Onde mexer:** ache o assunto na tabela e abra só aquele arquivo. Se a sua mudança precisa de dois mixins ao mesmo tempo, provavelmente o que você quer é um método novo no `core.py` que os dois chamem. **Cuidado:** os mixins compartilham `self`. Um método novo que colida de nome com outro mixin sobrescreve em silêncio — a ordem em `modifier.py` decide quem ganha. Hoje nenhum colide; mantenha assim. --- ## `models/` — dados e enums Fonte única da estrutura de dados. **Nunca mexa aqui sem rodar `test_models.py`.** | Módulo | Linhas | Conteúdo | |--------|-------:|----------| | `timing.py` | 304 | `TimeValue` (fração racional), `Timecode` | | `timeline.py` | 217 | `Clip`, `ConnectedClip`, `CompoundClip`, `Timeline`, `Project`, `Marker` | | `enums.py` | 183 | `MarkerType`, `MarkerColor`, `TransitionType`, `PacingStyle`… | | `subtitles.py` | 157 | `WordLook`, `WordStyle`, `DynamicSubtitleConfig`, paleta | | `qc.py` | 121 | `FlashFrame`, `GapInfo`, `DuplicateGroup`, `ValidationIssue` | | `planning.py` | 93 | `SegmentSpec`, `PacingConfig`, `RoughCutResult`, `MontageConfig` | `MarkerType` é o dono da serialização de marcador (`from_string`, `from_xml_element`, `xml_attrs`) — não reimplemente isso em outro lugar. --- ## O caminho da voz (do áudio à decisão) Estes seis módulos formam um pipeline. É o fluxo mais novo e o menos óbvio do projeto, então vale ler nesta ordem: ``` transcribe.py áudio → palavras com tempo + diarize.py quem falou cada trecho + voice_features.py pitch, energia, ritmo, pausas ▼ emphasis.py combina tudo num índice 0–1 por palavra ▼ voice_timeline.py monta o _voice_timeline.json ◄── é isto que a IA lê ▼ [decisão: skill "editar-por-voz", ou a mão do usuário] ▼ voice_actions.py valida a lista de cut/zoom/text/marker ▼ phrase_review.py funde tudo em frases revisáveis (etapa 5 do app) ▼ writer/ aplica no FCPXML ``` **Regra de ouro do pipeline:** toda ação carrega tempo da **mídia original**, nunca pós-corte. Cortes deslocam tudo depois deles; resolver o deslocamento só na hora de aplicar (`shift_after_cuts`) elimina uma classe inteira de bug. ### `voice_timeline.py` — o contrato com a IA Saída em camadas, para um modelo raciocinar do topo e descer só onde importa: ``` {version, source, language, layers: {transcript, acoustics, speakers, emotion} ← o que rodou de verdade scales: {…} ← como ler cada número summary: {…} speakers: [...] segments: [{start, end, speaker, text, gap_before, take_boundary, avg_energy, peak_emphasis, emotion, emotion_confidence, words: [{text, start, end, energy, pitch_delta, rate_delta, pause_before, emphasis}]}]} ``` `layers` existe para separar *"a fala é monótona"* de *"a análise acústica nunca carregou"* — os dois deixam os mesmos zeros nos dados. ### `phrase_review.py` — a revisão humana Junta o timeline de voz com as ações da IA numa lista de frases editáveis, e converte de volta. Frase inativa vira `cut`; ênfase ≥ 1 vira `zoom` mais um `emphasis_spans` que a etapa de legendas usa. O trim de cada frase anda em **fronteira de palavra** — cortar é apontar para uma palavra, nunca caçar frame. --- ## Legendas dinâmicas (três módulos que andam juntos) | Módulo | Papel | |--------|-------| | `text_layout.py` | Quebra a frase em linhas e posiciona cada palavra | | `font_metrics.py` | Largura real de cada glifo na fonte escolhida | | `collision.py` | Detecta título saindo do quadro ou colidindo com outro | Estes três não estão divididos porque **cada um já é um assunto só**. O `text_layout.py` tem 901 linhas de um problema coeso: diagramação. --- ## Armadilhas do FCPXML (custaram sessões de depuração) - Tempo é fração: `"3600/2400s"` = 1,5 s. - `offset` é posição na timeline; `start` é o in-point da mídia. - `` (biblioteca) é diferente de `` (timeline). - Marcadores são **filhos** do clipe, não irmãos. - `.fcpxmld` é um **diretório** — sidecars precisam ser copiados no save, ou dados de object tracking e Cinematic são destruídos. - Negrito no FCP é `bold="1"` (atributo); itálico é `fontFace` + `italic="1"`. - `id` de `` precisa ser XML Name válido — acento, espaço ou dígito inicial fazem o FCP recusar o arquivo inteiro. - `code/examples/sample.fcpxml` **não** é DTD-conformante. Não use como fixture de validade.