docs: varredura geral, documentação por função e regra de atualização

A documentação descrevia um sistema que não existe mais: 62/73 ferramentas
(são 74), writer.py e models.py como arquivos (viraram pacotes), 1032 testes
(são 1454), models_api.py descrito como "API FastAPI" (é ponte JSON) e o app
SwiftUI ausente por completo — 5.500 linhas que o usuário opera todo dia sem
uma linha de documentação.

Cada arquivo passa a ter uma função específica, com cabeçalho de escopo
dizendo o que cobre e o que NÃO cobre (com a seta para quem cobre). O objetivo
é ler só o necessário: doc fora do assunto custa tempo e processamento sem
entregar nada.

    01 arquitetura   camadas, duas portas de entrada, regras transversais
    02 módulos       mapa do engine, incluindo o pipeline de voz
    03 server/tools  as 74 tools, helpers e como criar uma nova
    08 app macOS     NOVO — build por swiftc, telas, ponte, etapa 5
    09 manutenção    NOVO — por onde começar, o que está aberto, sintoma→arquivo

CLAUDE.md ganha a seção "Documentação (MANDATORY)": tabela de roteamento
(qual arquivo abrir para cada tarefa) e a regra de que toda alteração de
código atualiza a doc no mesmo commit, com o mapa de o-que-mexeu → o-que-
atualizar. Doc velha engana mais que doc ausente.

O índice do 05_EXPERIENCIAS subiu para o topo: consultar "isso já quebrou
antes?" custava carregar 1.281 linhas antes de chegar na tabela.

Dívidas levantadas na varredura e registradas em 09 §2: etapa 6 ainda ignora
o phrase_review.json, offset de ~400ms do Whisper, MacApp sem teste, admin/
fora do lint, confirmações visuais pendentes no FCP, submódulo WHISPERX sujo.

