Files
gart/code/Engine/docs/03_SERVER_TOOLS.md
T
João HenriqueandClaude Opus 5 dcdd73edb5 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>
2026-08-19 22:51:33 -04:00

184 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
```python
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 em `TextContent` MCP).
## Para adicionar uma ferramenta nova
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`.