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:
co-authored by
Claude Opus 5
parent
ffaebb3f72
commit
dcdd73edb5
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
Cliente MCP ──JSON-RPC──► server.py
|
||||
│ TOOL_HANDLERS[nome]
|
||||
▼
|
||||
server.py ── dispatcher (TOOL_HANDLERS)
|
||||
│ valida path, parseia projeto, chama engine
|
||||
server_tools/<categoria>.py
|
||||
│ _shared/: valida path, parseia projeto
|
||||
▼
|
||||
fcpxml/parser.py XML → objetos
|
||||
fcpxml/writer.py edita / grava
|
||||
fcpxml/rough_cut.py gera novas timelines
|
||||
fcpxml/export.py cross-NLE
|
||||
fcpxml/ (parser → writer → safe_xml)
|
||||
▼
|
||||
output_<suffix>.fcpxml (original intocado)
|
||||
▼
|
||||
Final Cut Pro: File → Import → XML (ou push_to_fcp, sem cliques)
|
||||
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
|
||||
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
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user