Também corrigidos dois links quebrados no Engine/README que apontavam um
nível acima do certo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
João Henrique
2026-08-19 22:51:33 -04:00
co-authored by Claude Opus 5
parent ffaebb3f72
commit dcdd73edb5
10 changed files with 881 additions and 261 deletions
+173 -96
View File
@@ -1,114 +1,191 @@
# 02 — Módulos do Engine (`fcpxml/`)
Guia módulo a módulo do núcleo Python. Tamanho em linhas, responsabilidade e as
funções/classes públicas de cada um. APIs públicas são reexportadas em
`fcpxml/__init__.py` (fonte da verdade para o `__all__`).
> **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)
## Versão atual
`__version__ = "0.6.35"` — ver `fcpxml/__init__.py`.
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.
---
| Módulo | Linhas | Papel |
|--------|-------:|-------|
| `models.py` | 930 | Data classes e enums (tempo, clips, markers, QC) |
| `parser.py` | 367 | FCPXML → objetos Python |
| `writer.py` | 3154 | Edição e escrita de FCPXML (o maior) |
## 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 |
| `dtd.py` | 112 | Validação contra DTDs oficiais |
| `safe_xml.py` | 113 | Wrappers `defusedxml` + `serialize_xml()` |
| `media_intel.py` | 173 | Silêncio (ffmpeg) e beats (librosa) |
| `transcribe.py` | 184 | Transcrição Whisper + edição por transcrição |
| `model_manager.py` | 298 | Gestão de modelos Whisper (cache/catálogo) |
| `export.py` | 226 | Export DaVinci Resolve v1.9 + FCP7 XMEML v5 |
| `diff.py` | 269 | Comparação de timelines |
| `live.py` | 273 | Modo Live — push_to_fcp / list_fcp_libraries |
| `model_manager.py` | 748 | Modelos Whisper: catálogo, download, config |
| `voice_timeline.py` | 594 | 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 |
| `__init__.py` | 139 | Reexporta API pública |
| `parser.py` | 367 | FCPXML → objetos Python |
| `transcribe.py` | 300 | Transcrição Whisper e corte por texto |
| `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 |
---
## `models.py` — modelos e enums
Single source of truth para estrutura de dados. NUNCA mexa aqui sem rodar
`test_models.py`.
## `writer/` — edição e escrita
- **Tempo:** `TimeValue` (fração racional), `Timecode`.
- **Clips:** `Clip`, `VideoClip`, `AudioClip`, `ConnectedClip` (lane),
`CompoundClip`, `Transition`.
- **Contêineres:** `Timeline`, `Project`, `Keyword`.
- **Markers:** `Marker`, `MarkerType`, `MarkerColor`, `MARKER_XML_TAGS`.
`MarkerType` é o dono da serialização (`from_string`/`from_xml_element`/`xml_attrs`).
Match estrito do atributo `completed` (`'0'`/`'1'`, sem padding).
- **QC:** `SilenceCandidate`, `FlashFrame`, `GapInfo`, `DuplicateGroup`,
`ValidationIssue`, `ValidationResult`.
- **Geração:** `SegmentSpec`, `PacingConfig`, `PacingStyle`, `RoughCutResult`.
O `FCPXMLModifier` é montado por **composição de mixins**: um mixin por assunto
editorial, todos operando sobre o mesmo documento e os mesmos índices.
## `parser.py` — leitura
- `parse_fcpxml(path)` → `Project`.
- `FCPXMLParser` — lê spine, connected clips (lanes), secondary storylines, roles.
| 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 |
## `writer.py` — o coração (3154 linhas)
Duas classes principais:
**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.
- **`FCPXMLModifier`** — edita documento existente de forma index-based
(dicts de `clips`/`resources`/`formats`), imune a ambiguidade de nomes duplicados.
Métodos: `insert_clip`, `add_marker`, `trim_clip`, `delete_clip`, `split_clip`,
`change_speed`, `cut_clip_ranges` (usado pela remoção de silêncio), etc.
- **`FCPXMLWriter`** — gera FCPXML novo a partir de objetos Python.
Helpers de nível de arquivo: `modify_fcpxml`, `add_marker_to_file`,
`trim_clip_in_file`, `build_marker_element`, `write_fcpxml`, `validate_fcpxml`,
`list_effects`, `FCP_EFFECTS`.
## `rough_cut.py` — geração
- `RoughCutGenerator`, `generate_rough_cut`, `generate_segmented_rough_cut`.
## `media_intel.py` — inteligência de mídia (v0.10)
- Silêncio via `ffmpeg silencedetect` (subprocess limitado), `remove_silence_candidates`,
mapeamento source→timeline.
- Beats via `librosa` (import lazy, extra `[intelligence]`).
- Degrada para `None` quando `ffmpeg` ausente.
## `transcribe.py` — Whisper local
- `transcribe(media_path, model_size, language)` → dict com `words` (spans).
- `ALLOWED_MODELS` — allowlist de nomes de modelo (também usado por `model_manager`).
- Edição por transcrição: remove filler words, aparar por transcrição.
## `model_manager.py` — gestão de modelos
Catálogo `models.json` + cache no HF hub. Config em `~/.fcp-mcp-server/config.json`.
Funções: `get/save_models_dir`, `list_installed_models`, `download_model`,
`delete_model`, `get/load_selected_model`, `save_selected_model`, `load_catalog`.
Permite cancelamento de download via `threading.Event`. Segue convenções:
allowlist, lazy imports, degradação graciosa.
## `export.py` — cross-NLE
- `DaVinciExporter` — FCPXML v1.9 p/ DaVinci Resolve.
- Export FCP7 XMEML v5.
## `diff.py` — comparação
- `compare_timelines`, `TimelineDiff`, `ClipDiff`, `MarkerDiff`.
- Detecta added/removed/moved/trimmed clips & markers.
## `live.py` — FCP ao vivo (macOS)
- `push_to_fcp(path, library, options)` — Apple event *Open Document* + `<import-options>`.
Requer `.fcpbundle` p/ zero-click real.
- `list_fcp_libraries()` — AppleScript read-only.
## `templates.py`
- `Template`, `TemplateSlot`, `ClipSpec`, `BUILTIN_TEMPLATES`, `apply_template`,
`list_templates`. Estruturas prontas: intro/outro, lower thirds, music video.
## `safe_xml.py`
Wrappers `defusedxml` centralizados + `serialize_xml()`. Todo parse/escrita passa aqui.
## `dtd.py`
Valida output contra DTDs oficiais no bundle do FCP (via `xmllint`; exige o caminho
do DTD percent-encoded por causa dos espaços em "Final Cut Pro.app").
**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.
---
## Como adicionar um módulo novo
1. Criar `fcpxml/<seu_modulo>.py` — função pura, sem conhecer MCP.
2. Reexportar classes/funções em `fcpxml/__init__.py` (`__all__`).
3. Cobrir em `tests/test_<seu_modulo>.py`.
4. Rodar `./Engine/run_after_fix.sh`.
## `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.