chore: atualização geral

This commit is contained in:
João Henrique
2026-08-19 16:35:29 -04:00
parent 8fca456ceb
commit e7748c2c58
66 changed files with 13037 additions and 4237 deletions
@@ -0,0 +1,71 @@
# 01 — Leitura do JSON
O arquivo `<mídia>_voice_timeline.json` é a entrada de todo o trabalho.
Leia em camadas, de cima para baixo, e só desça quando precisar.
## Camadas
| Camada | O que traz | Para quê |
|---|---|---|
| `layers` | o que de fato rodou na análise | **leia primeiro** — ver `07-analise-incompleta.md` |
| `summary` | forma da peça, `peak_moments`, contagens | visão geral em poucos números |
| `speakers` | quem fala, % do tempo, frases de exemplo | identificar papéis |
| `segments` | cada fala com seus agregados | **onde você mais trabalha** |
| `segments[].words` | detalhe por palavra | achar o instante exato de um destaque |
| `scales` | o que cada número significa | documentação dentro do próprio arquivo |
## Campos que decidem quase tudo
**`gap_before`** — silêncio antes da fala, em segundos. É o mapa estrutural
da gravação: acima de ~3s (`take_boundary: true`) a câmera parou ou a
tomada recomeçou. Num material real de 3min17s isso identificou 6
fronteiras, todas exatamente onde a pessoa recomeçava o roteiro.
**`take_boundary`** — booleano derivado do `gap_before`. Use para agrupar
tomadas.
**`emphasis`** (0–1) — índice combinado de energia, variação de tom,
variação de ritmo, pausa anterior e duração. **É relativo ao material
analisado**, nunca uma medida absoluta. Ver `02-enfase-e-reanalise.md`.
**`energy`** (0–1) — intensidade relativa ao trecho mais alto da gravação.
**`pitch_delta`** (0–1) — quanto o tom se afasta da média do falante.
**`peak_emphasis`** e **`avg_energy`** (por segmento) — permitem julgar uma
frase inteira sem ler palavra por palavra. É por aqui que você avalia o
arco narrativo.
**`energy_raw`** e **`pitch_hz`** — valores brutos, sem normalização. Não
use para decidir; existem para permitir a reanálise da Fase 2.
## O que NÃO fazer
- **Não recalcule** energia, tom ou ênfase. O sistema mede melhor e de
forma reprodutível.
- **Não reestime tempos "no olho".** Use os timestamps do JSON.
- **Não trate `emphasis` como valor absoluto.** Um 0,35 pode ser o pico de
uma gravação e ruído em outra.
## 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.
Isso não é "reestimar no olho" — é um bug de medição na fonte, não um
julgamento seu. Na prática:
- 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
de decidir, ou confira com `ffmpeg -af astats` se a precisão importar
para o frame.
- **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.
@@ -0,0 +1,51 @@
# 02 — Triagem: roteiro vs. conversa de bastidor
**Primeira coisa a fazer, antes de qualquer decisão de efeito.**
Material bruto de gravação quase nunca é uma tomada só. A pessoa lê o
roteiro, erra, conversa com a equipe e recomeça.
## Por que isso é tarefa sua, e não do sistema
No áudio essa separação é **invisível** — e pior: o índice de ênfase
*favorece* a conversa, que é mais solta e mais alta que o texto decorado.
Caso real: a fala mais enfática de um vídeo inteiro (energia **1,00**, o
topo absoluto da gravação) era *"Amor, eu tô intacto!"*, dita para o marido
fora de quadro. Três das sete palavras de maior ênfase do vídeo vinham
dessa única frase de bastidor.
Nenhum limiar acústico separa isso. O **texto** separa sem erro.
> Atenção: isso também **não é diarização**. Num caso real, a pessoa da
> equipe estava fora do microfone — a diarização a ouvia, mas o Whisper não
> a transcrevia. As falas a descartar eram da **própria protagonista**:
> mesma voz, contexto diferente. "Quem fala" e "isso é tomada válida" são
> perguntas diferentes.
## Descartar — conversa com a equipe
Reconhece-se pelo **conteúdo**:
- **vocativo para alguém da sala** — *"Amor, eu tô intacto!"*
- **pergunta operacional** — *"Posso começar da mastopexia?"*,
*"E aí, continua?"*, *"Mas eu vou ter que falar tudo de novo?"*
- **instrução técnica** — *"Só clica aí agora na tela."*, *"Aumenta."*
- **comentário sobre a própria gravação** — *"Vou falar só a última frase,
só um pouquinho, não pegou?"*
## Descartar — frases interrompidas
Texto que morre no meio, tipicamente em reticências ou emendando numa
pergunta:
- *"E tudo isso associado à medida..."*
- *"Aquela mama com um formato mais estruturado, com o colo que..."*
- *"de pele..."*
## Sinais estruturais que ajudam
Use `take_boundary` para achar onde cada tomada recomeça. Num material
real, as fronteiras (gaps de 3,6s a 19,8s) caíam exatamente nos pontos onde
a médica reiniciava o roteiro — inclusive nas duas retomadas da frase de
abertura.
@@ -0,0 +1,60 @@
# 03 — Escolha da melhor tomada
A mesma frase costuma aparecer 2, 3, 4 vezes. Seu trabalho é ficar com
**uma**.
## Como agrupar
1. Use `take_boundary` para localizar onde cada tomada recomeça.
2. Agrupe as repetições **pelo texto**, não pelo tempo — a mesma frase
reaparece em pontos distantes da gravação. Num caso real, a abertura
*"Aquela mama com um formato mais estruturado"* apareceu aos 2,0s, 64,9s
e 86,4s.
## Critérios, nesta ordem
### 1. Completa
Não morre no meio, não emenda numa pergunta. Uma tomada incompleta está
descartada por definição, mesmo que a dicção seja ótima.
### 2. Dicção limpa
Sem tropeço, sem repetição de palavra, sem vício de linguagem. **Compare os
textos lado a lado:**
| Tomada 1 | Tomada 3 | Escolha |
|---|---|---|
| *"isso é desejo de muitas mulheres"* | *"**aí** isso é desejo de muitas mulheres"* | Tomada 1 |
### 3. Formulação melhor
Quando as duas estão limpas, prefira a mais direta — normalmente a última,
porque é onde a pessoa já se ajustou:
| Antes | Depois | Escolha |
|---|---|---|
| *"a gente **faz a inserção de** próteses"* | *"a gente **insere** próteses"* | a segunda |
| *"reestrutura a mama"* | *"reestrutura a **sua** mama"* | a segunda |
### 4. Entrega
**Só então** desempate por `avg_energy` / `peak_emphasis`.
Quando duas tomadas têm texto **idêntico palavra por palavra**, aí a
energia decide sozinha — é o único sinal disponível. Caso real: o fecho
tinha duas tomadas iguais, energia **0,38** e **0,19**. A de 0,38 é a boa,
e o texto sozinho jamais diria isso.
## Regra de ouro
A **última** tomada costuma ser a melhor — é onde a pessoa acertou. Mas
**confirme lendo o texto**; nunca assuma.
## Quando estiver em dúvida
Não decida no escuro. Coloque um `marker` nas duas candidatas, explique a
dúvida no `reason`, e deixe a escolha para o editor humano.
## Continuidade
Ao montar o corte final você pode misturar blocos de tomadas diferentes —
abertura da tomada 1, corpo da tomada 3. Isso é normal. Mas **avise nas
emendas**: coloque um `marker` em cada junção para o editor conferir se o
enquadramento e a posição da pessoa combinam.
@@ -0,0 +1,70 @@
# 04 — Reanálise do material que sobrou
**Não escolha zooms com os números da análise bruta.**
## O problema
Ênfase e energia são **relativas ao conjunto analisado**. Energia é
normalizada contra o momento mais alto da gravação; ênfase deriva dela.
Se esse momento mais alto foi cortado — uma piada, um grito, uma conversa
de bastidor — tudo que sobrou continua pontuado contra uma referência que o
espectador **nunca verá**. As notas do corte final ficam artificialmente
comprimidas, e o ranking aponta para as palavras erradas.
Caso real: o pico do vídeo era *"Amor, eu tô intacto!"* (energia 1,00),
descartado na triagem. Todo o material restante estava sendo medido contra
ele.
## A solução
Depois de definir os cortes, renormalize sobre os sobreviventes. Chame a
ferramenta **`refine_voice_timeline`**, passando a mídia e a lista de
cortes que você já decidiu:
```
refine_voice_timeline(media_path, cuts=[{start, end}, ...], min_gap=8.0)
```
Ela devolve, numa chamada só, a comparação bruto × sobreviventes, os picos
re-ranqueados e os candidatos a zoom. É barata: renormaliza os números já
medidos, sem reabrir o áudio.
Efeito medido no mesmo material:
| | Bruto | Só o que sobrou |
|---|---|---|
| Ênfase média | 0,179 | **0,197** |
| *"Aquela"* | 0,39 | **0,42** |
| *"mastopexia"* | 0,26 | **0,34** |
| *"devolver"* | — | **0,35** |
*"mastopexia"* só virou candidata legítima depois da reanálise.
## As janelas que ela propõe
A seção **Zoom Candidates** da resposta já vem com três coisas resolvidas:
1. **Pega a palavra de conteúdo mais enfática de cada frase.** Artigos e
conectivos são filtrados — um *"a"* falado alto continua sendo um artigo.
Sem esse filtro, o ranking bruto apontava para "o", "a", "eu": picos de
*entrega*, não de *sentido*.
2. **Estende a janela até o fim da frase**, não do segmento (ver
`05-zoom.md`).
3. **Mantém distância mínima** entre zooms.
São **candidatos, não obrigações.** Corte a lista pelo ritmo
(`07-ritmo.md`). Os tempos continuam na mídia original — vão direto para
`apply_voice_actions`.
`max_zooms` limita a lista, mas prefira cortá-la você mesmo: o corte por
ritmo é decisão editorial, não um teto numérico.
## Princípio geral
> "Qual o momento mais forte da **gravação**?" e "qual o momento mais forte
> do **vídeo final**?" são perguntas diferentes sempre que a métrica for
> relativa.
Toda métrica normalizada precisa ser recalculada quando o conjunto muda —
senão ela responde a pergunta errada, silenciosamente.
@@ -0,0 +1,71 @@
# 05 — Zoom (punch-in)
## Quando usar
No momento em que o argumento vira. Um pico acústico só merece zoom se for
também um pico **de sentido**.
Palavra gritada sem peso narrativo não ganha nada — e isso inclui os picos
que caem em artigos e conectivos, que são picos de entrega, não de conteúdo.
## A janela
**`start`** — na palavra de ênfase.
**`end`** — no **fim da frase**. A frase inteira, não o fim do segmento da
transcrição.
O Whisper corta frases no meio, por respiração e não por gramática:
> *"Aquela mama com um formato mais estruturado, que valoriza o seu colo,
> que dá aquele ar"* **|** *"de elegância, isso é desejo de muitas mulheres,
> né?"*
Soltar o zoom no fim do primeiro segmento libera **no meio do pensamento** —
é o que faz um punch-in parecer arbitrário. `suggest_zoom_windows` já
estende até a pontuação final (`.` `!` `?` `…`), e nunca atravessa uma
fronteira de tomada.
## A forma — o programa decide sozinho
Você escolhe `start` e `end`; a forma sai da posição da janela dentro do
trecho:
| Situação | Comportamento | Por quê |
|---|---|---|
| Começa a **>0,5s** do início do trecho | entrada rápida (~0,25s) | o movimento chega junto com a palavra |
| Começa a **≤0,5s** do início | **entra já ampliado, sem transição** | o corte já foi a transição; uma rampa ali lê como a imagem se acomodando |
| Frase termina no meio do trecho | **saída seca**, 1 frame | volta ao enquadramento sem chamar atenção |
| Frase termina a **≤1s** do corte | **não volta** — segura até o corte | o próximo trecho já abre no enquadramento dele; voltar antes é movimento desperdiçado |
Os limiares são diferentes de propósito: no fim o corte esconde um retorno
inacabado, mas no início a rampa é visível desde o primeiro frame.
Para forçar manualmente, existem `start_at_peak` e `hold_at_end` — mas o
automático acerta na quase totalidade dos casos.
## Escala
| Valor | Uso |
|---|---|
| 1,15 | sutil |
| 1,18 – 1,3 | padrão |
| 1,5 | forte |
Em vídeo institucional, fique na faixa baixa. Acima de 3,0 é rejeitado.
O zoom é **relativo ao enquadramento existente**: se o clipe já tem escala
1,77 (material gravado de lado e reenquadrado), um zoom 1,18 anima de 1,77
para 2,09 e preserva rotação e posição.
## Dois zooms no mesmo clipe
Depois do corte, dois picos que você escolheu podem cair no **mesmo**
trecho sobrevivente (nenhum corte os separou em clipes distintos) — é
comum quando a corrida limpa de uma tomada é longa. O sistema resolve isso
sozinho, e a regra é a mesma que rege o resto: janelas **distantes**
empilham (os dois zooms convivem, cada um voltando ao enquadramento real
entre um e outro); janelas que **se sobrepõem** substituem (é o mesmo
evento sendo reajustado, não dois). Você não precisa calcular isso na
hora de decidir — só respeitar o `min_gap` de `07-ritmo.md`, que já
garante que dois zooms escolhidos por você nunca se sobrepõem.
@@ -0,0 +1,88 @@
# 06 — Texto, corte e marcador
## Texto
Para fixar um **conceito, número ou nome** que o espectador precisa reter.
- Use a palavra **dita**, não uma paráfrase.
- Curta, em caixa alta. Até 120 caracteres (é truncado além disso).
- Uma por frase, no máximo.
**Não legende a frase inteira.** Para isso existe
`generate_dynamic_subtitles`, que é outra ferramenta e outro propósito.
Boas candidatas são as palavras-chave que sobram depois de filtrar as
funcionais — num caso real: *mastopexia*, *flacidez*, *próteses*,
*devolver*, *desejo*.
## Corte
Digressão, repetição, frase abandonada, conversa de bastidor, tomada pior —
e mais duas coisas que **são** seu trabalho, ao contrário do que parece.
### Vícios de linguagem entram na sua lista
Não delegue para `remove_filler_words`. Você já está percorrendo palavra por
palavra na triagem; marcar as muletas é uma linha a mais, sem custo. E você
tem o que a lista fixa não tem: **contexto**.
Um *"tipo"* em *"tipo assim, sabe"* é muleta. Em *"esse tipo de cirurgia"*
é a palavra principal. Um *"não não não"* pode ser gagueira ou ênfase. A
lista fixa não distingue; você distingue.
### Lacunas longas entram na sua lista — curtas, nunca
**O tamanho da lacuna muda o que ela é.** A régua está medida em material
real (`pause_weight()` em `emphasis.py`, `TAKE_BOUNDARY_GAP` em
`voice_timeline.py`):
| `pause_before` | O que é | O que fazer |
|---|---|---|
| até ~1,5s | o falante montando a frase — **isso É a ênfase** | **nunca cortar** |
| 1,5–3s | zona cinza | julgue pela frase |
| acima de 3s | troca de tomada, ar morto, outra pessoa falando | **cortar** |
Cortar a pausa curta é o erro grave: ela é uma das cinco entradas do índice
de ênfase, então você estaria apagando justamente a batida que faz a palavra
seguinte pontuar alto. Uma frase fluida não se aperta.
Acima de 3s a pausa deixa de contar como ênfase por construção — medido em
material real, lacunas de 6–9s rankeavam como os momentos mais enfáticos da
gravação só porque a escala saturava.
### O que continua NÃO sendo seu trabalho
| Tarefa | Ferramenta | Por quê |
|---|---|---|
| Apertar o ar **dentro** da fala | `remove_media_silence` | Lê o áudio real com ffmpeg; você só tem os intervalos entre palavras transcritas |
E cuidado: **ausência de fala não é ausência de som.** Respiração, riso,
suspiro, a reação depois da frase — nada disso vira palavra, então aparece
como lacuna, e às vezes é o melhor frame do vídeo. Lacuna longa é candidata
a corte, não corte automático.
`remove_media_silence` roda **depois** de `apply_voice_actions`, como
acabamento opcional, sobre o material que sobrou.
**Antes de rodar `remove_media_silence` sobre o corte final, sempre rode a
detecção primeiro** (sem aplicar) e leia os spans um a um contra a régua
acima. O detector corta por limiar de dB — ele não sabe distinguir "batida
de 0,8s entre duas frases", que a régua protege, de "ar morto de emenda",
que deveria ser apertado. Aplicar direto, sem essa checagem, é o mesmo erro
de cortar pausa curta, só que por outra ferramenta.
## Marcador
Quando você quer **sinalizar para o editor humano decidir**, em vez de
decidir por ele.
Use em:
- **Emendas entre tomadas** — sempre. O editor precisa conferir se o
enquadramento e a posição da pessoa combinam na junção.
- **Dúvida entre duas tomadas** — marque as duas, explique no `reason`.
- **Momentos que talvez mereçam efeito** mas que você não tem confiança
para decidir.
Marcador é um **ponto**, não um trecho: sobrevive mesmo encostado na borda
de um corte, o que é justamente o caso das emendas.
@@ -0,0 +1,37 @@
# 07 — Ritmo
**O erro mais comum é efeito demais.** Cansa mais que efeito de menos, e
denuncia edição automática.
## Limites
| Regra | Valor |
|---|---|
| Distância mínima entre dois zooms | **8–10 segundos** |
| Zooms por minuto de vídeo | **2 a 4** (teto) |
| Zoom + texto no mesmo instante | só com motivo claro |
Se dois picos estiverem colados, **escolha o mais forte e abra mão do
outro**. Não tente encaixar os dois.
## Candidatos ≠ obrigações
`suggest_zoom_windows` devolve uma lista de candidatos. Normalmente você usa
uma **fração** dela.
Caso real: num corte de 47,6s a ferramenta sugeriu **5** janelas. O certo
foram **3** — 5 violaria o teto de 2–4 por minuto. Ficaram a abertura, o
termo central e o fecho; as duas descartadas eram frases de apoio.
O mesmo vale para `peak_moments` no `summary`: é lista de candidatos.
## Como escolher quais manter
Quando precisar cortar a lista, priorize por **função narrativa**, não por
nota:
1. **A abertura** — prende o espectador.
2. **O conceito central** — o termo que o vídeo existe para explicar.
3. **O fecho** — a frase que fica.
Só depois disso, as frases de apoio, por ordem de ênfase.
@@ -0,0 +1,65 @@
# 08 — Formato de saída
O produto do seu trabalho é **este JSON**. É ele que vai para o programa
gerar o FCPXML. Você nunca escreve XML.
## Estrutura
```json
{
"source": "0E6A8290.mp4",
"actions": [
{"kind": "cut", "start": 21.9, "end": 127.6,
"reason": "tomadas descartadas, frases interrompidas e conversa com a equipe"},
{"kind": "zoom", "start": 2.0, "end": 10.7,
"params": {"scale": 1.15}, "reason": "abertura: \"Aquela mama\" (ênfase 0.42)"},
{"kind": "text", "start": 127.7, "end": 129.0,
"params": {"content": "MASTOPEXIA"}, "reason": "fixa o termo central"},
{"kind": "marker", "start": 21.85, "end": 22.0,
"reason": "EMENDA 1 — conferir junção entre tomadas"}
]
}
```
## Regras
### 1. Tempos em segundos da mídia ORIGINAL
Exatamente como aparecem no `voice_timeline.json`.
**Nunca compense para "depois do corte".** O programa faz esse deslocamento
sozinho: ele resolve os cortes primeiro e reposiciona todo o resto. Se você
compensar por conta própria, **todo destaque cai no frame errado** — e o
erro é silencioso.
### 2. `end` sempre maior que `start`
Ambos ≥ 0. Um `end <= start` é rejeitado.
### 3. Tipos
`cut` · `zoom` · `text` · `marker`
### 4. Parâmetros por tipo
| Tipo | `params` |
|---|---|
| `cut` | nenhum |
| `zoom` | `scale` entre 1.0 e 3.0 (padrão 1.3 se omitido) |
| `text` | `content` **obrigatório**, até 120 caracteres |
| `marker` | opcional: `content` vira o nome do marcador |
### 5. `reason` — sempre preencha
É o que o usuário lê para revisar sua decisão, e o que te obriga a **ter**
uma. Um `reason` vazio é sinal de decisão sem critério.
Inclua o dado que embasou: *"abertura: 'Aquela mama' (ênfase 0.42)"* é útil;
*"zoom"* não é.
## Como o programa trata erros
- **Ação inválida** → rejeitada e reportada **individualmente**. Uma linha
malformada nunca derruba as outras.
- **Ação apontando para material cortado** → descartada e reportada, nunca
deslizada para o conteúdo vizinho.
- **Ação fora da mídia** → reportada como não colocada.
Você recebe o relatório dos três casos. **Repasse ao usuário** — nunca
relate só os acertos.
@@ -0,0 +1,48 @@
# 09 — Quando a análise veio incompleta
O bloco `layers` no topo do JSON diz **o que de fato rodou**. Leia antes de
qualquer outra coisa.
```json
"layers": {"transcript": true, "acoustics": false, "speakers": false}
```
## Por que esse bloco existe
Fala monótona e acústica que não carregou deixam **os mesmos zeros** nos
dados. Sem o `layers`, é impossível distinguir "esta pessoa fala de forma
uniforme" de "a análise acústica falhou".
## Os casos
### `acoustics: false`
Todos os valores acústicos são 0. **Você não tem ênfase real.**
- Decida só pelo texto.
- **Avise o usuário** explicitamente.
- Prefira `marker` a `zoom` — sinalize em vez de decidir.
Causa comum: o componente librosa não está instalado, ou o ffmpeg não
conseguiu extrair o áudio do container.
### `speakers: false` num vídeo com várias pessoas
A diarização não rodou — falta o token do HuggingFace (aba Modelos do app).
- Avise antes de tratar tudo como uma voz só.
- Lembre que isso **não impede** a triagem roteiro/conversa, que é feita
pelo texto (ver `02-triagem-roteiro-vs-conversa.md`).
### `peak_count: 0`
Nada cruzou o piso de ênfase. Duas causas possíveis:
1. A fala é uniforme mesmo — material sem picos.
2. O limiar está alto para esse material.
Sugira ajustar em **Análise de Voz** no app. **Não force destaques
inexistentes** só para entregar alguma coisa.
## Regra geral
Não finja precisão que você não tem. Uma edição entregue com a ressalva
certa é útil; uma entregue como se estivesse completa, quando metade dos
dados faltou, custa a confiança do usuário no sistema inteiro.