Files
gart/code/Engine/docs/03_SERVER_TOOLS.md

150 lines
7.8 KiB
Markdown
Raw Permalink 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`) — 73 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).
## 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 |
## As 73 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 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`.
4. Rodar `./Engine/run_after_fix.sh`.