Marca cada título gerado (dynamic/plain) em metadata para que regenerar substitua a saída anterior em vez de empilhar, e usa os spans de ênfase revisados (não os segmentos brutos do Whisper) como janela da composição dinâmica, evitando que ela invada o trecho de legenda comum seguinte. suppress_plain_under_dynamic corta qualquer sobra visível como rede de segurança. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
12 KiB
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.pytinha 4.199 linhas emodels.py1.091, cada um com muitos assuntos dentro. Viraram pacotes com um módulo por assunto. Do lado de fora nada mudou:from .writer import FCPXMLModifierefrom .models import TimeValueseguem valendo, porque os__init__.pyreexportam 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) |
speaker_review.py |
207 | Revisão de falantes (etapa 3 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 |
319 | 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 |
867 | Títulos de texto e legendas dinâmicas |
cut.py |
333 | Dividir, cortar faixas, apagar |
speed.py |
94 | Velocidade de reprodução |
zoom.py |
204 | Zoom (punch-in) via clipe de ajuste conectado |
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 |
90 | FCPXMLWriter — orquestra a criação do zero (estado + delegação) |
builders.py |
— | Um builder por tipo de elemento: FormatBuilder, AssetBuilder, MarkerBuilder, KeywordBuilder, ClipBuilder, SequenceBuilder, LibraryBuilder |
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 ◄── a análise crua
▼
speaker_review.py (opcional) filtra falante mutado + linha riscada
→ _voice_timeline_clean.json ◄── é isto que a IA prefere
▼
[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.
speaker_review.py — a triagem antes da IA
Roda logo após analyze_voice (etapa 3 do assistente, tela SpeakerReviewView
no app): lista quem foi detectado (speaker_profiles, com % de fala e falas
de amostra) e a transcrição segmento a segmento, para o usuário nomear cada
falante, mutar quem não interessa (ex.: o entrevistador) e riscar linhas soltas
antes de qualquer IA ver o arquivo. build_speaker_review nunca toca a
timeline crua; apply_speaker_review/write_clean_voice_timeline produzem
uma cópia separada, _voice_timeline_clean.json, reaproveitando
enrich_words/_segment_rows/_summary de voice_timeline.py para
recalcular a ênfase só sobre quem sobrou — mesma lógica de restrict_to_kept,
por falante/segmento em vez de por intervalo de tempo. O merge de decisões
salvas segue o padrão de phrase_review.merge_saved_decisions: sempre
reconstrói da análise atual, só as escolhas humanas persistem.
A skill "editar-por-voz", generate_voice_script e cmd_build_phrase_review
(etapa 4/5, admin/api/review.py) preferem o _clean quando ele existe; sem
revisão salva, seguem lendo o _voice_timeline.json normal — a etapa 3 é
sempre opcional. Os três pontos de leitura precisam concordar nessa
preferência: se um deles voltar a ler o arquivo cru direto, a revisão de
falantes vira letra morta sem nenhum erro visível (ver 05_EXPERIENCIAS.md).
Cada linha em build_speaker_review carrega suas words originais (ênfase
por palavra), para a tela desenhar os mesmos chips da etapa 5 sem esperar um
recálculo. apply_speaker_review é reaproveitada por dois caminhos: gravar
(write_clean_voice_timeline, via save_speaker_review) e só prever
(cmd_recalc_speaker_review, sem tocar disco) — o botão "Recalcular" da tela
usa o segundo caminho para atualizar a ênfase só sobre quem sobreviveu ao
corte, sem reprocessar áudio.
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.
Separação de role entre legendas dinâmicas e convencionais
As duas categorias são ambas <title> conectados, mas recebem roles
diferentes para ficarem didáticas na timeline do FCP (cada role ganha cor
própria no índice). O atributo usado em <title> é role (CDATA) — nunca
videoRole, que é DTD-inválido para títulos (ver 05_EXPERIENCIAS.md,
entrada 32).
| Categoria | role |
De onde vem |
|---|---|---|
| Legendas dinâmicas | titles.dinamicas |
DynamicSubtitleConfig.role / load_dynamic_subtitle_config()["role"] |
| Legendas convencionais | titles.convencionais |
load_plain_subtitle_config()["role"] |
A cor do texto em si continua nos configs de fonte (abas do app), não no
role. Os geradores generate_dynamic_subtitles (writer/titles.py),
handle_generate_plain_subtitles e handle_generate_subtitles_by_emphasis
(server_tools/subtitles.py) aplicam o role em cada <title> criado; o
parâmetro role das ferramentas MCP sobrescreve o default.
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.<asset-clip>(biblioteca) é diferente de<clip>(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". idde<text-style-def>precisa ser XML Name válido — acento, espaço ou dígito inicial fazem o FCP recusar o arquivo inteiro.code/examples/sample.fcpxmlnão é DTD-conformante. Não use como fixture de validade.