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
+79 -13
View File
@@ -9,25 +9,91 @@ normalmente; a regra é sobre a comunicação com o usuário.
## What This Is
MCP server that reads/writes Final Cut Pro XML (FCPXML) files. 73 tools for timeline analysis, batch editing, QC, generation, multi-track support, media relink, NLE export, transcript-based editing (local Whisper), and LIVE FCP control (push_to_fcp / list_fcp_libraries via Apple events). Reads FCPXML 1.8–1.14 (incl. `.fcpxmld` bundles with sidecar preservation), writes 1.13 by default. Dual-mode (XML + Live) direction: `code/docs/CAPABILITY-AUDIT-2026-06.md`.
MCP server that reads/writes Final Cut Pro XML (FCPXML) files. 74 tools for timeline analysis, batch editing, QC, generation, multi-track support, media relink, NLE export, transcript-based editing (local Whisper), and LIVE FCP control (push_to_fcp / list_fcp_libraries via Apple events). Reads FCPXML 1.8–1.14 (incl. `.fcpxmld` bundles with sidecar preservation), writes 1.13 by default. Dual-mode (XML + Live) direction: `code/docs/CAPABILITY-AUDIT-2026-06.md`.
## Architecture
Toda a estrutura do projeto fica em `code/`. A pasta `admin/` fica na raiz (fora de `code/`).
```
code/server.py — MCP server entry point. All 62 tool definitions, handlers, resources, prompts.
Dispatch dict pattern: TOOL_HANDLERS maps tool names → async handler functions.
Há **duas portas de entrada** para o mesmo engine: o MCP (Claude decide a
edição) e a ponte JSON (o app macOS opera). Nenhuma das duas tem lógica de
timeline — as duas delegam a `fcpxml/`.
code/fcpxml/parser.py — Reads FCPXML → Python objects (Timeline, Clip, ConnectedClip, Marker, etc.)
code/fcpxml/writer.py — Writes modifications back to FCPXML. Handles markers, trimming, gaps, transitions.
code/fcpxml/rough_cut.py — Generates new timelines from source clips (rough cuts, montages, A/B rolls).
code/fcpxml/diff.py — Timeline comparison engine. Detects added/removed/moved/trimmed clips & markers.
code/fcpxml/export.py — DaVinci Resolve FCPXML v1.9 export + FCP7 XMEML v5 export for cross-NLE workflows.
code/fcpxml/models.py — Data classes: TimeValue, Timecode, Clip, ConnectedClip, CompoundClip, Timeline, etc.
code/fcpxml/media_intel.py — Real media analysis. Audio silence detection + beat detection.
code/fcpxml/dtd.py — Validates output against Apple's official DTDs.
```
code/server.py — MCP entry point (592 linhas). Só dispatch: TOOL_HANDLERS.
code/server_tools/ — Os handlers das 74 tools, um módulo por categoria.
code/server_tools/_shared/ — Helpers compartilhados (paths, project, formatting,
captions, detection, media).
code/fcpxml/parser.py — Reads FCPXML → Python objects (Timeline, Clip, Marker…)
code/fcpxml/writer/ — PACOTE. Edição/escrita de FCPXML. FCPXMLModifier é
montado por mixins, um por assunto (markers, trim,
speed, titles, cut, silence…). Ver writer/modifier.py.
code/fcpxml/models/ — PACOTE. Data classes por família: timing, timeline,
enums, subtitles, qc, planning.
code/fcpxml/rough_cut.py — Generates new timelines (rough cuts, montages, A/B rolls).
code/fcpxml/diff.py — Timeline comparison engine.
code/fcpxml/export.py — DaVinci Resolve v1.9 + FCP7 XMEML v5 export.
code/fcpxml/media_intel.py — Silence detection + beat detection.
code/fcpxml/dtd.py — Validates output against Apple's official DTDs.
code/fcpxml/voice_*.py — Pipeline de voz: features → emphasis → voice_timeline
→ voice_actions → phrase_review. Ver Engine/docs/02.
admin/models_api.py — Ponte JSON com o app: docstring de comandos + dispatch.
admin/api/ — Os 37 comandos, um módulo por assunto.
code/MacApp/Sources/ — App SwiftUI. Compilado por swiftc (sem Xcode/SPM).
```
Os dois `__init__.py` de pacote (`writer/`, `models/`) reexportam tudo, então
`from .writer import FCPXMLModifier` e `from .models import TimeValue` seguem
valendo em todo o projeto.
## Documentação (MANDATORY)
A documentação viva fica em `code/Engine/docs/`. Cada arquivo tem **uma função
específica** — leia só o que a tarefa exige, não o conjunto. Carregar
documentação que não é do assunto custa tempo e processamento sem entregar nada.
### Qual arquivo abrir
| Sua tarefa | Abra | Não precisa de |
|-----------|------|----------------|
| Entender como o sistema é dividido | `01_ARCHITECTURE.md` | o resto |
| Achar onde mora uma função do engine | `02_MODULES.md` | 01, 03 |
| Criar/alterar uma ferramenta MCP | `03_SERVER_TOOLS.md` | 08 |
| Entender ou rodar os testes | `04_TESTS_AND_WORKFLOW.md` | — |
| "Isso já quebrou antes?" | `05_EXPERIENCIAS.md` — **só o índice no topo** | as entradas que não são a sua |
| Checklist antes de fechar | `06_BOAS_PRATICAS.md` | — |
| Mexer no app / no Assistente | `08_APP_MACOS.md` | 02, 03 |
| Escolher o que fazer, ver o que está aberto | `09_MANUTENCAO.md` | — |
Quando não souber por onde começar: `09_MANUTENCAO.md`. Ele roteia para o resto.
### Regra de atualização (obrigatória)
**Toda alteração de código atualiza a documentação no mesmo commit.** Doc velha
engana mais do que doc ausente — quem lê confia nela e erra com confiança.
| Você alterou | Atualize |
|--------------|----------|
| Estrutura de pastas, camadas ou dependências | `01_ARCHITECTURE.md` |
| Criou/moveu/dividiu módulo em `fcpxml/` | `02_MODULES.md` (tabela + linhas) |
| Criou/removeu ferramenta MCP | `03_SERVER_TOOLS.md` + contagem no `CLAUDE.md` |
| Comando da ponte | docstring de `admin/models_api.py` + `08_APP_MACOS.md` |
| Tela ou fluxo do app | `08_APP_MACOS.md` |
| Resolveu ou abriu uma dívida | `09_MANUTENCAO.md` §2 |
| Bateu num problema estrutural ou erro recorrente | `05_EXPERIENCIAS.md` + **índice no topo** |
Se um número (tools, testes, linhas) mudou, corrija onde ele aparece. Se um
documento divergir do código, **o código está certo** — conserte o documento.
### Ao escrever documentação
- **Um assunto por arquivo.** Se um doc começar a cobrir dois, divida.
- **Diga o que não está ali** e para onde ir — economiza a leitura seguinte.
- **Fatos verificados**, não suposições: rode o comando e use o número real.
- **Registre o porquê**, não só o quê. O "o quê" está no código; o "por quê"
se perde, e é o que evita alguém desfazer uma decisão por engano.
## Key Patterns
@@ -88,7 +154,7 @@ CI runs both on every push to main. If either fails, the commit gets an X on Git
## Testing
1342 tests across 34 files. `test_models.py` covers TimeValue arithmetic, Timecode parsing/formatting, Clip properties, validation models, and Timeline helpers. `test_writer.py` covers insert_clip, add_marker (all types), trim_clip, delete_clip, split_clip, and change_speed operations. `test_server.py` covers MCP tool handlers, parsers, and dispatch. `test_rough_cut.py` covers RoughCutGenerator. `test_features_v05.py` covers connected clips, roles, timeline diff, reformat, silence detection, export, and backward compatibility. `test_marker_pipeline.py` covers build_marker_element shared builder, batch auto-modes, clip index duplicate-name behavior, and write_fcpxml output format. `test_refactored_helpers.py` covers _index_elements, _iter_spine_clips, _find_spine_clip_at_seconds, _resolve_clip_duration, _make_asset_clip, _format_batch_result, and serialize_xml edge cases. `test_transcribe.py` covers phrase/filler span matching, range merge/invert algebra, whisper graceful degradation, and transcript-driven handler cuts against cached transcripts. `test_media_intel.py` covers silencedetect stderr parsing, source-to-timeline mapping, parameter bounds, and real-WAV ffmpeg integration (skips without ffmpeg; CI installs it). Tests use `examples/sample.fcpxml` as fixture data and inline XML fixtures. Tests create temp files and clean up after.
1454 tests across 42 files, all under `code/tests/`. Um teste fora dessa pasta não roda (`testpaths = ["tests"]`) — se você criar um em outro lugar, confirme que a contagem total subiu. Cobertura por área: `test_models.py` (TimeValue/Timecode/Clip/Timeline), `test_writer.py` (insert/marker/trim/delete/split/speed), `test_server.py` (handlers e dispatch), `test_rough_cut.py`, `test_features_v05.py` (connected clips, roles, diff, reformat, silêncio, export), `test_marker_pipeline.py`, `test_refactored_helpers.py`, `test_transcribe.py`, `test_media_intel.py` (pula sem ffmpeg; o CI instala), `test_phrase_review.py` (revisão de frases da etapa 5) e `test_models_api.py` (comandos da ponte). Fixtures: `examples/sample.fcpxml` e XML inline. Os testes criam temporários e limpam depois.
## FCPXML Gotchas
+43 -32
View File
@@ -10,12 +10,20 @@ opera **fora** do Final Cut Pro: você exporta o XML, o servidor processa o
documento como dados estruturados e devolve um XML modificado para importação.
Nada é patcheado, nenhuma API privada é usada.
Toda a análise foi feita a partir do código-fonte em `server.py` e `fcpxml/`.
Toda a análise foi feita a partir do código-fonte. Este README é a visão
geral; o detalhe módulo a módulo mora em `docs/02_MODULES.md`, que é o
documento a manter atualizado quando a estrutura mudar.
> **Guia rápido:** [01 Arquitetura](docs/01_ARCHITECTURE.md) ·
> **Começando agora?** Leia [01 Arquitetura](docs/01_ARCHITECTURE.md) e depois
> [09 Manutenção](docs/09_MANUTENCAO.md) — o primeiro diz como o sistema é
> dividido, o segundo diz por onde começar a mexer e o que está em aberto.
>
> **Guia completo:** [01 Arquitetura](docs/01_ARCHITECTURE.md) ·
> [02 Módulos](docs/02_MODULES.md) · [03 Server/Tools](docs/03_SERVER_TOOLS.md) ·
> [04 Testes & Workflow](docs/04_TESTS_AND_WORKFLOW.md) ·
> [05 Experiências](docs/05_EXPERIENCIAS.md) · [06 Boas Práticas](docs/06_BOAS_PRATICAS.md)
> [05 Experiências](docs/05_EXPERIENCIAS.md) · [06 Boas Práticas](docs/06_BOAS_PRATICAS.md) ·
> [07 Projeto Ativo no FCP](docs/07_ESTUDO_PROJETO_ATIVO_FCP.md) ·
> [08 App macOS](docs/08_APP_MACOS.md) · [09 Manutenção](docs/09_MANUTENCAO.md)
---
@@ -26,7 +34,7 @@ Toda a análise foi feita a partir do código-fonte em `server.py` e `fcpxml/`.
Python, e reescreve de volta sem perda de sidecars (object tracking,
Cinematic).
2. **Uma camada MCP de 62 ferramentas** — expõe análise, edição em lote, QC,
2. **Uma camada MCP de 74 ferramentas** — expõe análise, edição em lote, QC,
geração, exportação cross-NLE, inteligência de mídia (silêncio/beats) e
edição baseada em transcrição, tudo acessível por um cliente MCP (Claude).
@@ -40,7 +48,7 @@ Toda a análise foi feita a partir do código-fonte em `server.py` e `fcpxml/`.
| Camada | Tecnologia |
|--------|-----------|
| Linguagem | **Python 3.10+** (~7.1k linhas em `server.py` + `fcpxml/`) |
| Linguagem | **Python 3.10+** (~13k linhas em `server.py`, `server_tools/` e `fcpxml/`) |
| Protocolo MCP | **mcp** (`mcp` SDK), servidor por stdio |
| Parsing XML | **defusedxml** em todos os 4 entry points + `lxml`/`ElementTree` |
| Tempo racional | frações `numerador/denominador` no formato `"600/2400s"` |
@@ -56,26 +64,29 @@ Toda a análise foi feita a partir do código-fonte em `server.py` e `fcpxml/`.
```
G-ART/
├── server.py # MCP server — 62 tools, prompts, resources, dispatch
├── fcpxml/ # "Engine" — biblioteca Python de núcleo
│ ├── models.py # TimeValue, Timecode, Clip, Timeline, enums, QC models
│ ├── parser.py # FCPXML → objetos Python (spine, connected clips, roles)
│ ├── writer.py # Modifica e grava FCPXML (markers, trim, gaps, speed)
│ ├── rough_cut.py # Gera timelines novas (rough cuts, montages, A/B)
│ ├── diff.py # Motor de comparação de timelines
│ ├── export.py # Export DaVinci Resolve v1.9 + FCP7 XMEML v5
│ ├── media_intel.py # Detecção real de silêncio (ffmpeg) e beats (librosa)
│ ├── transcribe.py # Transcrição Whisper local + edição por transcrição
│ ├── templates.py # Templates de timeline (intro/outro, lower thirds)
│ ├── live.py # Modo Live — push_to_fcp / list_fcp_libraries
│ ├── safe_xml.py # Wrappers defusedxml + serialize_xml()
│ └── dtd.py # Validação contra DTDs oficiais da Apple
├── Engine/ # Esta documentação da arquitetura
├── admin/ # Scripts de manutenção (graphify.sh, graphify.md)
├── docs/ # WORKFLOWS, CAPABILITY-AUDIT, specs
├── examples/ # Fixture de teste (sample.fcpxml)
├── tests/ # 1032 testes / 24 suítes
└── tools/ # Pacote Python (__init__)
├── CLAUDE.md # Regras do projeto para o agente
├── admin/ # Ponte com o app (fora de code/)
│ ├── models_api.py # Entry point: docstring dos comandos + dispatch
│ └── api/ # Os 37 comandos, um módulo por assunto
└── code/
├── server.py # MCP entry point — só dispatch
├── server_tools/ # Handlers das 74 tools + _shared/
├── fcpxml/ # "Engine" — biblioteca Python de núcleo
│ ├── writer/ # PACOTE: edição/escrita (mixins por assunto)
│ ├── models/ # PACOTE: dados por família (timing, timeline…)
│ ├── parser.py # FCPXML → objetos Python
│ ├── rough_cut.py # Gera timelines novas
│ ├── voice_*.py # Pipeline de voz (features → timeline → actions)
│ ├── phrase_review.py # Revisão de frases da etapa 5
│ ├── text_layout.py # Diagramação das legendas
│ ├── live.py # Modo Live — push_to_fcp
│ ├── safe_xml.py # defusedxml + serialize_xml()
│ └── dtd.py # Validação contra DTDs da Apple
├── MacApp/Sources/ # App SwiftUI (compilado por swiftc)
├── Engine/ # Esta documentação
├── docs/ # WORKFLOWS, CAPABILITY-AUDIT, specs
├── examples/ # Fixture de teste (sample.fcpxml)
└── tests/ # 1.454 testes / 42 suítes
```
---
@@ -102,7 +113,7 @@ TimeValue(600, 2400) # "600/2400s" == 0.25s
- Soma/subtração compartilham um único caminho `_binop()` (fast-path de mesmo
denominador + alinhamento por LCM).
### 4.2 Modelos principais — `models.py`
### 4.2 Modelos principais — `models/`
| Classe | Função |
|--------|--------|
@@ -126,8 +137,8 @@ escrita. `from_xml_element` faz match estrito do atributo `completed`
| Subsistema | Módulo | Função |
|-----------|--------|--------|
| Parser | `parser.py` | FCPXML → objetos Python: espinha, connected clips, secondary storylines, roles |
| Modifier | `writer.FCPXMLModifier` | Edição index-based (clips/resources/formats dicts) do documento existente |
| Writer | `writer.FCPXMLWriter` | Gera FCPXML novo a partir de objetos Python |
| Modifier | `writer/` (`FCPXMLModifier`) | Edição index-based (clips/resources/formats dicts) do documento existente |
| Writer | `writer/generator.py` | Gera FCPXML novo a partir de objetos Python |
| Rough cut | `rough_cut.py` | Gera timelines (rough cuts, montages, A/B roll) |
| Diff | `diff.py` | Compara timelines — detecta added/removed/moved/trimmed |
| Export | `export.py` | DaVinci Resolve v1.9 + FCP7 XMEML v5 |
@@ -151,7 +162,7 @@ assíncrono:
TOOL_HANDLERS = {
"analyze_timeline": handle_analyze_timeline,
"list_clips": handle_list_clips,
# ... 62 tools
# ... 74 tools, todos em server_tools/
}
```
@@ -249,7 +260,7 @@ correção, sem depender de lembrar dos dois comandos no pre-commit.
- [docs/02_MODULES.md](docs/02_MODULES.md) — guia módulo a módulo do `fcpxml/`
(responsabilidade, tamanho, APIs públicas).
- [docs/03_SERVER_TOOLS.md](docs/03_SERVER_TOOLS.md) — a camada MCP `server.py`,
62 ferramentas, helpers e o padrão de handler.
74 ferramentas, helpers e o padrão de handler.
- [docs/04_TESTS_AND_WORKFLOW.md](docs/04_TESTS_AND_WORKFLOW.md) — suíte de testes,
fluxo de trabalho (lint + pytest), execução e estado atual do sistema.
- [docs/05_EXPERIENCIAS.md](docs/05_EXPERIENCIAS.md) — **memória de projeto**:
@@ -259,10 +270,10 @@ correção, sem depender de lembrar dos dois comandos no pre-commit.
programação** a aplicar em toda alteração/correção; inclui checklist final.
### Outros documentos
- [../CLAUDE.md](../CLAUDE.md) — visão geral, key patterns, execução e pre-commit.
- [../CLAUDE.md](../../CLAUDE.md) — visão geral, key patterns, execução e pre-commit.
- [../docs/CAPABILITY-AUDIT-2026-06.md](../docs/CAPABILITY-AUDIT-2026-06.md) —
auditoria do ecossistema e roadmap dual-mode (XML + Live).
- [../docs/WORKFLOWS.md](../docs/WORKFLOWS.md) — 8 receitas de workflow de produção.
- [../docs/specs/](../docs/specs/) — schemas de tools, estrutura FCPXML, pseudocódigo
do writer, algoritmo de rough cut, implementação do server, roadmap, modelos.
- [../admin/graphify.md](../admin/graphify.md) — pipeline de graphify do código.
- [../admin/graphify.md](../../admin/graphify.md) — pipeline de graphify do código.
+136 -68
View File
@@ -1,109 +1,177 @@
# 01 — Arquitetura do Sistema (G-ART / fcp-mcp-server)
> Referência canônica de como o sistema está dividido e implementado. Leia este
> documento antes de qualquer mudança de código.
> **Escopo:** Como o sistema é dividido em camadas e onde cada responsabilidade mora.
> **Não cobre:** Detalhe módulo a módulo (→ 02) · ferramentas MCP (→ 03) · app (→ 08)
> Referência canônica de como o sistema está dividido. Leia antes de qualquer
> mudança de código. Se algo aqui divergir do código, **o código está certo e
> este documento está velho** — corrija-o no mesmo commit.
Última varredura: 2026-08-19 · 74 ferramentas MCP · 1.454 testes · versão `0.6.35`
---
## 1. Visão de cima (camadas)
O sistema é um **servidor MCP em Python** que lê/analisa/reescreve arquivos
**FCPXML** do Final Cut Pro. Há **três camadas** bem separadas:
O sistema lê, analisa e reescreve **FCPXML** do Final Cut Pro. Ele opera *fora*
do FCP: você exporta o XML, o programa processa como dados estruturados e
devolve um XML para importar. Nada é patcheado, nenhuma API privada é usada.
São **quatro camadas**, e o ponto importante é que existem **duas portas de
entrada diferentes** para o mesmo motor:
```
┌─────────────────────────────────────────────────────────────┐
│ admin/ — Aplicações complementares (fora do MCP) │
│ models_api.py API (FastAPI) p/ gerenciar modelos │
│ models_gui.py UI desktop (Flet) p/ gerenciar modelos │
│ graphify.sh/.md Pipeline de graphify do código │
├─────────────────────────────────────────────────────────────┤
│ server.py — CAMADA MCP / TRANSPORTE (NÃO tem lógica) │
│ 73 tools, handlers, prompts, resources, dispatch │
│ Só valida entrada/saída e traduz JSON-RPC → chamadas │
├─────────────────────────────────────────────────────────────┤
│ fcpxml/ — "ENGINE" = NÚCLEO PURO Python (desacoplado) │
│ Não conhece MCP nem argumentos de tool. │
│ Trabalha com objetos Python e XML. │
│ É o foco / onde quase tudo mora. │
└─────────────────────────────────────────────────────────────┘
┌──────────────────────────┐ ┌──────────────────────────────┐
│ MacApp/ (SwiftUI) │ │ Cliente MCP (Claude) │
│ O app que o usuário usa │ │ Conversa, decide a edição │
└───────────┬──────────────┘ └───────────────┬──────────────┘
│ subprocesso + JSON-lines │ JSON-RPC (stdio)
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────────┐
│ admin/models_api.py │ │ server.py + server_tools/ │
│ + admin/api/ │ │ 74 tools, dispatch, schemas │
│ 37 comandos da ponte │ │ NÃO tem lógica de timeline │
└───────────┬──────────────┘ └───────────────┬──────────────┘
└───────────────┬────────────────────┘
▼
┌───────────────────────────────────┐
│ fcpxml/ — O ENGINE │
│ Núcleo puro Python, desacoplado. │
│ Não conhece MCP nem o app. │
│ É onde quase tudo mora. │
└───────────────────────────────────┘
```
**Regra de arquitetura:** `server.py` NUNCA implementa lógica de timeline —
ele delega ao `fcpxml/`. Tudo em `fcpxml/` é testável isoladamente (1032 testes).
**A regra que sustenta tudo:** nem `server.py` nem `admin/api/` implementam
lógica de timeline. Os dois validam entrada, chamam o engine e formatam a
saída. Toda regra de negócio é testável sem MCP e sem app.
## 2. Regras transversais (convenções em todo o código)
**Por que duas portas.** O MCP existe para o julgamento editorial — qual tomada
usar, onde dar zoom — que é conversa com uma IA. A ponte existe para o que o
usuário faz sozinho no app — transcrever, configurar, processar. As duas caem
no mesmo engine, então uma correção ali vale para as duas.
---
## 2. Regras transversais (valem em todo o código)
| Conceito | Regra |
|----------|-------|
| **Tempo** | `TimeValue` fração racional `"600/2400s"`. Nunca use float p/ tempo. |
| **I/O paths** | Sempre via helpers `_validate_filepath` / `_validate_output_path` (sandbox). |
| **Nome de saída** | Nunca sobrescrever original: `output_<suffix>.fcpxml`. |
| **Segurança XML** | Sempre `defusedxml` (via `safe_xml.py`). Nunca `xml.etree` direto. |
| **Tempo** | `TimeValue`, fração racional `"600/2400s"`. **Nunca float para tempo.** |
| **Tempo de decisão** | Ações de voz usam sempre segundos da **mídia original**, nunca pós-corte. |
| **I/O paths** | Sempre via `_validate_filepath` / `_validate_output_path` (sandbox). |
| **Nome de saída** | Nunca sobrescrever o original: `generate_output_path()` gera `_suffix`. |
| **Segurança XML** | Sempre `defusedxml` via `safe_xml.py`. Nunca `xml.etree` direto para ler. |
| **Deps opcionais** | `librosa`/`ffmpeg`/`huggingface_hub` importados **lazy**, degradam com `None`. |
| **Lint** | `ruff check . --exclude docs/` — zero erros. |
| **Validação pós-correção** | `./Engine/run_after_fix.sh` SEMPRE após cada correção. |
| **Idioma** | Comunicação com o usuário em português. Código e comentários em inglês. |
| **Validação** | `./Engine/run_after_fix.sh` **sempre** após cada correção. |
| **App** | Alterou `MacApp/`? Compile e rode: `./MacApp/build_app.sh --run`. |
## 3. Fluxo de um request (round-trip)
---
## 3. Fluxo de um request
### Pela porta MCP (Claude decidindo a edição)
```
Cliente MCP (Claude)
│ JSON-RPC (stdio)
▼
server.py ── dispatcher (TOOL_HANDLERS)
│ valida path, parseia projeto, chama engine
▼
fcpxml/parser.py XML → objetos
fcpxml/writer.py edita / grava
fcpxml/rough_cut.py gera novas timelines
fcpxml/export.py cross-NLE
▼
output_<suffix>.fcpxml (original intocado)
▼
Final Cut Pro: File → Import → XML (ou push_to_fcp, sem cliques)
Cliente MCP ──JSON-RPC──► server.py
│ TOOL_HANDLERS[nome]
▼
server_tools/<categoria>.py
│ _shared/: valida path, parseia projeto
▼
fcpxml/ (parser → writer → safe_xml)
▼
projeto_<suffix>.fcpxml (original intocado)
```
### Pela porta do app (usuário operando)
```
MacApp ──Process + argv JSON──► admin/models_api.py
│ handlers[comando]
▼
admin/api/<assunto>.py
│ shared.emit() devolve JSON-lines
▼
fcpxml/ (ou chama um handler do server)
▼
arquivo gerado + caminho de volta ao app
```
A saída da ponte é **JSON-lines**: um documento JSON por linha, para que
comandos longos transmitam progresso enquanto rodam. Toda escrita passa por
`admin/api/shared.py::emit`, que serializa o acesso a stdout — dois comandos
escrevendo ao mesmo tempo entrelaçariam documentos.
---
## 4. Dual-mode: XML + Live
O sistema opera em **dois modos complementares**:
- **Modo XML (principal):** exporta FCPXML, processa como dados, reimporta.
Roda fora do FCP. Nenhuma API privada.
- **Modo Live (`fcpxml/live.py`):** *push* do FCPXML direto p/ o FCP em
execução via Apple events oficiais (`Open Document`), com `import-options`.
Leitura de bibliotecas via AppleScript read-only.
- **Modo Live (`fcpxml/live.py`):** *push* do FCPXML direto para o FCP em
execução via Apple events oficiais (`Open Document`). Leitura de bibliotecas
via AppleScript read-only.
**Assimetria estrutural:** import é scriptable, mas a Apple não oferece export
programático — round-trips voltam pelas ferramentas XML.
programático. Round-trips sempre voltam pelas ferramentas XML.
---
## 5. Onde está cada responsabilidade
| Responsabilidade | Fica em |
|------------------|---------|
| Modelos de dados (tempo, clips, markers) | `fcpxml/models.py` |
| Modelos de dados (tempo, clips, markers, QC, legendas) | `fcpxml/models/` |
| Parse FCPXML → objetos | `fcpxml/parser.py` |
| Editing/escrita (modifier + writer) | `fcpxml/writer.py` |
| Edição e escrita de FCPXML | `fcpxml/writer/` |
| Geração de timeline nova | `fcpxml/rough_cut.py` |
| Comparação de timelines | `fcpxml/diff.py` |
| Export cross-NLE (Resolve, FCP7) | `fcpxml/export.py` |
| Inteligência de mídia (silêncio/beats) | `fcpxml/media_intel.py` |
| Transcrição Whisper local | `fcpxml/transcribe.py` |
| Silêncio e beats | `fcpxml/media_intel.py` |
| Transcrição Whisper | `fcpxml/transcribe.py` |
| Diarização (quem falou) | `fcpxml/diarize.py` |
| Ênfase acústica | `fcpxml/emphasis.py`, `fcpxml/voice_features.py` |
| Timeline de voz (o JSON que a IA lê) | `fcpxml/voice_timeline.py` |
| Decisões de edição (cut/zoom/text/marker) | `fcpxml/voice_actions.py` |
| Revisão de frases da etapa 5 | `fcpxml/phrase_review.py` |
| Layout de legendas e métricas de fonte | `fcpxml/text_layout.py`, `font_metrics.py`, `collision.py` |
| Gestão de modelos Whisper | `fcpxml/model_manager.py` |
| Templates de timeline | `fcpxml/templates.py` |
| Controle Live do FCP | `fcpxml/live.py` |
| Segurança XML (`defusedxml`, `serialize_xml`) | `fcpxml/safe_xml.py` |
| Segurança XML | `fcpxml/safe_xml.py` |
| Validação contra DTDs da Apple | `fcpxml/dtd.py` |
| Transporte MCP (73 tools) | `server.py` |
| Transporte MCP (74 tools) | `server.py` + `server_tools/` |
| Ponte com o app (37 comandos) | `admin/models_api.py` + `admin/api/` |
| Interface do usuário | `MacApp/Sources/` |
## 6. Mapa de dependências (você está aqui se for mexer no X → quem tocar)
---
## 6. Mapa de dependências
```
server.py ──► fcpxml/parser, writer, rough_cut, export, diff,
media_intel, transcribe, templates, live, dtd
admin/models_gui.py ──► fcpxml/media_intel, model_manager,
parser, transcribe
admin/models_api.py ──► fcpxml/model_manager
fcpxml/writer.py ──► fcpxml/models, safe_xml, dtd
fcpxml/__init__.py ──► reexporta a API pública
MacApp/ ──► admin/models_api.py (subprocesso, por caminho)
admin/api/ ──► fcpxml/* e, para algumas operações, server.py
server.py ──► server_tools/*
server_tools/* ──► server_tools/_shared/ ──► fcpxml/*
fcpxml/writer/ ──► fcpxml/models/, safe_xml, dtd, text_layout, collision
fcpxml/models/ ──► fcpxml/text_layout (só o pacote subtitles)
fcpxml/__init__.py ──► reexporta a API pública
```
> Se você cria uma **nova ferramenta MCP**, o trabalho principal é em `fcpxml/`
> (função pura + testes). O handler em `server.py` fica fino: validação de
> caminho → `_parse_project` → chama a função → `_text_result`.
**A seta que não existe, e não deve existir:** `fcpxml/` nunca importa de
`server_tools/`, de `admin/` ou de qualquer coisa que saiba o que é uma tool.
Se você precisar disso, a lógica está no lugar errado.
---
## 7. Criando algo novo — por onde começar
| Você quer… | Comece por |
|-----------|-----------|
| Uma **ferramenta MCP** nova | Função pura em `fcpxml/` + teste. O handler em `server_tools/` fica fino. |
| Um **comando do app** novo | Mesmo caminho, e exponha em `admin/api/<assunto>.py` + tabela em `models_api.py`. |
| Uma **tela** nova | `MacApp/Sources/`, consumindo comandos que já existem na ponte. |
| Uma **regra de edição** nova | `fcpxml/` sempre. Se você está escrevendo `if` sobre timeline fora de `fcpxml/`, pare. |
O trabalho principal é **sempre** no engine. As camadas de cima são finas de
propósito: é o que permite testar 1.454 casos sem abrir o app nem subir o MCP.
+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.
+55 -21
View File
@@ -1,26 +1,49 @@
# 03 — Camada MCP (`server.py`) — 73 ferramentas
# 03 — Camada MCP (`server.py` + `server_tools/`) — 74 ferramentas
`server.py` (3824 linhas) é a camada de transporte. Não tem lógica de timeline —
mapeia nome → handler e delega ao Engine. O dispatch é um dicionário
`TOOL_HANDLERS` (padrão de despacho, sem cadeias gigantes de if/elif).
> **Escopo:** As 74 ferramentas MCP: helpers, categorias e como criar uma nova.
> **Não cobre:** Lógica de edição, que mora no engine (→ 02) · comandos do app (→ 08)
`server.py` (592 linhas) é só o transporte: dispatch por dicionário
`TOOL_HANDLERS`, sem cadeia de if/elif e **sem lógica de timeline**. Os handlers
moram em `server_tools/`, um módulo por categoria, e os helpers que todos usam
em `server_tools/_shared/`.
```
server_tools/
editing.py (649) qc.py (696) voice.py (754) timeline.py (400)
subtitles.py markers_import generation.py transcript.py
export.py roles.py live.py
_shared/ ← helpers compartilhados, ver abaixo
```
## Helpers centrais (use-os, não reinvente)
| Helper | Linha | Função |
|--------|------:|--------|
| `_check_json_depth()` | 83 | Rejeita payloads além de 50 níveis |
| `_validate_filepath()` | 103 | Sandbox de entrada |
| `_validate_output_path()` | 149 | Sandbox de saída |
| `_format_clip_table()` | 245 | Renderização de tabela |
| `_markdown_table()` | 259 | Renderização de tabela markdown |
| `_parse_project()` | 319 | Parseia FCPXML → `(tree, timeline, project)`; quase todos os handlers começam aqui |
| `_resolve_io_paths()` | 357 | Validação de caminho de entrada/saída |
| `_setup_modifier()` | 390 | Prepara modifier com validação |
| `_setup_generator()` | 414 | Prepara generator com validação |
| `_parse_timestamp_parts()` | 433 | Parse de timestamps (min:seg, H:MM:SS, SMPTE) |
| `_detect_flash_frames/gaps/duplicate_groups()` | 1667+ | Detectores de QC |
Todos reexportados por `server_tools/_shared`, então `from ._shared import X`
continua funcionando. A coluna diz o módulo real, para quando você precisar
**editar** o helper — ou apontar um `monkeypatch` para ele.
## As 73 ferramentas por categoria
| Helper | Mora em | Função |
|--------|---------|--------|
| `_validate_filepath()` | `_shared/paths.py` | Sandbox de entrada |
| `_validate_output_path()` | `_shared/paths.py` | Sandbox de saída |
| `_check_json_depth()` | `_shared/paths.py` | Rejeita payloads além de 50 níveis |
| `generate_output_path()` | `_shared/paths.py` | Nome derivado, sem tocar no original |
| `_resolve_io_paths()` | `_shared/paths.py` | Entrada + saída de uma vez |
| `_parse_project()` | `_shared/project.py` | FCPXML → `(tree, timeline, project)`; quase todo handler começa aqui |
| `_setup_modifier()` | `_shared/project.py` | Prepara modifier já validado |
| `_setup_generator()` | `_shared/project.py` | Prepara generator já validado |
| `_text_result()` | `_shared/project.py` | Envolve o texto em `TextContent` MCP |
| `_markdown_table()` | `_shared/formatting.py` | Tabela markdown |
| `_format_clip_table()` | `_shared/formatting.py` | Tabela de clipes |
| `_format_batch_result()` | `_shared/formatting.py` | Relatório de operação em lote |
| `_parse_timestamp_parts()` | `_shared/captions.py` | min:seg, H:MM:SS, SMPTE |
| `parse_srt()` / `parse_vtt()` | `_shared/captions.py` | Legendas coladas |
| `_detect_flash_frames/gaps/duplicate_groups()` | `_shared/detection.py` | Detectores de QC |
| `_load_or_transcribe()` | `_shared/media.py` | Transcrição com cache em disco |
| `_cut_transcript_spans()` | `_shared/media.py` | Corte por trecho falado |
| `_apply_placed_action()` | `_shared/media.py` | Aplica zoom/text/marker já posicionado |
## As 74 ferramentas por categoria
### Timeline & análise (Projeto)
`list_projects`, `analyze_timeline`, `list_clips`, `list_markers`, `list_connected_clips`,
@@ -144,7 +167,18 @@ Regras:
- Sempre retornam via `_text_result(text)` (envolve o texto em `TextContent` MCP).
## Para adicionar uma ferramenta nova
1. Escrever a função no módulo do Engine (`fcpxml/…`) + testes.
2. Criar `handle_<nome>` em `server.py` seguindo o padrão acima.
3. Registrar no dicionário `TOOL_HANDLERS`.
1. **Escrever a função no Engine** (`fcpxml/…`) com testes. É aqui que mora o
trabalho de verdade; o resto é encanamento.
2. **Criar `handle_<nome>`** em `server_tools/<categoria>.py`, seguindo o padrão
acima. Escolha a categoria pelo assunto, não pelo tamanho do arquivo.
3. **Declarar o schema** (`Tool(...)`) no mesmo módulo.
4. **Registrar** no `TOOL_HANDLERS` de `server.py`.
5. Rodar `./Engine/run_after_fix.sh`.
Se a ferramenta também deve aparecer no app, exponha um comando equivalente em
`admin/api/<assunto>.py` e registre na tabela de `admin/models_api.py` — ver
`08_APP_MACOS.md`. Uma capacidade que só existe como tool MCP **não existe para
quem usa o app** (foi exatamente o que aconteceu com `apply_voice_actions`,
`05_EXPERIENCIAS.md` #20).
4. Rodar `./Engine/run_after_fix.sh`.
@@ -1,5 +1,8 @@
# 04 — Testes, Fluxo de Trabalho e Estado Atual
> **Escopo:** Como rodar e escrever testes, e o gate antes de commitar.
> **Não cobre:** O que testar em cada módulo (→ 02) · checklist de qualidade (→ 06)
## 1. Suíte de testes
**1032 testes em 24 arquivos** em `tests/`. Rode com `uv run pytest tests/ -v`.
+38 -31
View File
@@ -11,6 +11,44 @@ houver uma correção ou trabalho em torno dele, **adicione um registro aqui**
antes de prosseguir. Um problema que se repete em várias tentativas é sinal de
que merece entrada.
> **Como usar:** o índice abaixo é o ponto de entrada. Procure o sintoma
> aqui primeiro; só abra a entrada completa (mais abaixo) se ela for a sua.
> As entradas ficam em ordem cronológica depois do índice.
## Resumo rápido (índice)
| # | Data | Problema | Estado |
|---|------|----------|--------|
| 1 | 2026-08-14 | Início do registro de experiências | `resolvido` |
| 4 | 2026-08-14 | XML fora da grade de frame em NTSC (23.976/29.97fps), confirmado no FCP | `resolvido` |
| 5 | 2026-08-14 | `TimeValue.from_timecode` corrompia segundos decimais em NTSC (3º ponto do bug) | `resolvido` |
| 6 | 2026-08-17 | Clipe-fantasma de 1 frame no início/fim após remoção de silêncio (padding sem vizinho na borda) | `resolvido` |
| 7 | 2026-08-17 | Legendas dinâmicas sobrepondo entre clipes (título conectado não é aparado pelo out-point do pai) | `resolvido` |
| 8 | 2026-08-17 | Importação recusada: `id` de `<text-style-def>` derivado do texto (acentos/espaços/dígito inicial) não é XML Name válido | `resolvido` |
| 9 | 2026-08-17 | Legendas palavra a palavra centradas em vez da composição progressiva diagramada (bloco por trecho, palavra-chave em display italic) | `resolvido` |
| 10 | 2026-08-17 | Cedilha/acentos da display italic invadindo a linha vizinha: empilhamento passou a usar a tinta real por classe de glifo | `resolvido` |
| 11 | 2026-08-18 | Preview das legendas dinâmicas desproporcional ao render do FCP (stagger/gap/canvas divergentes) e `inactive_color` exposto sem efeito | `resolvido` |
| 12 | 2026-08-18 | Espaço de coordenadas do modelo "Text": `fontSize`, `kerning` e `Position` no espaço do quadro — converter só o tamanho descolou o espaçamento | `resolvido` |
| 13 | 2026-08-19 | Reanálise de ênfase implementada no Engine mas sem ferramenta MCP — Fase 4 da skill era inexecutável | `resolvido` |
| 14 | 2026-08-19 | Offset sistemático de ~0,4s no timing por palavra (faster-whisper sem alinhamento forçado) — corrigido manualmente no teste, WhisperX pendente | `parcialmente resolvido` |
| 15 | 2026-08-19 | `add_zoom` perdia o enquadramento real (voltava a 100%) quando dois zooms caiam no mesmo clipe pós-corte; agora empilha ou substitui conforme as janelas se sobrepõem | `resolvido` |
| 16 | 2026-08-19 | `validate_subtitle_layout` acusava colisão severa em títulos que só se tocam na borda, por não-associatividade de float; 7 de 8 colisões reportadas no teste real eram falso positivo | `resolvido` |
| 17 | 2026-08-19 | Linha de ênfase das legendas dinâmicas sem limite de largura — palavra longa/maiúscula estourava o frame inteiro; auto-fit encolhe até caber, nunca abaixo do corpo | `resolvido` |
| 18 | 2026-08-19 | Legendas dinâmicas geradas com `bold="0" fontFace="Bold"` não renderizam no FCP — negrito deve ser `bold="1"` (atributo) e itálico `fontFace`+`italic="1"` | `resolvido` |
| 19 | 2026-08-19 | `output_dir` usado só como cerca de validação e nunca como destino — toda chamada entre pastas falhava acusando o caminho que ela mesma gerou | `resolvido` |
| 20 | 2026-08-19 | `apply_voice_actions` ausente da ponte e do encadeamento do app — dava para analisar e legendar, não para cortar | `resolvido` |
| 21 | 2026-08-19 | Teste ainda afirmava o default `zoom scale=1.3` removido do parser (agora vem do `zoom_scale` do usuário) | `resolvido` |
| 22 | 2026-08-19 | `VideoPlayer` (AVKit) aborta em runtime no app compilado por `swiftc` — etapa 5 fechava o app; trocado por `AVPlayerLayer` | `resolvido` |
| 23 | 2026-08-19 | Dividir `writer.py` em pacote quebrou `@patch('fcpxml.writer.subprocess')` — a suíte protege comportamento, não localização | `resolvido` |
| 24 | 2026-08-19 | `admin/test_models_api.py` existia mas estava fora de `testpaths` — 13 testes que nunca rodaram | `resolvido` |
> Mantenha o índice acima sempre sincronizado com as entradas mais recentes.
---
## Entradas (ordem cronológica)
---
## Como registrar (template de entrada)
@@ -1275,34 +1313,3 @@ o outro; percentil entrega um punhado útil nos dois casos.
rodando. Vale também para o lint: `admin/` ainda não é coberto pelo
`run_after_fix.sh`, que roda só dentro de `code/`.
- **Estado:** `resolvido`
---
## Resumo rápido (índice)
| # | Data | Problema | Estado |
|---|------|----------|--------|
| 1 | 2026-08-14 | Início do registro de experiências | `resolvido` |
| 4 | 2026-08-14 | XML fora da grade de frame em NTSC (23.976/29.97fps), confirmado no FCP | `resolvido` |
| 5 | 2026-08-14 | `TimeValue.from_timecode` corrompia segundos decimais em NTSC (3º ponto do bug) | `resolvido` |
| 6 | 2026-08-17 | Clipe-fantasma de 1 frame no início/fim após remoção de silêncio (padding sem vizinho na borda) | `resolvido` |
| 7 | 2026-08-17 | Legendas dinâmicas sobrepondo entre clipes (título conectado não é aparado pelo out-point do pai) | `resolvido` |
| 8 | 2026-08-17 | Importação recusada: `id` de `<text-style-def>` derivado do texto (acentos/espaços/dígito inicial) não é XML Name válido | `resolvido` |
| 9 | 2026-08-17 | Legendas palavra a palavra centradas em vez da composição progressiva diagramada (bloco por trecho, palavra-chave em display italic) | `resolvido` |
| 10 | 2026-08-17 | Cedilha/acentos da display italic invadindo a linha vizinha: empilhamento passou a usar a tinta real por classe de glifo | `resolvido` |
| 11 | 2026-08-18 | Preview das legendas dinâmicas desproporcional ao render do FCP (stagger/gap/canvas divergentes) e `inactive_color` exposto sem efeito | `resolvido` |
| 12 | 2026-08-18 | Espaço de coordenadas do modelo "Text": `fontSize`, `kerning` e `Position` no espaço do quadro — converter só o tamanho descolou o espaçamento | `resolvido` |
| 13 | 2026-08-19 | Reanálise de ênfase implementada no Engine mas sem ferramenta MCP — Fase 4 da skill era inexecutável | `resolvido` |
| 14 | 2026-08-19 | Offset sistemático de ~0,4s no timing por palavra (faster-whisper sem alinhamento forçado) — corrigido manualmente no teste, WhisperX pendente | `parcialmente resolvido` |
| 15 | 2026-08-19 | `add_zoom` perdia o enquadramento real (voltava a 100%) quando dois zooms caiam no mesmo clipe pós-corte; agora empilha ou substitui conforme as janelas se sobrepõem | `resolvido` |
| 16 | 2026-08-19 | `validate_subtitle_layout` acusava colisão severa em títulos que só se tocam na borda, por não-associatividade de float; 7 de 8 colisões reportadas no teste real eram falso positivo | `resolvido` |
| 17 | 2026-08-19 | Linha de ênfase das legendas dinâmicas sem limite de largura — palavra longa/maiúscula estourava o frame inteiro; auto-fit encolhe até caber, nunca abaixo do corpo | `resolvido` |
| 18 | 2026-08-19 | Legendas dinâmicas geradas com `bold="0" fontFace="Bold"` não renderizam no FCP — negrito deve ser `bold="1"` (atributo) e itálico `fontFace`+`italic="1"` | `resolvido` |
| 19 | 2026-08-19 | `output_dir` usado só como cerca de validação e nunca como destino — toda chamada entre pastas falhava acusando o caminho que ela mesma gerou | `resolvido` |
| 20 | 2026-08-19 | `apply_voice_actions` ausente da ponte e do encadeamento do app — dava para analisar e legendar, não para cortar | `resolvido` |
| 21 | 2026-08-19 | Teste ainda afirmava o default `zoom scale=1.3` removido do parser (agora vem do `zoom_scale` do usuário) | `resolvido` |
| 22 | 2026-08-19 | `VideoPlayer` (AVKit) aborta em runtime no app compilado por `swiftc` — etapa 5 fechava o app; trocado por `AVPlayerLayer` | `resolvido` |
| 23 | 2026-08-19 | Dividir `writer.py` em pacote quebrou `@patch('fcpxml.writer.subprocess')` — a suíte protege comportamento, não localização | `resolvido` |
| 24 | 2026-08-19 | `admin/test_models_api.py` existia mas estava fora de `testpaths` — 13 testes que nunca rodaram | `resolvido` |
> Mantenha o índice acima sempre sincronizado com as entradas mais recentes.
+3
View File
@@ -1,5 +1,8 @@
# 06 — Boas Práticas de Programação (G-ART)
> **Escopo:** Checklist de qualidade a aplicar antes de dar algo por pronto.
> **Não cobre:** Por onde começar uma tarefa (→ 09) · o que já quebrou (→ 05)
> **Propósito:** registrar as melhores práticas de programação a serem aplicadas
> **sempre** que qualquer alteração ou correção for feita neste programa.
> Servem de checklist obrigatório antes de concluir qualquer mudança.
+195
View File
@@ -0,0 +1,195 @@
# 08 — O app macOS (`MacApp/`) e o Assistente
> **Escopo:** O app SwiftUI e o Assistente: build, telas, ponte e a etapa 5.
> **Não cobre:** Engine Python (→ 02) · ferramentas MCP (→ 03)
O app SwiftUI é como o usuário opera o sistema sem abrir terminal nem conversar
com uma IA. São ~5.500 linhas em `MacApp/Sources/`, e ele **não tem lógica de
edição**: tudo que ele faz é montar argumentos, chamar a ponte Python e mostrar
o resultado.
Última varredura: 2026-08-19
---
## 1. Como o app é construído — leia antes de mexer
**Não existe `.xcodeproj` nem `Package.swift`.** O app é compilado invocando o
`swiftc` direto sobre `MacApp/Sources/*.swift`:
```bash
cd code && ./MacApp/build_app.sh # compila e monta o .app
cd code && ./MacApp/build_app.sh --run # compila e abre
```
Consequências práticas, todas já sentidas:
- **Arquivo novo em `Sources/` entra sozinho** no build. Não há lista de alvos.
- **Não dá para adicionar dependência SPM** sem antes migrar o build inteiro.
- **Compilar não prova que roda.** Componentes SwiftUI que embrulham classes
Objective-C podem falhar só em tempo de execução, ao abrir a tela. Foi o que
aconteceu com `VideoPlayer` (AVKit): compilava limpo e abortava ao abrir a
etapa 5 (`05_EXPERIENCIAS.md` #22). Por isso a regra: **alterou a interface,
abra a tela de fato.**
### Testando uma tela sem navegar o app inteiro
Um harness de vinte linhas compila os mesmos fontes com um `@main` próprio que
monta só a tela em questão. Reproduz crash de runtime em segundos:
```bash
swiftc -parse-as-library -sdk "$(xcrun --sdk macosx --show-sdk-path)" \
-target arm64-apple-macosx26.0 \
MacApp/Sources/PhraseReviewView.swift MacApp/Sources/PhraseReviewModel.swift \
MacApp/Sources/TimelineTracksView.swift MacApp/Sources/Models.swift \
MacApp/Sources/PythonBridge.swift /tmp/HarnessMain.swift -o /tmp/harness
```
O `@main` do harness carrega a tela, imprime o que interessa e chama
`NSApplication.shared.terminate` — dá para afirmar "abriu e funcionou" sem
depender de screenshot.
---
## 2. Estrutura das telas
| Arquivo | Linhas | Papel |
|---------|-------:|-------|
| `WizardView.swift` | 808 | **O Assistente** — fluxo guiado de 7 etapas |
| `TranscriptionView.swift` | 843 | Transcrição avulsa e processamento em lote |
| `ModelDownloadView.swift` | 545 | Catálogo e download de modelos Whisper |
| `CaptionsView.swift` | 545 | Legendas dinâmicas: estilo + preview ao vivo |
| `TimelineTracksView.swift` | 506 | Timeline com trilhas, zoom e playhead |
| `PhraseReviewModel.swift` | 429 | Estado da etapa 5: frases, player, zooms |
| `PhraseReviewView.swift` | 413 | Etapa 5: preview + inspector de frases |
| `VoiceAnalysisView.swift` | 322 | Parâmetros do motor de ênfase |
| `ProjectView.swift` | 293 | Inspeção do `.fcpxml` |
| `Models.swift` | 274 | Espelhos Swift do JSON da ponte |
| `PythonBridge.swift` | 230 | **A ponte** — ver seção 3 |
| `SubtitlePreviewView.swift` | 218 | Preview 9:16 das legendas |
| `App.swift` | 69 | `NavigationSplitView` e as abas |
Abas (`ActiveTab` em `App.swift`): Assistente · Projeto · Legendas · Análise de
Voz · Modelos · Sobre. As cinco últimas são "Avançado" — atalhos para operações
soltas. O Assistente é o caminho principal.
---
## 3. `PythonBridge.swift` — como o app fala com o Python
O app lança `admin/models_api.py` como **subprocesso**, passando o comando e um
JSON como `argv`, e lê **JSON-lines** no stdout.
```swift
PythonBridge.call(command: "build_phrase_review",
arguments: ["voice_timeline": path]) { result, error in … }
```
Dois pontos que já causaram problema e estão resolvidos no código — não os
desfaça sem entender:
- **`uv run` precisa rodar com cwd em `code/`.** O `uv` escolhe o ambiente pelo
diretório do processo, não pelo caminho do script. Rodar da raiz fazia o `uv`
criar um segundo `.venv` vazio e ignorar tudo que estava instalado em
`code/.venv` — librosa e pyannote instalavam com sucesso e o app insistia que
faltavam.
- **`scriptURL` procura `admin/models_api.py`** subindo diretórios a partir do
cwd, do bundle e do home. É o que faz o app funcionar tanto rodando do Xcode
quanto do `.app` montado.
Para adicionar um comando: função em `admin/api/<assunto>.py`, registro na
tabela de `admin/models_api.py`, e `PythonBridge.call` do lado Swift. Os 37
comandos e seus formatos estão documentados no docstring de `models_api.py`.
---
## 4. O Assistente — as 7 etapas
`WizardStep` (`WizardView.swift`) é um enum sequencial; `canAdvance` decide
quando o botão "Continuar" libera.
| # | Etapa | O que acontece | Comando da ponte |
|---|-------|----------------|------------------|
| 1 | Projeto | Escolhe a pasta de saída e o `.fcpxml` | `project_config` |
| 2 | Transcrever | Transcreve toda a mídia do projeto | `transcribe` |
| 3 | Analisar voz | Mede ênfase, locutores, emoção | `analyze_voice` |
| 4 | Decisões da IA | Copia para o chat, cola o JSON de volta, aplica | `apply_voice_actions` |
| 5 | **Revisar ênfases** | Lapida frase a frase — ver seção 5 | `build_phrase_review` / `save_phrase_review` |
| 6 | Processar | Silêncios, preenchimento, legendas | vários, em cadeia |
| 7 | Concluído | Abre no FCP ou mostra no Finder | — |
**A etapa 4 é a única manual do fluxo**, e de propósito: o julgamento de qual
tomada usar e onde dar zoom é conversa com uma IA (skill `editar-por-voz`), não
um botão. O app monta o pedido pronto no clipboard e recebe o JSON de volta.
**Etapa 1 — armadilha registrada:** não escolha como "o projeto" um arquivo já
gerado pelo fluxo (`_voice_edit`, `_silence_removed`, …). Os cortes de voz
assumem timestamps da mídia **original**; reaplicá-los sobre um arquivo já
cortado desloca tudo em silêncio. O wizard avisa (`looksLikeGeneratedFile`).
---
## 5. Etapa 5 — a sala de edição
Única tela que ocupa a janela toda: o corpo do wizard é uma coluna de 640pt, e
essa etapa escapa dela porque precisa da largura (`step == .revisar` em
`WizardView.body`).
```
┌────────────────────────────┬──────────────┐
│ Preview (AVPlayerLayer) │ Inspector │
│ enquadrado no formato │ de frases │
│ de entrega do projeto │ │
├────────────────────────────┴──────────────┤
│ Timeline: 6 trilhas, zoom, playhead │
└───────────────────────────────────────────┘
```
**Trilhas:** zooms · frases · energia por palavra · emoção · locutor ·
roteiro/bastidor. Todas desenhadas sobre o mesmo eixo de tempo, com uma coluna
fixa à esquerda nomeando cada uma.
**O que o usuário decide por frase:** nível de ênfase (0–3), ativo/inativo,
texto, roteiro/bastidor e o trim das pontas. O trim anda em **fronteira de
palavra** — cortar é apontar para uma palavra, arrastando a borda do bloco ou
clicando na palavra no inspector.
**Zoom manual:** arrastar na timeline marca um trecho; botão direito cria um
zoom nele. O zoom guarda **só o quando** — escala e ramp vêm das configurações
de Análise de Voz no momento do render, então mudar lá restiliza todos.
**Decisões de implementação que parecem detalhe e não são:**
- **O preview não renderiza nada.** Ele toca a mídia original e *pula* os
trechos removidos. Renderizar para conferir um toggle poria minutos entre a
decisão e o resultado. O observador roda a 60 Hz porque o período dele é
exatamente quanto de material cortado dá para ouvir antes do pulo.
- **O enquadramento é o do projeto, não o da mídia.** As gravações são
horizontais e a entrega é vertical; o app lê o formato do `.fcpxml`
(`inspect`) e mostra o corte central aproximado, com um selo para alternar
para a mídia original. O enquadramento real de cada clipe vem do FCP — o
preview é aproximação, e o selo diz isso.
- **Nada é processado aqui.** "Continuar" grava o `_phrase_review.json` e o
`_phrase_actions.json` derivado dele. A geração é da etapa 6.
- **A revisão é sempre remontada da análise atual**, com as decisões salvas
reaplicadas por cima (`merge_saved_decisions`). Assim refazer a análise de voz
melhora a tela em vez de ficar mascarado por uma cópia velha; uma decisão cuja
frase se moveu mais de 0,25 s é descartada em vez de colar na frase errada.
---
## 6. Estado atual e o que falta
**Funciona e foi verificado:** carga das frases com decisões da IA, as 6
trilhas, seleção sincronizada nos três painéis, trim por palavra, zoom manual,
reprodução parando no ponto exato (erro de 0 ms medido), pulo dos trechos
removidos, enquadramento vertical, gravação ao avançar.
**Ainda em aberto:**
- **A etapa 6 não consome o `_phrase_review.json`.** A ligação — zoom e legenda
dinâmica só nas frases de ênfase, legenda comum no resto — é a próxima tarefa.
- **`MacApp/` não tem teste automatizado.** A rede é o harness da seção 1 e o
olho do usuário. Toda mudança de interface precisa ser aberta de fato.
- **O preview aproxima o reenquadramento vertical** pelo corte central; se os
clipes forem reposicionados no FCP, diverge.
+156
View File
@@ -0,0 +1,156 @@
# 09 — Manutenção: onde mexer, o que está aberto, o que dói
> **Escopo:** Por onde começar cada tipo de tarefa, o que está aberto e onde dói.
> **Não cobre:** Como as coisas funcionam — este doc roteia para quem explica
Este é o documento de rota. Os outros descrevem o que **é**; este diz o que
**fazer** e por onde começar quando chega uma implementação, uma melhoria ou
uma correção.
Última varredura: 2026-08-19 · 1.454 testes · lint zerado
---
## 1. Chegou uma tarefa — por onde começo?
| A tarefa é… | Comece em | Não esqueça |
|-------------|-----------|-------------|
| Regra nova de edição (corte, zoom, legenda) | `fcpxml/<módulo>` + teste | Expor na tool **e** na ponte, senão só metade dos usuários alcança |
| Corrigir XML que o FCP recusa | `fcpxml/writer/` + `dtd.py` | Validar contra o DTD real, não só o teste |
| Mudança visível na interface | `MacApp/Sources/` | **Abrir a tela** — compilar não prova nada (§4) |
| Comando novo para o app | `admin/api/<assunto>.py` | Registrar na tabela de `models_api.py` |
| Ferramenta MCP nova | `server_tools/<categoria>.py` | Schema `Tool(...)` + `TOOL_HANDLERS` |
| Ajuste de análise de voz | `fcpxml/voice_*`, `emphasis.py` | Regerar os `_voice_timeline.json` de teste |
| "Está lento" / "está errado" e não sei onde | §5 (mapa de sintomas) | — |
**A pergunta que resolve 90% das dúvidas de lugar:** essa lógica precisa saber
o que é uma tool MCP ou uma tela? Se não precisa — e quase nunca precisa — ela
vai para `fcpxml/`.
---
## 2. O que está aberto agora
Ordenado por quanto atrapalha, não por esforço.
### 2.1 A etapa 6 ignora a revisão de ênfases
O usuário lapida as frases na etapa 5, o `_phrase_review.json` é gravado — e a
etapa 6 ainda processa como antes. Falta ligar: **zoom e legenda dinâmica só
nas frases de ênfase, legenda comum no resto**. É a continuação natural do
trabalho da etapa 5 e o item mais valioso da lista.
→ `MacApp/Sources/WizardView.swift` (`finalizeProcessing`), `admin/api/subtitles.py`,
`fcpxml/phrase_review.py` (`emphasis_spans` já é produzido e ninguém consome).
### 2.2 Offset de ~400 ms no timing por palavra
O faster-whisper sem alinhamento forçado erra o início de cada palavra em
~0,4 s. Isso desloca zoom, corte e `gap_before` de uma vez. Há paliativo
aplicado por projeto; a correção estrutural é ligar o **WhisperX** (ou
alinhamento equivalente) em `transcribe.py`, o que levaria o erro para ~30 ms.
Custo real: regerar todos os `_transcript.json` e `_voice_timeline.json`
existentes. → `05_EXPERIENCIAS.md` #14, estado `parcialmente resolvido`.
### 2.3 `MacApp/` não tem teste automatizado
5.500 linhas de Swift sem uma asserção. A rede hoje é o harness manual (§4) e
o olho do usuário. Não é para sair criando suíte de UI — mas lógica pura que
foi parar na camada de tela (cálculo de trim, mapeamento de tempo) deveria
descer para o Python, onde já existe rede.
### 2.4 `admin/` fica fora do lint
`run_after_fix.sh` roda o ruff de dentro de `code/`, então `admin/` — 1.751
linhas de código que o app depende para funcionar — nunca é verificado.
Incluir mexe no gate, então é decisão consciente, não esquecimento.
### 2.5 Confirmações visuais pendentes no FCP
Várias entradas do `05_EXPERIENCIAS.md` estão marcadas como resolvidas *no XML*
— testes verdes, DTD válido — mas **pendentes de importação real no Final Cut**.
XML válido não é o mesmo que XML que renderiza como o esperado. Ao mexer em
legenda, zoom ou keyframe, a confirmação final é abrir no FCP.
### 2.6 Submódulo `WHISPERX` com conteúdo modificado e não commitado
Está fora dos commits de propósito, porque ninguém verificou o que mudou lá
dentro. Precisa ser olhado e resolvido — ou commitado, ou revertido.
---
## 3. Onde o código ainda é grande (e onde isso não é problema)
Quatro arquivos foram divididos (`writer.py`, `models.py`, `models_api.py`,
`_shared.py`): 6.685 linhas concentradas viraram 43 módulos.
O que sobrou grande, e o diagnóstico honesto de cada um:
| Arquivo | Linhas | Vale dividir? |
|---------|-------:|---------------|
| `fcpxml/text_layout.py` | 901 | **Não.** É diagramação — um assunto coeso. |
| `fcpxml/rough_cut.py` | 798 | **Não.** É geração de timeline, um assunto. |
| `fcpxml/model_manager.py` | 748 | Talvez: mistura catálogo, download e config. |
| `server_tools/voice.py` | 754 | Talvez, se crescer mais. |
| `MacApp/TranscriptionView.swift` | 843 | Sim, quando for mexer nela. |
| `MacApp/WizardView.swift` | 808 | Sim: sete etapas num `switch` só. |
**Critério, não número:** divida quando o arquivo tiver **assuntos** que não se
falam. Um arquivo grande de um assunto só é mais fácil de ler que seis arquivos
pequenos que você precisa abrir juntos. Código picado sem motivo atrapalha tanto
quanto arquivo gigante.
---
## 4. Checklist antes de dar algo por pronto
```bash
cd code && ./Engine/run_after_fix.sh # lint zerado + 1.454 testes
cd code && ./MacApp/build_app.sh --run # se mexeu no app
```
E, além do script:
- [ ] **Mexeu na interface? Abriu a tela?** Compilar não prova que roda —
`VideoPlayer` compilava e abortava (`05_EXPERIENCIAS.md` #22).
- [ ] **Mexeu em XML? Importou no FCP?** DTD válido ≠ renderiza certo.
- [ ] **Dividiu ou moveu módulo?** Procure `patch('<módulo>.` e imports
relativos dentro de funções — é o que quebra em silêncio (#23).
- [ ] **Criou teste fora de `code/tests/`?** Confirme que a contagem total
subiu. Teste fora de `testpaths` não roda e dá falsa sensação de rede (#24).
- [ ] **Problema estrutural ou erro recorrente?** Registre em
`05_EXPERIENCIAS.md` com o índice atualizado.
- [ ] **Documentação divergiu?** Corrija no mesmo commit. Doc velha engana mais
que doc ausente.
---
## 5. Mapa de sintomas → onde olhar
| Sintoma | Suspeite de | Arquivo |
|---------|-------------|---------|
| FCP recusa o arquivo ao importar | `id` inválido, ordem de filhos, timebase | `writer/validation.py`, `dtd.py` |
| Título importa mas não aparece | Template/uid Motion que não resolve | `writer/titles.py` |
| Corte no lugar errado | Tempo pós-corte usado como se fosse original | `voice_actions.py` (`shift_after_cuts`) |
| Zoom no lugar errado | Idem, ou offset de timing do Whisper | §2.2 |
| Legenda sobrepondo | Layout ou conteúdo antigo no arquivo | `collision.py`, `text_layout.py` |
| "Ênfase" apontando para palavra à toa | Falta renormalizar após o corte | `refine_voice_timeline` |
| App diz que falta librosa/pyannote | `uv run` com cwd errado | `PythonBridge.swift` (§3 do doc 08) |
| Tela do app fecha o programa | Componente de framework que só falha em runtime | `05_EXPERIENCIAS.md` #22 |
| Comando existe no MCP mas não no app | Falta expor na ponte | `admin/api/`, #20 |
---
## 6. Convenções que não são negociáveis
Estão em `01_ARCHITECTURE.md` §2 e valem repetir as três que mais custaram:
1. **Tempo é fração racional.** Float para tempo produz drift que só aparece
depois de dez operações encadeadas.
2. **Ação de voz é sempre em tempo da mídia original.** Nunca pós-corte.
3. **Original nunca é sobrescrito.** Toda saída ganha sufixo.
---
## Documentos relacionados
- [01 Arquitetura](01_ARCHITECTURE.md) — camadas e onde cada coisa mora
- [02 Módulos](02_MODULES.md) — mapa do engine, módulo a módulo
- [03 Server/Tools](03_SERVER_TOOLS.md) — as 74 ferramentas MCP
- [04 Testes & Workflow](04_TESTS_AND_WORKFLOW.md)
- [05 Experiências](05_EXPERIENCIAS.md) — o que já quebrou e por quê
- [06 Boas Práticas](06_BOAS_PRATICAS.md)
- [08 App macOS](08_APP_MACOS.md) — o app e o Assistente