fix(legendas): impede legenda comum sob composição dinâmica e duplicação ao regerar
Marca cada título gerado (dynamic/plain) em metadata para que regenerar substitua a saída anterior em vez de empilhar, e usa os spans de ênfase revisados (não os segmentos brutos do Whisper) como janela da composição dinâmica, evitando que ela invada o trecho de legenda comum seguinte. suppress_plain_under_dynamic corta qualquer sobra visível como rede de segurança. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
32d78d0f8d
commit
0fdfe33613
@@ -28,6 +28,7 @@ Versão: `0.6.35` · Última varredura: 2026-08-19
|
||||
| `model_manager.py` | 748 | Modelos Whisper: catálogo, download, config |
|
||||
| `voice_timeline.py` | 600 | O JSON de voz que a IA lê |
|
||||
| `phrase_review.py` | 547 | Revisão de frases (etapa 5 do assistente) |
|
||||
| `speaker_review.py` | 207 | Revisão de falantes (etapa 3 do assistente) |
|
||||
| `collision.py` | 472 | Colisão entre títulos na tela |
|
||||
| `font_metrics.py` | 445 | Largura real de glifos por fonte |
|
||||
| `templates.py` | 387 | Templates de timeline |
|
||||
@@ -36,7 +37,7 @@ Versão: `0.6.35` · Última varredura: 2026-08-19
|
||||
| `forced_align.py` | 181 | Alinhamento forçado opcional (whisperx/wav2vec2) que corrige o viés de ~0,4s no início das palavras |
|
||||
| `live.py` | 273 | Modo Live (push_to_fcp) |
|
||||
| `diff.py` | 269 | Comparação de timelines |
|
||||
| `voice_actions.py` | 263 | Decisões de edição (cut/zoom/text/marker) |
|
||||
| `voice_actions.py` | 319 | Decisões de edição (cut/zoom/text/marker) |
|
||||
| `export.py` | 226 | Export Resolve v1.9 + FCP7 XMEML v5 |
|
||||
| `voice_features.py` | 220 | Pitch, energia, ritmo, pausas |
|
||||
| `diarize.py` | 180 | Quem falou (pyannote) |
|
||||
@@ -55,9 +56,10 @@ editorial, todos operando sobre o mesmo documento e os mesmos índices.
|
||||
| Módulo | Linhas | Conteúdo |
|
||||
|--------|-------:|----------|
|
||||
| `core.py` | 723 | `ModifierCore`: carga, índices, navegação na spine, `save` |
|
||||
| `titles.py` | 600 | Títulos de texto e legendas dinâmicas |
|
||||
| `titles.py` | 867 | Títulos de texto e legendas dinâmicas |
|
||||
| `cut.py` | 333 | Dividir, cortar faixas, apagar |
|
||||
| `speed.py` | 297 | Velocidade e zoom (punch-in) |
|
||||
| `speed.py` | 94 | Velocidade de reprodução |
|
||||
| `zoom.py` | 204 | Zoom (punch-in) via clipe de ajuste conectado |
|
||||
| `helpers.py` | 279 | Sanitização, escalas, construtores de elemento |
|
||||
| `rapid.py` | 240 | Flash frames, rapid trim, preencher buracos |
|
||||
| `validation.py` | 232 | Verificações estruturais antes de salvar |
|
||||
@@ -66,7 +68,8 @@ editorial, todos operando sobre o mesmo documento e os mesmos índices.
|
||||
| `document.py` | 170 | Assets de vídeo, timebases, `write_fcpxml` |
|
||||
| `markers.py` | 165 | Marcadores: um, por timecode, em lote |
|
||||
| `audio.py` | 162 | Clipes de áudio e cama musical |
|
||||
| `generator.py` | 147 | `FCPXMLWriter` — cria documento do zero |
|
||||
| `generator.py` | 90 | `FCPXMLWriter` — orquestra a criação do zero (estado + delegação) |
|
||||
| `builders.py` | — | Um builder por tipo de elemento: `FormatBuilder`, `AssetBuilder`, `MarkerBuilder`, `KeywordBuilder`, `ClipBuilder`, `SequenceBuilder`, `LibraryBuilder` |
|
||||
| `reorder.py` | 126 | Reordenar e recalcular offsets |
|
||||
| `trim.py` | 125 | Aparar e propagar o ripple |
|
||||
| `transitions.py` | 94 | Transições entre vizinhos |
|
||||
@@ -121,7 +124,10 @@ voice_features.py pitch, energia, ritmo, pausas
|
||||
▼
|
||||
emphasis.py combina tudo num índice 0–1 por palavra
|
||||
▼
|
||||
voice_timeline.py monta o _voice_timeline.json ◄── é isto que a IA lê
|
||||
voice_timeline.py monta o _voice_timeline.json ◄── a análise crua
|
||||
▼
|
||||
speaker_review.py (opcional) filtra falante mutado + linha riscada
|
||||
→ _voice_timeline_clean.json ◄── é isto que a IA prefere
|
||||
▼
|
||||
[decisão: skill "editar-por-voz", ou a mão do usuário]
|
||||
▼
|
||||
@@ -155,6 +161,36 @@ Saída em camadas, para um modelo raciocinar do topo e descer só onde importa:
|
||||
`layers` existe para separar *"a fala é monótona"* de *"a análise acústica nunca
|
||||
carregou"* — os dois deixam os mesmos zeros nos dados.
|
||||
|
||||
### `speaker_review.py` — a triagem antes da IA
|
||||
|
||||
Roda logo após `analyze_voice` (etapa 3 do assistente, tela `SpeakerReviewView`
|
||||
no app): lista quem foi detectado (`speaker_profiles`, com % de fala e falas
|
||||
de amostra) e a transcrição segmento a segmento, para o usuário nomear cada
|
||||
falante, mutar quem não interessa (ex.: o entrevistador) e riscar linhas soltas
|
||||
antes de qualquer IA ver o arquivo. `build_speaker_review` nunca toca a
|
||||
timeline crua; `apply_speaker_review`/`write_clean_voice_timeline` produzem
|
||||
uma cópia separada, `_voice_timeline_clean.json`, reaproveitando
|
||||
`enrich_words`/`_segment_rows`/`_summary` de `voice_timeline.py` para
|
||||
recalcular a ênfase só sobre quem sobrou — mesma lógica de `restrict_to_kept`,
|
||||
por falante/segmento em vez de por intervalo de tempo. O merge de decisões
|
||||
salvas segue o padrão de `phrase_review.merge_saved_decisions`: sempre
|
||||
reconstrói da análise atual, só as escolhas humanas persistem.
|
||||
|
||||
A skill "editar-por-voz", `generate_voice_script` e `cmd_build_phrase_review`
|
||||
(etapa 4/5, `admin/api/review.py`) preferem o `_clean` quando ele existe; sem
|
||||
revisão salva, seguem lendo o `_voice_timeline.json` normal — a etapa 3 é
|
||||
sempre opcional. Os três pontos de leitura precisam concordar nessa
|
||||
preferência: se um deles voltar a ler o arquivo cru direto, a revisão de
|
||||
falantes vira letra morta sem nenhum erro visível (ver `05_EXPERIENCIAS.md`).
|
||||
|
||||
Cada linha em `build_speaker_review` carrega suas `words` originais (ênfase
|
||||
por palavra), para a tela desenhar os mesmos chips da etapa 5 sem esperar um
|
||||
recálculo. `apply_speaker_review` é reaproveitada por dois caminhos: gravar
|
||||
(`write_clean_voice_timeline`, via `save_speaker_review`) e só **prever**
|
||||
(`cmd_recalc_speaker_review`, sem tocar disco) — o botão "Recalcular" da tela
|
||||
usa o segundo caminho para atualizar a ênfase só sobre quem sobreviveu ao
|
||||
corte, sem reprocessar áudio.
|
||||
|
||||
### `phrase_review.py` — a revisão humana
|
||||
|
||||
Junta o timeline de voz com as ações da IA numa lista de frases editáveis, e
|
||||
@@ -175,6 +211,25 @@ converte de volta. Frase inativa vira `cut`; ênfase ≥ 1 vira `zoom` mais um
|
||||
Estes três não estão divididos porque **cada um já é um assunto só**. O
|
||||
`text_layout.py` tem 901 linhas de um problema coeso: diagramação.
|
||||
|
||||
### Separação de role entre legendas dinâmicas e convencionais
|
||||
|
||||
As duas categorias são ambas `<title>` conectados, mas recebem **roles
|
||||
diferentes** para ficarem didáticas na timeline do FCP (cada role ganha cor
|
||||
própria no índice). O atributo usado em `<title>` é `role` (CDATA) — **nunca**
|
||||
`videoRole`, que é DTD-inválido para títulos (ver `05_EXPERIENCIAS.md`,
|
||||
entrada 32).
|
||||
|
||||
| Categoria | `role` | De onde vem |
|
||||
|-----------|--------|-------------|
|
||||
| Legendas dinâmicas | `titles.dinamicas` | `DynamicSubtitleConfig.role` / `load_dynamic_subtitle_config()["role"]` |
|
||||
| Legendas convencionais | `titles.convencionais` | `load_plain_subtitle_config()["role"]` |
|
||||
|
||||
A cor do texto em si continua nos configs de fonte (abas do app), não no
|
||||
role. Os geradores `generate_dynamic_subtitles` (writer/titles.py),
|
||||
`handle_generate_plain_subtitles` e `handle_generate_subtitles_by_emphasis`
|
||||
(server_tools/subtitles.py) aplicam o role em cada `<title>` criado; o
|
||||
parâmetro `role` das ferramentas MCP sobrescreve o default.
|
||||
|
||||
---
|
||||
|
||||
## Armadilhas do FCPXML (custaram sessões de depuração)
|
||||
|
||||
Reference in New Issue
Block a user