Trabalho da branch feat/revisao-enfases: pipeline de edição por voz ganha alinhamento forçado (whisperx), roteirização por LLM local (Ollama), e a etapa 5 (revisão de frases) passa a refletir de verdade o que é aplicado. - generate_subtitles_by_emphasis: legenda comum cobre o clipe inteiro, legenda dinâmica só nas frases de ênfase, e a comum é desativada (enabled="0") onde a dinâmica cobre, em vez de nunca ser gerada ali. - validate_subtitle_layout ignora títulos com enabled="0" — corrige falso positivo de colisão contra o que está desativado no lugar dele. - Corrige zoom/marcador sendo descartado quando a borda encosta exatamente no início de um corte. - Etapa 5 do Assistente: recarrega quando as decisões da IA mudam (com fresh=true, ignorando a revisão salva antiga) — resolve a dessincronia entre "ativa" na tela e o que já foi cortado no FCPXML. - Etapa "Processar" reaplica as decisões da revisão (_phrase_actions.json) antes da cadeia de remoção de silêncio/legendas — antes, desativar uma frase na etapa 5 não tinha efeito nenhum no vídeo final. - Etapa "Concluído" fundida em "Processar" — abrir no Final Cut/Finder aparece assim que termina, sem slide extra. - Palavra clicável na etapa 5 agora funciona como toggle (clique de novo desfaz) e mostra a própria ênfase (sublinhado colorido + peso da fonte). - fcpxml/forced_align.py, fcpxml/llm_local.py, ai_edit.py: alinhamento fonético via whisperx e roteirização local via Ollama/Gemma. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.8 KiB
01 — Arquitetura do Sistema (G-ART / fcp-mcp-server)
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 · 77 ferramentas MCP · 1.498 testes · versão 0.6.35
1. Visão de cima (camadas)
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:
┌──────────────────────────┐ ┌──────────────────────────────┐
│ 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/ │ │ 77 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. │
└───────────────────────────────────┘
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.
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 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. |
| 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: admin/run_app.command (padrão de revisão; equivale a ./MacApp/build_app.sh --run). |
3. Fluxo de um request
Pela porta MCP (Claude decidindo a edição)
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
- Modo XML (principal): exporta FCPXML, processa como dados, reimporta.
- 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 sempre voltam pelas ferramentas XML.
5. Onde está cada responsabilidade
| Responsabilidade | Fica em |
|---|---|
| Modelos de dados (tempo, clips, markers, QC, legendas) | fcpxml/models/ |
| Parse FCPXML → objetos | fcpxml/parser.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 |
| 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 |
| Controle Live do FCP | fcpxml/live.py |
| Segurança XML | fcpxml/safe_xml.py |
| Validação contra DTDs da Apple | fcpxml/dtd.py |
| Transporte MCP (77 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
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
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.498 casos sem abrir o app nem subir o MCP.