Files
gart/code/Engine/docs/02_MODULES.md
T
João HenriqueandClaude Sonnet 5 0fdfe33613 fix(legendas): impede legenda comum sob composição dinâmica e duplicação ao regerar
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>
2026-09-22 21:51:02 -04:00

12 KiB
Raw Blame History

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)
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".
  • id de <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.fcpxml não é DTD-conformante. Não use como fixture de validade.