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>
9.7 KiB
03 — Camada MCP (server.py + server_tools/) — 74 ferramentas
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)
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.
| 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,
list_compound_clips, list_library_clips, list_roles, list_keywords, list_effects.
QC e detecção
find_short_cuts, find_long_clips, analyze_pacing, detect_flash_frames,
detect_duplicates, detect_gaps, validate_timeline, detect_silence_candidates,
detect_media_silence, remove_silence_candidates, remove_media_silence, detect_beats.
Edição
add_marker, batch_add_markers, trim_clip, reorder_clips, add_transition,
change_speed, delete_clips, split_clip, insert_clip, fix_flash_frames,
rapid_trim, fill_gaps, add_audio, create_compound_clip, flatten_compound_clip.
Geração
auto_rough_cut, generate_montage, generate_ab_roll, list_templates, apply_template.
Beats / markers importados
import_beat_markers, snap_to_beats, import_srt_markers, import_transcript_markers.
Roles
assign_role, filter_by_role, export_role_stems.
Transcrição & edição por transcrição
transcribe_media, edit_by_transcript, remove_filler_words.
Diferenciação
diff_timelines.
Export / relink
export_edl, export_csv, export_resolve_xml, export_fcp7_xml, relink_media.
Reformat
reformat_timeline.
Voz (análise → decisão → aplicação)
analyze_voice_features, build_voice_timeline, refine_voice_timeline,
remove_speakers, apply_voice_actions, get_voice_analysis_config,
save_voice_analysis_config.
O fluxo é sempre o mesmo: build_voice_timeline mede (caro, roda uma vez) →
o modelo decide os cortes → refine_voice_timeline renormaliza sobre o que
sobrou (barato, sem reabrir áudio) e propõe as janelas de zoom → o modelo
corta a lista pelo ritmo → apply_voice_actions aplica. Pular a renormalização
faz o ranking de ênfase apontar para as palavras erradas (ver
05_EXPERIENCIAS.md).
Legendas dinâmicas (geração → validação → aplicação)
generate_dynamic_subtitles, validate_subtitle_layout, transcript_markers.
Sempre gere e depois valide — nunca dê a geração como pronta sem
validate_subtitle_layout. A composição garante "sem sobreposição" só
por construção dentro do que ela mesma sabe medir; um título editado à
mão, uma palavra fora do alcance do que foi calibrado, ou conteúdo antigo
no mesmo arquivo escapam dessa garantia. Fluxo:
generate_dynamic_subtitles(filepath)
↓
validate_subtitle_layout(output_path) ← sempre, mesmo quando "parece certo"
↓
severidade none/warning → entregar
severidade probable/severe → investigar CADA colisão pela fração exata do
XML antes de mudar código (ver checklist abaixo)
Antes de atribuir uma colisão ao gerador, confirme que é o gerador.
Um <title> de nome estranho (ref diferente, params tipo Auto-Shrink/
Left Margin que _make_text_title_clip nunca escreve) é conteúdo humano
ou de outra ferramenta, não um bug — comparar contra o arquivo original
(grep pelo texto) resolve em segundos. Caso real: uma colisão "severa"
era um título manual feito no FCP que sobrou no arquivo reaproveitado como
base de teste (05_EXPERIENCIAS.md, 2026-08-19).
Antes de atribuir uma colisão a uma sobreposição real, confirme pela
fração exata do FCPXML, não pelo float arredondado. Dois títulos que só
se tocam na borda (um bloco some exatamente quando o próximo começa, por
design) podem imprimir tempos "iguais" e ainda assim colidir no relatório
por ruído de ponto flutuante — float(a+b) != float(c) mesmo quando as
frações a+b e c são idênticas. temporal_overlap() já tem uma
tolerância (_BOUNDARY_EPSILON = 1e-6, muitas ordens abaixo de um frame)
para absorver isso; se uma colisão nova parecer nascer do nada, comparar
m._parse_time(...) dos dois títulos por igualdade exata antes de
suspeitar de sobreposição de verdade.
Constantes que resolvem os três bugs já encontrados nesta área (todas
em fcpxml/text_layout.py, exceto a última):
| Constante | O que resolve | Por quê |
|---|---|---|
TEXT_TEMPLATE_FONT_SCALE = 2.0 |
Posição e tamanho de fonte dessincronizados | O template "Text" do FCP posiciona no espaço do frame (2160×3840), mas o layout mede em pontos de meia-escala (1080×1920). Escalar só o tamanho da fonte e não a posição espalha o texto errado — os dois têm que ser convertidos pelo mesmo fator na saída. |
_EMPHASIS_ITALIC_CUSHION_RATIO = 0.06 |
Linha de corpo lendo apertada sob a linha de ênfase | O itálico da Playfair inclina as hastes além da caixa de tinta que a métrica mede; ~14pt de respiro extra só nessa fronteira corrige sem tocar no line_gap do resto. |
fit_emphasis() (função, não constante) |
Palavra de ênfase estourando o frame inteiro | Só linhas de corpo faziam wrap contra box.width; a linha de ênfase (sempre uma palavra só) nunca foi checada. Uma palavra longa ou toda maiúscula podia medir mais que o frame inteiro sozinha. Encolhe font_size+kerning pelo mesmo fator até caber — nunca abaixo do tamanho do corpo, senão ênfase deixa de ser ênfase. |
_BOUNDARY_EPSILON = 1e-6 (fcpxml/collision.py) |
Falso positivo de colisão em títulos que só se tocam | Ver parágrafo acima. |
Detalhe de implementação e efeito medido de cada um: 05_EXPERIENCIAS.md,
entradas de 2026-08-19 (#15 zoom, #16 colisão por float, #17 auto-fit da
ênfase — a #15 é do módulo de voz, não de legendas, mas mesma causa-raiz
de fundo: um mecanismo que lê a própria saída anterior precisa continuar
sendo fonte de verdade legível, não só efeito colateral write-only).
Live (macOS)
push_to_fcp, list_fcp_libraries.
Padrão de handler (a forma de fazer)
async def handle_<nome>(arguments: dict):
tree, timeline, project = _parse_project(arguments) # 1. parseia
# ...opera com o Engine (parser/writer/rough_cut/export)...
return _text_result(text) # 2. devolve
Regras:
- Todo handler valida caminho com
_validate_filepath/_validate_output_path. - Saídas sempre com sufixo
_modified,_chapters, etc. — original nunca é tocado. - Cada handler tem o seu
async def handle_<name>(arguments: dict). - Sempre retornam via
_text_result(text)(envolve o texto emTextContentMCP).
Para adicionar uma ferramenta nova
- Escrever a função no Engine (
fcpxml/…) com testes. É aqui que mora o trabalho de verdade; o resto é encanamento. - Criar
handle_<nome>emserver_tools/<categoria>.py, seguindo o padrão acima. Escolha a categoria pelo assunto, não pelo tamanho do arquivo. - Declarar o schema (
Tool(...)) no mesmo módulo. - Registrar no
TOOL_HANDLERSdeserver.py. - 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.