Files
gart/code/Engine/docs/02_MODULES.md
T
João HenriqueandClaude Sonnet 5 7b5aed79ee feat(voz): legenda por ênfase, forced align, IA local e correções de zoom/revisão
Trabalho da branch feat/revisao-enfases: pipeline de edição por voz ganha
alinhamento forçado (whisperx), roteirização por LLM local (Ollama), e a
etapa 5 (revisão de frases) passa a refletir de verdade o que é aplicado.

- generate_subtitles_by_emphasis: legenda comum cobre o clipe inteiro,
  legenda dinâmica só nas frases de ênfase, e a comum é desativada
  (enabled="0") onde a dinâmica cobre, em vez de nunca ser gerada ali.
- validate_subtitle_layout ignora títulos com enabled="0" — corrige falso
  positivo de colisão contra o que está desativado no lugar dele.
- Corrige zoom/marcador sendo descartado quando a borda encosta exatamente
  no início de um corte.
- Etapa 5 do Assistente: recarrega quando as decisões da IA mudam (com
  fresh=true, ignorando a revisão salva antiga) — resolve a dessincronia
  entre "ativa" na tela e o que já foi cortado no FCPXML.
- Etapa "Processar" reaplica as decisões da revisão (_phrase_actions.json)
  antes da cadeia de remoção de silêncio/legendas — antes, desativar uma
  frase na etapa 5 não tinha efeito nenhum no vídeo final.
- Etapa "Concluído" fundida em "Processar" — abrir no Final Cut/Finder
  aparece assim que termina, sem slide extra.
- Palavra clicável na etapa 5 agora funciona como toggle (clique de novo
  desfaz) e mostra a própria ênfase (sublinhado colorido + peso da fonte).
- fcpxml/forced_align.py, fcpxml/llm_local.py, ai_edit.py: alinhamento
  fonético via whisperx e roteirização local via Ollama/Gemma.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 18:26:04 -04:00

193 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
- `<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.