Files
gart/code/Engine/docs/03_SERVER_TOOLS.md
T
João HenriqueandClaude Sonnet 5 2ad5854570 fix(voz): sobra de fatia interior no corte, e legenda comum poluindo com clipe desativado
Dois problemas reais vistos no projeto Mastopexia:

1. cut_clip_ranges só absorvia um keep-segment curto no INÍCIO/FIM do
   clipe (a lógica já existente do #6). Um keep curto no MEIO (entre dois
   cuts, sem nenhum vizinho mantido pra herdar) nunca era absorvido —
   sobrava como clipe de vídeo de 0,07-0,23s na timeline. Generalizado
   pra qualquer posição, com limiar maior (6 frames / 0,3s, medido no
   material real) — no meio, o pedacinho é descartado (vira parte do
   corte ao redor), nas bordas continua sendo herdado pelo vizinho.

2. generate_subtitles_by_emphasis gerava a legenda comum inteira e
   desativava (enabled="0") onde a dinâmica cobre. Título desativado
   continua aparecendo como clipe riscado na timeline do Final Cut mesmo
   sem renderizar — um corte com bastante ênfase virava dezenas de clipes
   mortos poluindo a trilha (visto ao vivo pelo usuário: "ficou uma
   bosta"). Trocado por não gerar o bloco comum ali, em vez de gerar e
   desativar. Custo: reativar ênfase manualmente depois exige regenerar a
   legenda comum daquele trecho, não só reabilitar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 18:59:21 -04:00

12 KiB
Raw Blame History

03 — Camada MCP (server.py + server_tools/) — 77 ferramentas

Escopo: As 77 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 77 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_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, generate_voice_script, get_voice_analysis_config, save_voice_analysis_config.

O fluxo manual é: 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).

generate_voice_script é o fluxo automático e fechado (sem wizard, sem copiar-e-colar): transcreve (cache) → build_voice_timeline → entrega a timeline a um modelo local Ollama que dirige a edição → devolve o roteiro legível (markdown) e o JSON de ações, e opcionalmente aplica num FCPXML. O cliente fica em fcpxml/llm_local.py; o modelo é tratado como entrada não confiável e cada ação é validada por parse_actions. Padrão: qwen2.5:7b-instruct-q4_K_M (troca de gemma3:12b — não cabia em máquina de 8GB de RAM; Gemma 3 4B foi testado antes e falhou por apagar o roteiro principal em vez de só cortar bastidor). Passe model= para usar outro servido pelo Ollama.

Legendas dinâmicas (geração → validação → aplicação)

generate_dynamic_subtitles, generate_plain_subtitles, generate_subtitles_by_emphasis, 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)

generate_subtitles_by_emphasis gera as duas legendas numa passada só, dividindo por palavra: a dinâmica cobre as frases marcadas como ênfase na etapa 5 (zoom aplicado, nível ≥ 1); a comum cobre todo o resto — um bloco comum simplesmente não é criado onde a dinâmica já cobre. A primeira versão gerava a comum inteira e desativava (enabled="0") o que ficava sob a dinâmica, mas um título desativado continua aparecendo como clipe riscado na timeline do Final Cut mesmo sem renderizar — um corte com bastante ênfase enchia a trilha de clipes mortos. Trocado por não gerar ali: o preço é que, se a ênfase for desativada à mão depois, a legenda comum daquele trecho precisa ser regenerada, não só reativada. É a tradução de 10-revisao-humana.md (skill editar-por-voz): "a frase de ênfase recebe zoom E legenda dinâmica; as demais recebem legenda comum". A decisão vem de <mídia>_phrase_actions.json["emphasis_spans"], escrito por save_phrase_review quando o editor termina a etapa 5 — sem esse arquivo (ou sem zoom/text marcados na revisão), a tool gera só a comum, tudo ligado, e avisa no relatório ("Sem revisão de ênfase"). Não expõe overrides de estilo por chamada — usa a config salva ("Legendas Dinâmicas"/plain); para estilo pontual, use generate_dynamic_subtitles/generate_plain_subtitles direto.

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 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.