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
+15 -5
View File
@@ -28,11 +28,21 @@ minutos.
`apply_voice_actions` aplica direto, mas é para **teste**. O produto do seu
trabalho é a lista de decisões.
**Para onde ela vai:** o usuário cola o seu JSON no app, e ele abre na etapa 5
do Assistente — uma tela onde cada frase do roteiro aparece com a sua decisão
já marcada, para ser revisada antes de gerar. Você é o **ponto de partida** da
edição, não a palavra final; escreva decisões defensáveis e motivos legíveis.
Como o app traduz cada ação sua: `criterios/10-revisao-humana.md`.
**Caminho automatizado (sem wizard, sem copiar-e-colar):** a tool
`generate_voice_script` (MCP) / comando `generate_voice_script` (ponte do app)
corre o fluxo fechado: transcreve → `build_voice_timeline` → entrega a timeline
a um **modelo local Ollama (Gemma 3 / Llama)** que age exatamente como este
skill descreve (separa roteiro de bastidor, escolhe tomadas, decide zoom/corte)
→ devolve o roteiro legível **e** o JSON de ações, e opcionalmente aplica no
FCPXML. O cliente fica em `code/fcpxml/llm_local.py`; o prompt que embute este
contrato está em `_SYSTEM_PROMPT`. Use essa tool quando o usuário pedir para
"rodar tudo internamente" ou "gerar o roteiro por IA local".
**Para onde ela vai (modo manual):** o usuário cola o seu JSON no app, e ele
abre na etapa 5 do Assistente — uma tela onde cada frase do roteiro aparece com
a sua decisão já marcada, para ser revisada antes de gerar. Você é o **ponto de
partida** da edição, não a palavra final; escreva decisões defensáveis e motivos
legíveis. Como o app traduz cada ação sua: `criterios/10-revisao-humana.md`.
## Ordem de trabalho
@@ -53,14 +53,19 @@ use para decidir; existem para permitir a reanálise da Fase 2.
## O timestamp por palavra tem um viés conhecido
O início de cada palavra vem sistematicamente **adiantado em ~0,3-0,5s** em
relação ao ataque real da fala — medido em material real com ffmpeg
(`astats`), consistente em 6 pontos do mesmo vídeo. O fim da palavra não
tem esse problema (erro de poucos centésimos). Causa: `word_timestamps` do
faster-whisper deriva por atenção cruzada, sem alinhamento forçado — ver
`05_EXPERIENCIAS.md`, entrada de 2026-08-19.
relação ao ataque real da fala — medido em material real com ffmpeg (`astats`),
consistente em 6 pontos do mesmo vídeo. O fim da palavra não tem esse problema
(erro de poucos centésimos). Causa: `word_timestamps` do faster-whisper deriva
por atenção cruzada, sem alinhamento forçado — ver `05_EXPERIENCIAS.md`, entrada
de 2026-08-19.
**Quando o pipeline já corrigiu isso:** se `layers.alignment` for `true`
(transcript gerado com alinhamento forçado fonético via whisperx, implementado
depois desse aviso), o viés foi removido na origem — **não aplique o offset
manual** abaixo. O aviso vale só para transcripts antigos sem `layers.alignment`.
Isso não é "reestimar no olho" — é um bug de medição na fonte, não um
julgamento seu. Na prática:
julgamento seu. Na prática (somente sem `layers.alignment`):
- Ao posicionar um `zoom` cujo `start` precisa cair exatamente na palavra
(não uma frase inteira), some **+0,3 a +0,4s** ao timestamp do JSON antes
@@ -69,6 +74,3 @@ julgamento seu. Na prática:
- **Não aplique essa correção a `gap_before` para decidir corte** — a régua
de silêncio (`06-texto-corte-marcador.md`) já é conservadora o bastante
para absorver esse erro; corrigir os dois ao mesmo tempo é redundante.
- Se um dia o pipeline ganhar alinhamento forçado (WhisperX), este aviso
perde a razão de existir — confira se `layers` ou a versão do documento
já indicam isso antes de aplicar o offset manualmente.