feat(voz): legenda por ênfase, forced align, IA local e correções de zoom/revisão

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>
This commit is contained in:
João Henrique
2026-08-21 18:26:04 -04:00
co-authored by Claude Sonnet 5
parent 711c397dfe
commit 7b5aed79ee
36 changed files with 2922 additions and 624 deletions
+37 -7
View File
@@ -1,6 +1,6 @@
# 03 — Camada MCP (`server.py` + `server_tools/`) — 74 ferramentas
# 03 — Camada MCP (`server.py` + `server_tools/`) — 77 ferramentas
> **Escopo:** As 74 ferramentas MCP: helpers, categorias e como criar uma nova.
> **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
@@ -43,7 +43,7 @@ continua funcionando. A coluna diz o módulo real, para quando você precisar
| `_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
## As 77 ferramentas por categoria
### Timeline & análise (Projeto)
`list_projects`, `analyze_timeline`, `list_clips`, `list_markers`, `list_connected_clips`,
@@ -82,18 +82,31 @@ continua funcionando. A coluna diz o módulo real, para quando você precisar
### 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`.
`remove_speakers`, `apply_voice_actions`, `generate_voice_script`,
`get_voice_analysis_config`, `save_voice_analysis_config`.
O fluxo é sempre o mesmo: `build_voice_timeline` mede (caro, roda uma vez) →
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`, `validate_subtitle_layout`, `transcript_markers`.
`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ó
@@ -111,6 +124,23 @@ 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ó —
mas não divide as palavras entre elas. A comum é gerada **completa, do início
ao fim do clipe**, sempre; a dinâmica é gerada só sobre as frases marcadas
como ênfase na etapa 5 (zoom aplicado, nível ≥ 1); e onde a dinâmica cobre um
trecho, os títulos comuns daquele trecho recebem `enabled="0"` — continuam no
XML (editáveis/reativáveis no Final Cut), só não são desenhados. É a tradução
literal de `10-revisao-humana.md` (skill `editar-por-voz`): "a frase de
ênfase recebe zoom E legenda dinâmica; as demais recebem legenda comum" —
sem nunca deixar um vão sem legenda nenhuma se a ênfase for desativada depois
(a comum já estava lá, só desligada). 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