feat(legendas): liga compound_subphrases por padrão no pipeline

generate_dynamic_subtitles e a metade dinâmica de generate_subtitles_by_emphasis
passam a empacotar cada sub-frase da legenda dinâmica num compound clip por
padrão (compound_subphrases=True), completando o wrap_titles_in_compound
e split_into_subphrases do commit anterior — que ainda não tinham chamador
em produção.

Também torna validate_subtitle_layout ciente de compound clips: media cada
grupo (spine principal + cada <media> de compound) no seu próprio espaço de
tempo, em vez de uma varredura .//title global — sem isso, âncoras de
compounds diferentes liam offset "0s" e acusavam colisão espacial entre
frases que nunca dividem a tela, só porque compartilham o mesmo zero de
tempo local.

Testado ponta a ponta na gravação real (Mastopexia): 12 compounds, 41
títulos todos empacotados, zero soltos, zero IDs duplicados, DTD válida.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
João Henrique
2026-08-26 17:05:20 -04:00
co-authored by Claude Sonnet 5
parent 688bdeddb6
commit c99274895c
3 changed files with 355 additions and 108 deletions
+41 -4
View File
@@ -1,6 +1,6 @@
# 03 — Camada MCP (`server.py` + `server_tools/`) — 77 ferramentas
# 03 — Camada MCP (`server.py` + `server_tools/`) — 78 ferramentas
> **Escopo:** As 77 ferramentas MCP: helpers, categorias e como criar uma nova.
> **Escopo:** As 78 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
@@ -82,8 +82,21 @@ 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`, `generate_voice_script`,
`get_voice_analysis_config`, `save_voice_analysis_config`.
`remove_speakers`, `remove_speech_gaps`, `apply_voice_actions`,
`generate_voice_script`, `get_voice_analysis_config`,
`save_voice_analysis_config`.
`remove_speech_gaps` corta pelo que a **transcrição** já sabe que não tem
fala — lê `words[].start/end` do `_voice_timeline.json` (função
`speech_gap_cut_actions`, em `fcpxml/voice_actions.py`) em vez de medir
volume. É o complemento correto para o caso que `remove_media_silence`
(silêncio por dB, ver seção "Silêncio e beats") não cobre: um trecho sem
fala mas com som real acima do limiar (respiração, ruído de roupa, batida) —
`remove_media_silence` nunca vai cortar isso porque tecnicamente não é
silêncio. Não corta a lacuna antes da primeiríssima palavra (pode ser quase
o arquivo inteiro, antes da tomada realmente começar) — isso continua
decisão manual na Fase 6 do `apply_voice_actions`
(`.claude/skills/editar-por-voz/criterios/06-texto-corte-marcador.md`).
O fluxo manual é: `build_voice_timeline` mede (caro, roda uma vez) →
o modelo decide os cortes → **`refine_voice_timeline` renormaliza sobre o que
@@ -143,6 +156,30 @@ e avisa no relatório ("Sem revisão de ênfase"). Não expõe overrides de esti
por chamada — usa a config salva ("Legendas Dinâmicas"/plain); para estilo
pontual, use `generate_dynamic_subtitles`/`generate_plain_subtitles` direto.
**Separação por role (didática na timeline):** `generate_dynamic_subtitles` e
`generate_plain_subtitles` (e a metade dinâmica/comum do `by_emphasis`) aplicam
`role="titles.dinamicas"` e `role="titles.convencionais"` em cada `<title>`
criado — sub-roles de `titles`, **nunca** `subtitles.*` (que esconderia o título
atrás de Code). O parâmetro `role` de cada ferramenta MCP sobrescreve o default
(vindo de `load_dynamic_subtitle_config()["role"]` /
`load_plain_subtitle_config()["role"]`). Ver `02_MODULES.md` (seção "Separação
de role") e `05_EXPERIENCIAS.md` entrada 32 (DTD: `<title>` leva `role`, não
`videoRole`).
**Compound clip por sub-frase (padrão em `generate_dynamic_subtitles` e na
metade dinâmica do `by_emphasis`):** `compound_subphrases=True` divide cada
frase em sub-frases pela vírgula (`transcribe.split_into_subphrases`) e
empacota os `<title>` de cada uma num `<ref-clip>` — a dúzia de títulos
empilhados por lane que uma frase gera vira uma barra só, arrastável/mutável
como unidade. Exceção: um trecho curto depois da vírgula ("né?", "Então...",
< 3 palavras) funde de volta na sub-frase anterior em vez de virar compound
próprio — soa como parte da mesma respiração, não uma frase nova. A estrutura
replica o que o próprio Final Cut gera em "New Compound Clip": o título mais
cedo vira âncora do spine interno em offset 0, os demais penduram nele por
lane. `validate_subtitle_layout` mede cada compound no seu próprio espaço de
tempo — sem isso, âncoras de compounds diferentes leem "0s" e colidem no
papel mesmo estando segundos distantes na timeline real.
**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