# Fase 2: como escrever o `edit_data.json`

O `compose_final.py` lê `<EDIT>/edit_data.json` e monta o `final.mp4` em cima do `cut.mp4`.
Leia este arquivo antes de escrever o primeiro `edit_data.json` da sessão.

Três regras valem para tudo:

- **Caminhos** são relativos à pasta `<EDIT>` (ex.: `"assets/foto1.jpg"`).
- **Tempos** são segundos no `cut.mp4`. O final tem a mesma duração do corte, então o tempo no
  final é o mesmo do corte.
- **Seção que não está no arquivo fica desligada** (câmera, gancho, legenda, inserts, animações,
  chamada, aviso, efeitos, trilha, limpeza de ruído). Os padrões do catálogo servem para a tela de estilo,
  não para o compose: escreva cada elemento escolhido. Uma seção com `"enabled": false` também
  desliga.
- **Posição automática.** `camera.focus_y` e os `center_y` do gancho, da legenda, da chamada e do
  aviso aceitam um número ou `"auto"`. Deixe de fora (é o mesmo que `"auto"`): o compose acha o
  rosto em cada trecho do corte e põe cada coisa no lugar certo. Número só se a pessoa pedir.

## Exemplo completo

```json
{
  "version": 1,
  "base": "cut.mp4",
  "timeline": "cut_timeline.json",
  "output": {"width": 1080, "height": 1920, "fps": 30},
  "accent": "#FFC400",
  "camera": {"zoom_cuts": true, "zoom_levels": [1.0, 1.14], "push_in": false,
             "push_in_amount": 0.05},
  "hook": {"enabled": true, "style": "manchete", "text": "Olha o que eu *achei pra revender*",
           "start": 0.0, "end": 2.8},
  "captions": {"enabled": true, "style": "impacto", "file": "captions.json",
               "hide_during_hook": true, "font_scale": 1.0},
  "inserts": [
    {"type": "split_top", "src": "assets/bolsa1.jpg", "start": 5.5, "end": 9.0,
     "ken_burns": true, "split": 0.46, "sfx": "whoosh"},
    {"type": "full", "src": "assets/vitrine.jpg", "start": 16.6, "end": 18.4}
  ],
  "transitions": [{"at": 16.6, "type": "flash"}],
  "motion": [
    {"template": "checklist", "start": 10.2, "end": 13.4,
     "params": {"title": "O que você *recebe*:", "items": ["Acesso na hora", "Suporte", "Garantia"],
                "times": [0.3, 1.1, 1.9]}}
  ],
  "cta": {"enabled": true, "text": "Toque em Saiba Mais", "sub": "Acesso na hora",
          "start": null, "duration": 2.5},
  "sfx": {"enabled": true, "volume": 0.6},
  "music": {"enabled": false},
  "audio": {"denoise": "off"},
  "loudness": {"i": -14, "tp": -1.0}
}
```

## Campo por campo

### Raiz

| Campo | Padrão | O que faz |
|---|---|---|
| `version` | 1 | Sempre 1. |
| `base` | `"cut.mp4"` | Vídeo de base. |
| `timeline` | `"cut_timeline.json"` | Onde ficam os cortes (zoom muda em cada um). |
| `output` | do timeline | `width`, `height`, `fps`. Deixe igual ao corte. |
| `accent` | `"#FFC400"` | Cor de destaque (formato `#RRGGBB`). Vale para legenda, gancho e chamada. |
| `edit_type` | nenhum | `"tela_verde"` liga a tela verde (seção `tela_verde`): os inserts `split_top`/`split_bottom` viram o fundo atrás da pessoa. `clean`, `split_top` e `split_bottom` são só anotação: o que muda o vídeo são os `inserts`. |

### `camera`

| Campo | Padrão | O que faz |
|---|---|---|
| `zoom_cuts` | false | Muda o zoom exatamente em cada corte, alternando `zoom_levels`. Disfarça o pulo do corte. |
| `zoom_levels` | `[1.0, 1.14]` | Zooms em ciclo, cada um entre 1.0 e 1.6. Mais que 1.2 começa a perder nitidez. |
| `push_in` | false | Aproximação lenta e suave dentro de cada trecho. |
| `push_in_amount` | 0.05 | Quanto aproxima em cada trecho (0 a 0.25). Entre 0.03 e 0.06 fica natural. |
| `focus_y` | `"auto"` | Altura dos olhos como fração da altura do vídeo (0.1 a 0.9). Auto: os olhos de cada trecho. O zoom enquadra em volta dele. |

### Como a posição automática funciona

O compose roda o `face_layout.py` (detector de rosto YuNet) em vários quadros de cada trecho do
`cut.mp4` e grava `<EDIT>/face_layout.json`. Com isso:
- o zoom de cada trecho mira nos olhos daquele trecho;
- a tela dividida reenquadra a pessoa em cada trecho que a janela cobre, e empurra o rosto para
  longe da legenda que fica na divisão;
- cada texto vai para o lugar livre mais perto da altura normal (gancho 430, legenda 1400,
  chamada 1110 ou 1180, aviso 1548, na tela 1080x1920), sem cobrir olhos, nariz e boca;
- textos que aparecem juntos nunca se sobrepõem. Prioridade: chamada, gancho, legenda, aviso. O
  de menor prioridade muda de lugar (se estiver em auto) ou some enquanto os dois estariam juntos.
  Tudo fica em `verify/final_report.json` (`adjustments`).

Para ver o que ele achou antes de montar (uma linha de resumo):
`uv run --directory '<SKILL>' python helpers/face_layout.py --edit-dir '<EDIT>'`
Se o resumo disser "o rosto muda de altura entre os trechos", deixe tudo em auto: cada trecho
ganha a sua posição. Trecho sem rosto (tela, produto) usa a posição normal.

### `hook` (gancho)

| Campo | Padrão | O que faz |
|---|---|---|
| `enabled` | true | Liga o gancho. |
| `style` | `manchete` | Um id de estilo de gancho (tabela abaixo). |
| `text` | obrigatório | 1 ou 2 linhas. `*palavras*` entre asteriscos saem na cor de destaque. |
| `start` | 0.0 | Entrada. |
| `end` | 3.0 | Saída. Entre 2 e 3,5 s. Termine junto do fim de uma frase. |
| `center_y` | `"auto"` | Centro vertical em pixels (tela 1080x1920). Auto: perto de 430, fora do rosto. |
| `sfx` | `"pop"` | Som na entrada. `null` tira. |

- O gancho entra com um pulinho e some suave nos últimos quadros.
- A legenda some enquanto o gancho está na tela (`hide_during_hook`), então não briga com ela.
- Número manual: nunca acima de 300 (a barra do Reels cobre o topo).
- O vídeo já tinha texto escrito na imagem: ponha o gancho numa altura longe dele (número manual)
  e confira na folha de quadros.
- Texto longo encolhe a letra e o compose avisa "gancho longo demais". Encurte.

### `captions` (legendas)

| Campo | Padrão | O que faz |
|---|---|---|
| `enabled` | true | Liga as legendas. |
| `style` | do `captions.json` ou `impacto` | Id de estilo de legenda (tabela abaixo). |
| `file` | `"captions.json"` | Gerado pelo `captions.py`. |
| `hide_during_hook` | true | Esconde a legenda enquanto o gancho aparece. |
| `font_scale` | 1.0 | 0.9 a 1.15. Acima disso a largura segura limita o tamanho. |
| `center_y` | `"auto"` | Altura em pixels (tela 1080x1920). Auto: perto de 1400 em cada trecho, fora do rosto. Um número vale para o vídeo inteiro. |

- O `captions.json` é agrupado para um estilo. Mudou o estilo? Rode de novo
  `captions.py --edit-dir '<EDIT>' --style NOVO`. O compose avisa quando os dois não batem e recusa
  legendas feitas para outro corte.
- As palavras vêm das transcrições das fontes, mapeadas no tempo do corte. Linhas `confira:` do
  `captions.py` mostram onde a transcrição do corte ouviu outra coisa; corrija com
  `--replace 'errado=certo'`.
- Durante uma tela dividida a legenda vai para a linha da divisão (mesmo com `center_y`).
- **Vídeo que já tem legenda escrita na imagem:** `"enabled": false`, a não ser que a pessoa peça
  as duas. Legenda em cima de legenda é o erro mais visível de um anúncio.

### `inserts` (imagens e vídeos por cima)

Lista. Cada item:

| Campo | Padrão | O que faz |
|---|---|---|
| `type` | obrigatório | `split_top`, `split_bottom`, `full` ou `pip`. |
| `src` | obrigatório | Imagem (jpg, png, webp) ou vídeo (mp4, mov). |
| `start`, `end` | obrigatórios | Entrada e saída, exatas no quadro. |
| `ken_burns` | true em imagem, false em vídeo | Movimento lento de zoom e deslize (até 6%). |
| `split` | 0.46 | Fração da altura para a imagem na tela dividida (0.25 a 0.75). |
| `sfx` | `"whoosh"` | Som na entrada. Efeito pronto: o ponto forte dele cai no quadro em que a imagem entra. Arquivo: começa 2 quadros antes. `null` tira. |
| `corner` | `"top_right"` | Só no `pip`: `top_right` ou `top_left`. |

O que cada tipo faz:
- `split_top`: imagem na faixa de cima, a pessoa reenquadrada embaixo (em cada trecho que a janela
  cobre), com uma sombra suave na divisão. Bom para mostrar produto ou tela enquanto a pessoa fala.
- `split_bottom`: pessoa em cima, imagem embaixo. Use quando o rosto fica alto no quadro.
- `full`: a imagem cobre a tela toda (a voz continua). Use por pouco tempo, até 2,5 s.
- `pip`: cartão branco arredondado com sombra, 380 px de largura, no canto de cima. Coloque no
  canto oposto ao rosto.
- Vídeo como insert começa do próprio início e repete se for mais curto que a janela.
- Inserts sobrepostos: o que vem depois na lista fica por cima (evite).
- Na tela verde (`"edit_type": "tela_verde"`), `split_top` e `split_bottom` não dividem a tela: viram o fundo
  atrás da pessoa no tempo deles e aceitam `encaixe`, `escurecer` e `desfocar` (seção `tela_verde`).
  `full` e `pip` continuam iguais.

### `tela_verde` (você recortado na frente de um fundo)

Com `"edit_type": "tela_verde"` a pessoa aparece recortada, sem o fundo da gravação, na frente das imagens e
vídeos da edição. Sem esse `edit_type` a seção fica de fora (o compose só avisa). Outro `edit_type` (`clean`,
`split_top`, `split_bottom`) não muda nada no compose.

**As imagens e vídeos entram como inserts normais.** Na tela verde, `split_top` e `split_bottom` não dividem a
tela: cada um vira o fundo atrás da pessoa, exatamente do `start` ao `end` dele, com o mesmo arquivo, o mesmo
`ken_burns`, o mesmo `sfx` e os flashes de `transitions` nas bordas. O `split` dele não vale. `full` continua
cobrindo a tela toda e `pip` continua como cartão por cima, no canto longe do rosto (o `corner` pedido, se ele
não cair no rosto). Nunca ponha tela dividida e tela verde no mesmo vídeo: trocar o tipo não pede mudar os
inserts. `tela_verde.fundo` é só para fundo extra que não depende de um momento da fala (uma página rolando o
vídeo todo, uma cor).

```json
"edit_type": "tela_verde",
"tela_verde": {"posicao": "direita", "altura": 0.55, "contorno": {"cor": "#FFFFFF", "largura": 8},
               "sombra": true, "fundo_original_desfocado": false, "sem_rosto": "original",
               "fundo": [{"src": "assets/pagina_rolando.mp4", "start": 0, "encaixe": "cobrir", "escurecer": 0.25,
                          "ken_burns": false, "desfocar": 0}]},
"inserts": [{"type": "split_top", "src": "assets/print_painel.png", "start": 0.0, "end": 5.0},
            {"type": "split_top", "src": "assets/clipe_quadrado.mp4", "start": 13.9, "end": 16.8, "sfx": null},
            {"type": "full", "src": "assets/vitrine.jpg", "start": 20.1, "end": 21.9}]
```

| Campo | Padrão | O que faz |
|---|---|---|
| `posicao` | `"direita"` | `"direita"` (canto direito), `"esquerda"` (canto esquerdo) ou `"centro"` (meio). |
| `altura` | 0.55 no canto, 0.75 no meio | Fração da altura da tela que a pessoa ocupa, do alto da cabeça até embaixo (0.3 a 1.0). No canto a pessoa nunca passa de 72% da largura, e no meio nunca passa da largura da tela: gravação bem de perto fica menor para continuar parecendo canto e não perder nitidez. |
| `contorno` | `{"cor": "#FFFFFF", "largura": 8}` | Contorno em volta da pessoa, em pixels da tela 1080 de largura (0 a 40). `null` tira. |
| `sombra` | true | Sombra suave atrás da pessoa. |
| `fundo` | nenhum | Fundo de base, no tempo do corte (abaixo). Os inserts `split_top`/`split_bottom` ganham dele enquanto duram. |
| `fundo_original_desfocado` | false | O próprio vídeo, desfocado e escurecido, onde nenhum fundo cobre. |
| `sem_rosto` | `"original"` | Trecho em que o detector não acha rosto (gravação de tela, produto na mão): `"original"` mostra como foi gravado, em tela cheia; `"recorte"` recorta mesmo assim. |
| `espelhar` | false | `true` vira a pessoa de lado, como num espelho (só os trechos recortados; os mostrados como foram gravados continuam como estão). Use quando a câmera cortou o corpo do lado contrário ao canto escolhido e não há texto na roupa nem nos objetos que aparecem (texto sairia invertido). |

Cada item de `fundo` (e cada insert `split_top`/`split_bottom`, que aceita `encaixe`, `escurecer` e `desfocar`
do mesmo jeito):

| Campo | Padrão | O que faz |
|---|---|---|
| `src` | obrigatório | Imagem (jpg, png, webp, HEIC do iPhone), vídeo (mp4, mov) ou, só no `fundo`, uma cor `#RRGGBB`. |
| `start` | o `end` do item anterior (0 no primeiro) | Segundo do corte em que o fundo entra (no insert, obrigatório). |
| `end` | o `start` do próximo item, ou o fim do vídeo | Segundo em que sai (no insert, obrigatório). |
| `ken_burns` | true em imagem, false em vídeo e cor | Zoom lento de 6%. |
| `encaixe` | `"auto"` | `"cobrir"` enche a tela 1080x1920, cortando o que sobra (página mais alta que a tela mostra o topo). `"inteiro"` mostra a imagem ou o vídeo inteiro e nítido, na largura da tela, na faixa que a pessoa não cobre (acima dela, no canto e no meio), sobre uma cópia desfocada e escura dele mesmo. Vídeo com borda lisa (estúdio, cor lisa, como os clipes de IA) não ganha essa cópia: a própria borda dele continua até as beiradas da tela, sem caixa em volta. `"auto"` usa `cobrir` quando o formato é perto de 9:16 (ou mais alto) e `inteiro` nos outros (print de celular ou de painel, foto deitada, clipe quadrado). |
| `escurecer` | 0.15 com `cobrir`, 0 com `inteiro` e em cor | De 0 a 0.9: escurece o fundo para a pessoa e os textos aparecerem mais. |
| `desfocar` | 0 | Desfoque em pixels (0 a 80). |

**Como o fundo cobre o vídeo**
- Os itens de `fundo` são a base. Um insert `split_top`/`split_bottom` ganha deles do quadro do `start` até o
  quadro antes do `end`. Entre itens do mesmo tipo, o que vem depois na lista fica por cima (o compose avisa).
- Buraco sem nenhum fundo (entre dois inserts sem `fundo`, ou depois do último): continua o fundo de antes. A
  imagem segue com o zoom lento (no máximo o dobro do tempo dela) e depois fica parada; o vídeo continua
  repetindo: cada volta começa com 0,4 s de transição suave a partir do último quadro (não pula nem congela).
  O vídeo toca inteiro, quadro a quadro, no tempo do próprio insert; a transição só aparece depois do fim dele.
- Quadros vazios no começo ou no fim do vídeo (clipe que abre com o cenário vazio e os objetos entram depois,
  ou que termina sumindo) nunca aparecem sozinhos: no começo, o fundo de antes se dissolve no vídeo enquanto
  os objetos entram (sem fundo antes, fica parado o primeiro quadro com conteúdo); no fim, fica o último
  quadro com conteúdo. Clipe que termina encolhendo até sumir (um X que diminui depois de aparecer) para no
  último quadro que ainda tem pelo menos 65% do conteúdo do pico final. O relatório mostra quantos foram em
  `quadros_vazios`.
- Antes do primeiro fundo: o vídeo original desfocado (com `fundo_original_desfocado`) ou uma cor escura, com
  aviso. Sem nenhum fundo: cor escura e aviso.
- `inteiro`: a imagem ocupa a largura da tela e fica na faixa entre o topo (120 px) e a cabeça da pessoa nos
  trechos em que ela aparece. A parte principal (num print com margem vazia, só o cartão) fica centrada nessa
  faixa; a margem vazia pode passar para trás da pessoa. Se a parte principal não couber, a imagem diminui
  (até metade da largura). Com legenda automática, a faixa deixa espaço para ela logo acima da cabeça quando a
  imagem continua com pelo menos 75% do tamanho.
- Vídeo `inteiro` com borda lisa (`borda_lisa` no relatório) fica largo: diminui no máximo até 75% da largura,
  para sobrar a faixa da legenda entre a parte principal e a cabeça. A parte principal é a faixa de linhas
  com 85% dos objetos do clipe todo (um fio ou um brilho na beirada não conta) e fica no alto da faixa; o que
  não cabe acima da cabeça passa para trás da legenda e da pessoa. A borda dele continua até as beiradas da
  tela, então ele vira o cenário atrás de você, não uma caixa.
- Vídeo começa do próprio início quando o item entra.

**Como a pessoa é colocada**
- O recorte sai do `helpers/matte.py` (modelo de recorte de pessoa, no computador, sem internet) e fica
  guardado em `renders/matte/`, um arquivo por trecho do corte. Leva uns 3 minutos por minuto de vídeo na
  primeira vez; depois, mudar fundo, posição ou textos não recorta de novo. Para adiantar:
  `uv run --directory '<SKILL>' python helpers/matte.py --edit-dir '<EDIT>'`.
- A pessoa fica encostada embaixo. No canto, o lado que a câmera cortou (ombro, braço) encosta na borda da
  tela; um corte que ainda cai dentro da tela some num degradê de 90 px só onde o corpo encosta nele (o braço
  que não encosta fica inteiro, e o contorno não aparece através dele). Pessoa que a câmera não cortou fica a
  40 px da borda.
- No canto, trecho em que a câmera corta os dois lados do corpo (os dois em 20% dos quadros ou mais) ocupa a
  largura toda da tela (se para isso a pessoa crescer no máximo 1,6 vez), com a cabeça na mesma altura do
  canto: até 20% da parte de baixo da gravação fica abaixo da tela. Assim nenhum corte cai no meio da tela.
  O compose avisa (`tela verde: em 00:00.00-00:20.03 a câmera corta os dois lados do seu corpo...`) e o trecho
  ganha `largura_toda` e `corte_embaixo` no relatório.
- O contorno vai só em volta do corpo: um vão fechado (entre o braço e o corpo, entre o relógio e o pulso,
  contando a borda de baixo e os lados cortados como fechados) mostra o fundo, sem contorno nem sombra nele.
- O começo de cada trecho do recorte usa o quadro de logo depois como referência: onde a imagem quase não
  mudou e esse quadro mostra fundo, um pedaço da sala que o modelo ainda achava que era você some (só pedaço
  encostado no fundo; mão e buraco dentro do corpo não mudam). O primeiro quadro, que costuma virar a capa,
  sai limpo. No meio, o lado cortado também fica na borda quando dá, e uma gravação cortada dos dois lados cresce
  até encher a largura (se faltar até 15%).
- Onde a imagem não mudou nada entre um quadro e o seguinte, um pulo do modelo (um pedaço da sala que ele toma
  por você por dois ou três quadros) não entra de uma vez: o recorte só vai aos poucos na direção dele.
- Cenário aprendido (câmera parada): de cada tomada (os trechos do mesmo vídeo bruto com o mesmo
  enquadramento) o compose aprende como é a sala atrás da pessoa, com os quadros em que o recorte mostra fundo.
  Onde o recorte fica em dúvida e a imagem é exatamente essa sala, o pedaço sai (névoa, mancha clara que pisca;
  uma sala escura só quando é diferente do corpo ao lado), e a cor da sala sai da borda macia da pessoa (franja
  clara ou azul). O que o modelo toma por corpo com certeza e nunca mostrou como fundo (uma cadeira escura
  entre o braço e o corpo) continua. Câmera que se mexe não ganha cenário. O relatório mostra cada tomada em
  `fundo_do_cenario` (`tomada`, `trechos`, `usado`, `conhece`, `motivo`).
- Trecho em que o rosto sai da câmera por um lado (a pessoa se inclinou) fica encostado nesse lado da tela,
  mesmo que o canto escolhido seja o outro: o rosto nunca aparece cortado no meio da tela. O compose avisa
  (`tela verde, trecho 00:05.07-00:05.97: o rosto sai da câmera pela direita...`) e o trecho ganha `posicao`
  no relatório.
- Se a câmera corta o corpo do lado de dentro do canto em boa parte do vídeo (40% ou mais), o compose avisa e
  sugere o que fica mais natural: o outro canto ou `"espelhar": true`, ou o `centro` quando o corte é dos dois
  lados e a pessoa teria que crescer mais de 1,6 vez para ocupar a largura. Repasse a sugestão para a pessoa em
  uma frase.
- Trechos da mesma tomada ficam do mesmo tamanho. `camera.zoom_cuts` e `camera.push_in` aumentam a pessoa
  (presa no lado da borda, ou na cabeça no meio); o vídeo inteiro não recebe o zoom da câmera, só os trechos
  mostrados como foram gravados.

**Textos**
- Rosto e corpo são calculados onde a pessoa ficou. Legenda, gancho, chamada, aviso e animações em auto nunca
  ficam no rosto e, quando cabe, ficam fora do corpo. Por cima do fundo pode.
- Legenda, gancho e chamada também fogem da parte principal de uma imagem `inteiro`: a legenda fica entre a
  imagem e a cabeça quando cabe, senão abaixo do rosto e, sem espaço ali, por cima da parte de baixo da
  imagem.
- Um `center_y` escrito à mão (legenda, gancho, chamada) fica onde está, a menos que caia no rosto (a pessoa
  mudou de lugar na tela verde): aí vai para a altura livre mais perto, com aviso e `adjustments`
  (`"because": "rosto (tela verde)"`). Para ela se ajustar sozinha, tire o `center_y`.
- O aviso de resultado só aparece se a edição ligar o `disclaimer`.

**No relatório** (`verify/final_report.json`): `layers` com `"type": "tela_verde"`: `posicao`, `altura`,
`contorno`, `sombra`, `espelhar`, `corte_dentro` (`lado`, `parte`, `melhor`, `espelhar`, ou null), `fundo` (cada trecho com o arquivo ou a cor que ficou, `item` quando veio de `fundo`,
`insert` quando veio de um insert, `encaixe` e, no `inteiro`, `rect` e `content` em pixels da saída; em vídeo,
`borda_lisa` e `quadros_vazios` com `inicio` e `fim`),
`inserts_no_fundo` (cada insert que virou fundo: `insert`, `type`, `src`, `start`, `end`, `encaixe`, `rect`,
`stage`), `segments` (por trecho do corte: `mode` `"pessoa"` ou `"original"`, `scale`, `x`, `y`, `zoom`,
`height_share`, `width_share`, `on_screen_edge`, `cut_edge_inside`, `posicao` quando o trecho foi para o lado
em que o rosto sai da câmera, `espelhado`, `largura_toda` e `corte_embaixo` quando o trecho ocupa a largura
toda), `matte` (motor, trechos, do cache ou não, de onde leu: `"fonte"` o vídeo bruto, `"corte"` o `cut.mp4`),
`fundo_do_cenario` e `seconds` (`matte` e `compose`, separados). Cada
insert que virou fundo aparece em `layers` com `"type": "insert"` e `"as": "fundo"`. O compose imprime `tela
verde: os inserts 1, 2 (imagem em cima) vão para o fundo, atrás de você (sem tela dividida)`, `recorte da
pessoa: ...` durante o recorte e `tela verde pronta: recorte Xs, montagem Ys`.

**Exemplos**

Comentando uma página, com o print dela e depois um vídeo do produto (os dois como inserts):

```json
"edit_type": "tela_verde",
"tela_verde": {"posicao": "direita"},
"inserts": [{"type": "split_top", "src": "assets/print_pagina.png", "start": 0.4, "end": 9.5},
            {"type": "split_top", "src": "assets/produto.mp4", "start": 9.5, "end": 15.0, "escurecer": 0.25}]
```

No meio, sem arquivo de fundo, com o próprio vídeo desfocado e contorno amarelo:

```json
"edit_type": "tela_verde",
"tela_verde": {"posicao": "centro", "fundo_original_desfocado": true, "contorno": {"cor": "#FFC400", "largura": 10}}
```

Uma página rolando o vídeo todo como base, com um print que entra só na fala dele:

```json
"edit_type": "tela_verde",
"tela_verde": {"posicao": "esquerda", "fundo": [{"src": "assets/pagina_rolando.mp4", "encaixe": "cobrir"}]},
"inserts": [{"type": "split_top", "src": "assets/print_pedido.png", "start": 6.2, "end": 10.0, "encaixe": "inteiro"}]
```

### `transitions` (flash)

Lista de `{"at": segundos, "type": "flash", "sfx": "click"}`. O flash é um clarão branco de 5
quadros com pico no quadro `at`. `"sfx": null` tira o som. Só existe o tipo `flash`.

### `motion` (animações de texto e número)

Textos e números que se desenham na tela, feitos em Python quadro a quadro: lista com check, lista
riscada, passo a passo, etiqueta de preço, contador, barra, gráficos, palavra de impacto, chamada para
comentar, notificação e destaque desenhado. Servem para o que a IA de vídeo não desenha certo (texto e
número exatos). Vídeo feito com IA (Higgsfield, Kling) continua em `inserts`.

Lista, ou `{"enabled": true, "tema": "papelaria", "quantidade": 3, "items": [...]}`. `tema` é o visual de
todas; um `params.tema` no item vale só para ele:
- papel claro com base colorida, Montserrat, cada um com a sua paleta: `papelaria` (lilás, o estilo da
  Vit, letra à mão Caveat), `papel_azul`, `papel_verde`, `papel_laranja` (Caveat), `papel_grafite`
  (cinza e vermelho-tijolo, sóbrio);
- `pop`: cartão creme, contorno preto grosso e sombra dura; `neon`: cartão escuro com borda brilhante e
  letra condensada;
- `escuro` (vidro) e `claro` (branco): usam a cor de destaque do vídeo.
Papel, pop e neon mantêm a própria paleta mesmo com outra cor no vídeo; só o `cor` do item muda.
`quantidade` é quantas a pessoa pediu no preview (o `motion_sugerir.py` lê). `"enabled": false` desliga
sem apagar. Cada item:

| Campo | Padrão | O que faz |
|---|---|---|
| `template` | obrigatório | O modelo (tabela abaixo). |
| `start` | obrigatório | Segundo do corte em que a animação começa. O quadro 0 dela cai exatamente nesse quadro. |
| `end` ou `duration` | a duração sugerida pelo modelo | Fim (ou duração). Use só um dos dois. Passou do fim do vídeo: é cortada, com aviso. |
| `params` | `{}` | Os campos do modelo (tabela abaixo). Campo que falta usa o padrão. |
| `region` | `"auto"` | `"auto"`: o lugar do modelo, e o compose desvia do rosto, da chamada, do gancho e de outra animação. `"top"`, `"middle"` ou `"bottom"` (ou `topo`, `centro`, `baixo`, `faixa_topo`, `tela`): fica nesse lugar, sem desviar. `{"x": 140, "y": 1000, "w": 800, "h": 520}`: caixa exata em pixels da tela 1080x1920. |
| `sfx` | true | Toca os sons do modelo (só com a seção `sfx` ligada). `false` deixa a animação muda. |
| `enabled` | true | `false` pula este item. |
| `note`, `id` | nenhum | Anotação sua (o `motion_sugerir.py` põe a fala aqui). O compose ignora. |

**Regras de uso**
- Só onde a fala pede: lista, "você não precisa de...", passo a passo, preço, porcentagem, número com
  unidade, "comenta X", e os momentos fortes: a oferta (grátis, liberado, bônus, garantia), a virada da
  história ("o caminho que eu descobri", "mas...") e frase curta de impacto. No máximo uma animação a
  cada 6 s, uma por vez na tela.
- Comece depois do fim do gancho e termine antes da chamada. `comentario_cta` é uma chamada: se ele for
  o fim do vídeo, desligue o card `cta` ou termine antes dele.
- Deixe `region` em `"auto"`. Nunca em cima do rosto.
- **Número só do material.** Use os números, valores e preços que a pessoa disse ou mostrou, como
  qualquer outro número (faturamento vira `contador` ou gráfico). Nunca invente número, venda,
  depoimento ou prova que não existe no material; o preço antigo precisa ser o de verdade.
- `times` (e `cross_times` no `riscado`): segundo de cada item contado do `start` da animação, para cada
  item entrar junto com a fala.

**Sugestões pela fala**

`uv run --directory "<SKILL>" python helpers/motion_sugerir.py --edit-dir "<EDIT>"` lê a fala do corte
(`transcripts/cut.json`) e imprime itens prontos de `motion`, com o tempo de cada palavra e a frase em
`note`. Ele só sugere: nunca mexe no `edit_data.json`. Valor que a pessoa diz que fez ("faturei 50 mil")
vira `contador` com `prefix` "R$", não etiqueta de preço.
`--json` imprime só o JSON; `-o arquivo.json` também grava. `--quantidade N` fica com as N mais fortes,
uma em cada parte do vídeo (padrão: `motion.quantidade` do edit_data); `--tema` põe o visual em todas.
Pontos marcados com "Motion aqui" no preview (`preview_marks.json`, `"kind": "motion"`) entram sempre,
por cima da quantidade, com o pedido da pessoa em `note` ("PEDIDO: ...").

**Ver um modelo antes**

`uv run --directory "<SKILL>" python helpers/motion_render.py --template riscado --params '{"items": ["Ter estoque"]}' --out "<EDIT>/verify/motion"`
gera `riscado.mov` e `riscado_folha.png` (8 quadros com a área segura). `--fundo "<EDIT>/verify/quadro.png"`
usa um quadro do vídeo de fundo; `--list` mostra os modelos e `--describe ID` todos os campos de um deles.

**Modelos**

| `template` | O que mostra | Lugar e duração | Sons |
|---|---|---|---|
| `checklist` | Lista num cartão; cada item entra e ganha um check na cor de destaque. | `baixo`, 3 s | `pop`, `tick` por item, `success` no fim |
| `riscado` | Lista que aparece e depois é riscada item por item (linha vermelha ou X). | `baixo`, 3,5 s | `pop`, `tick` (linha) ou `error` (X) por risco |
| `carimbo` | Uma palavra cai carimbada na tela, torta e com moldura, e a tela treme junto. | `centro`, 2 s | `bass_drop` na batida |
| `selo_garantia` | Selo redondo com serrilha e fita, com o prazo em dias no meio. | `centro`, 2,4 s | `pop` e `success` |
| `passos` | 2 a 4 passos em círculos numerados, ligados por setas. | faixa acima do rosto, 3 s | `pop` por passo |
| `etiqueta_preco` | Etiqueta de preço que pula e fica inclinada; preço antigo riscado em cima. | `baixo`, 3 s | `cash` (com preço antigo: `pop`, `tick`, `cash`) |
| `contador` | Número que sobe até o valor, com prefixo, sufixo e legenda. | `centro`, 3 s | `pop`, `count` |
| `barra_progresso` | Barra (ou anel) que enche até a porcentagem. | `baixo`, 3 s | `pop`, `count`, `success` em 100% |
| `grafico_barras` | 2 a 6 barras que crescem, com valor e nome. | `baixo`, 3,5 s | `pop`, `tick` por barra |
| `grafico_linha` | Linha que se desenha subindo, com área e valor no topo. | `baixo`, 4 s | `pop`, `count`, `pop` no fim |
| `palavra_impacto` | 1 a 4 palavras gigantes que batem na tela com tranco, clarão e tremida. | `baixo`, 2 s | `metal_hit` (ou `bass_drop`, `synth_hit`) por palavra |
| `comentario_cta` | Barra de comentário genérica: a palavra-chave é digitada e enviada. | `baixo`, 3,8 s | `pop`, `keyboard`, `mouse_click`, `pop` |
| `notificacao` | Notificação de celular genérica que desce do alto e sobe no fim. | `faixa_topo` (fixo), 3,5 s | `notification` |
| `destaque` | Traço de caneta num ponto da tela: círculo, sublinhado ou seta. | `target` (fixo), 2 s | `whoosh` (seta: `whoosh` e `pop`) |

`notificacao` e `destaque` ficam sempre no mesmo lugar (não desviam do rosto). Todo modelo aceita
também `tema` (`"escuro"`, padrão, ou `"claro"`; só muda o cartão: com `card: false` a letra é sempre branca
com contorno) e `cor` (cor de destaque só desta animação, ex.: `"#FF3B30"` ou `"vermelho"`).

**Campos de cada modelo** (padrão entre parênteses; textos com `*palavra*` saem na cor de destaque)

| `template` | `params` |
|---|---|
| `checklist` | `items` (1 a 6 textos, até 32 letras), `title` (""; até 36), `delay` (0.25), `stagger` (0.45), `times` (null; um por item), `mode` (`circulo` ou `caixa`), `size` (`normal` ou `grande`), `card` (true) |
| `riscado` | `items` (1 a 6, até 32 letras), `title` ("Você \*não\* precisa de:"), `mode` (`linha` ou `x`), `delay` (0.2), `stagger` (0.3), `times` (null), `cross_delay` (0.55), `cross_stagger` (0.4), `cross_times` (null; cada risco depois do item aparecer), `size`, `card` |
| `carimbo` | `texto` (até 22 letras, "APROVADO"), `subtexto` (""; linha menor dentro da moldura), `delay` (0.12), `angulo` (-7), `estilo` (`moldura` ou `cheio`), `tremer` (true) |
| `selo_garantia` | `numero` ("7"), `unidade` ("dias"), `titulo` ("GARANTIA"), `legenda` (""; \*palavra\* entre asteriscos na cor de destaque), `delay` (0.15), `fita` (true) |
| `passos` | `items` (2 a 4, até 20 letras), `title` (""), `direction` (`horizontal` ou `vertical`), `icon` (`numero` ou `check`), `delay` (0.1), `stagger` (0.6), `times` (null), `size`, `card` |
| `etiqueta_preco` | `price` (37; centavos com ponto: 37.90), `old_price` (null), `prefix` ("R$"), `decimals` (null), `label` ("por apenas"), `note` (""; até 26), `old_label` ("de"), `mode` (`etiqueta` ou `selo`), `tilt` (-4; -12 a 12), `shine` (true), `tag_delay` (0.62; com preço antigo, segundos até a etiqueta pular) |
| `contador` | `value` (1000), `start_value` (0), `prefix` (""; ex.: "R$" ou "+"), `suffix` (""; ex.: "%", "alunos"), `decimals` (null), `label` (""; até 40), `duration` (1.4), `delay` (0.15), `ease` (`suave` ou `linear`), `size`, `card` (true), `bar` (true) |
| `barra_progresso` | `value` (75; 0 a 100), `start_value` (0), `label` ("Concluído"), `note` (""), `decimals` (null), `mode` (`barra` ou `anel`), `segments` (null; 2 a 10 etapas), `show_value` (true), `duration` (1.4), `delay` (0.15), `ease`, `card` |
| `grafico_barras` | `bars` (2 a 6 de `{"label", "value", "highlight"}`; `value` obrigatório), `title`, `prefix`, `suffix`, `decimals`, `mode` (`colunas` ou `barras`), `highlight` (`maior`, `ultima`, `todas`, `nenhuma`), `show_values` (true), `duration` (0.9), `stagger` (0.18), `delay` (0.15), `card` |
| `grafico_linha` | `values` (2 a 24 números), `labels` ([]; 2 ou um por valor), `title`, `prefix`, `suffix`, `decimals`, `label` (""; ao lado do número), `badge` (""; ex.: "+40%"), `zero` (true), `dots` (true), `area` (true), `duration` (1.6), `delay` (0.2), `card` |
| `palavra_impacto` | `text` ("\*ATENÇÃO\*"; até 4 palavras, `\|` quebra a linha), `style` (`contorno` ou `tarja`), `font` (`condensada` ou `redonda`), `size`, `flash` (`tela`, `local`, `nenhum`), `flash_color` (`branco` ou `destaque`), `shake` (true), `burst` (true), `stagger` (0.24), `upper` (true), `sound` (`metal`, `grave`, `batida`) |
| `comentario_cta` | `keyword` ("IDEIA"; até 20), `prompt_text` ("Comente a palavra"; `{palavra}` vira a palavra-chave), `placeholder`, `user_name` ("você"), `upper` (true), `delay` (0.7; segundos até digitar), `typing_speed` (7), `button` (`icone` ou `texto`), `show_sent` (true), `size` |
| `notificacao` | `app_name` ("Mensagens"; nome genérico), `title` (obrigatório), `text` (até 120), `time_label` ("agora"), `icon` (`mensagem`, `sino`, `check`, `coracao`, `letra`), `exit_mode` (`subir` ou `apagar`), `stack` (false) |
| `destaque` | `target` ([x, y, largura, altura] em pixels da tela 1080x1920, medido num quadro), `mode` (`circulo`, `sublinhado`, `seta`), `side` (`auto`, `baixo`, `cima`, `esquerda`, `direita`), `label` (""; letra de mão), `thickness` (12), `duration` (0.6), `shadow` (true) |

**Exemplo** (a fala: "Você não precisa ter estoque, nem aparecer... por apenas 37 reais... comenta IDEIA"):

```json
"motion": [
  {"template": "riscado", "start": 4.2, "end": 7.9,
   "params": {"items": ["Ter estoque", "Aparecer"], "times": [0.4, 1.3], "cross_times": [0.85, 1.75]}},
  {"template": "etiqueta_preco", "start": 11.0,
   "params": {"price": 37, "old_price": 97, "note": "à vista no PIX", "tag_delay": 0.9}},
  {"template": "passos", "start": 17.5, "end": 20.5, "region": "top",
   "params": {"items": ["Compra", "Conecta", "Usa"], "times": [0.2, 0.8, 1.4]}},
  {"template": "comentario_cta", "start": 23.1, "params": {"keyword": "IDEIA",
   "prompt_text": "Comente *{palavra}* que eu te mando"}, "sfx": true}
]
```

**Como o compose monta**
- Cada animação vira um clipe transparente em `renders/full/motion/` (na prévia, `renders/preview/motion/`,
  em meia resolução), guardado para o próximo render. O clipe entra no quadro exato do `start`.
- Ordem das camadas: vídeo e inserts, animações, gancho, legenda, aviso, chamada e flash.
- Prioridade quando se encostam: chamada > gancho > animação > legenda > aviso. A legenda muda de altura
  enquanto a animação está no lugar dela (com `center_y` em auto) ou some nesse trecho (com número fixo).
  A animação some onde encosta na chamada ou no gancho (sobra de menos de 0,5 s some junto) e o compose
  avisa. Sem lugar livre de tudo, ela ainda fica fora do rosto e some onde encostar na chamada ou no gancho.
  A legenda e o aviso de resultado só desviam dela enquanto ela aparece.
- Os sons do modelo entram pela mesma regra do `sfx.events`: o ponto forte cai no quadro do som,
  com o volume de `sfx.volume` vezes o volume do modelo. O som de digitação acaba junto com a digitação.
- `final_report.json`: `layers` com `"type": "motion"` (`template`, `frames`, `shown`, `region`,
  `moved_by`, `rect`, `ink`, `clip`, `cached`, `params`, `sfx`); os sons em `layers` com `"type": "sfx"`,
  `"why": "animação N"`; em `adjustments`, `"layer": "motion N"` (mudou de lugar ou sumiu) e a legenda com
  `"because": "motion"`; em `face_checks`, `"layer": "motion N"` quando ela ficou no rosto.

### `cta` (chamada)

| Campo | Padrão | O que faz |
|---|---|---|
| `enabled` | true | Liga a chamada. |
| `text` | `"Toque em Saiba Mais"` | Texto do botão. |
| `sub` | null | Linha de apoio embaixo do botão. |
| `start` | null | `null` = últimos `duration` segundos. A chamada fica por cima do fim, não aumenta o vídeo. |
| `duration` | 2.5 | Duração. Entre 2,5 e 4 s. |
| `center_y` | 1110 com legenda, 1180 sem | Centro vertical em pixels. |
| `sfx` | nenhum | Som da chamada, ex.: `"rise"`: a subida começa antes (pelo ponto forte dela, veja `sfx`) e chega no máximo quando a chamada aparece. |

Com `center_y` em auto, a chamada sai de cima do rosto sozinha (ex.: sobe para perto de 430 num
rosto baixo) e a legenda abre espaço para ela.

### `disclaimer` (aviso de resultado)

Opcional. Desligado por padrão: sem a seção no arquivo, o vídeo sai sem aviso. Só escreva a seção se a
pessoa pedir um aviso na tela.

| Campo | Padrão | O que faz |
|---|---|---|
| `enabled` | true | Com a seção escrita, liga o aviso (pílula escura, letra pequena). `false` desliga. |
| `text` | "Resultado individual. Depende de execução. Não é promessa de ganho." | Até 2 linhas. Use o texto que a pessoa pedir. |
| `start` | 0.0 | Entrada. |
| `end` | null | `null` = até o fim. |
| `center_y` | `"auto"` | Centro vertical em pixels. Auto: perto de 1548, fora do rosto e dos outros textos. |

Uma seção `disclaimer` escrita sem `enabled` conta como ligada. Sem `text`, usa o texto padrão. Se o
aviso ficar escondido mais de 20% do tempo (atrás da chamada, do gancho e da legenda), o compose avisa:
deixe `center_y` em auto ou escolha outra altura.

### `sfx` (efeitos sonoros)

| Campo | Padrão | O que faz |
|---|---|---|
| `enabled` | true | Liga os efeitos. Sem `sfx` ligado, nenhum som toca (insert, gancho, flash, chamada ou `events`). |
| `volume` | 0.6 | Volume de todos os efeitos. Com 1.0 um whoosh fica uns 6 dB abaixo da voz. |
| `events` | nenhum | Sons soltos, cada um num segundo escolhido (veja abaixo). |

Efeitos prontos. Todos são CC0 / domínio público ou feitos pelo código do editor, então podem ir
junto com o produto e não precisam de crédito (`THIRD_PARTY_NOTICES.md`, seção 4; o comprovante de
cada um fica em `assets/sfx/licencas/` da skill):

| Nome | Em português | Som | Duração | Ponto forte | Volume com `volume` 1.0 | Bom para |
|---|---|---|---|---|---|---|
| `whoosh` | `swoosh` | Swoosh de ar que sobe e passa | 0,85 s | 0,15 s | -10,0 LUFS | Entrada de imagem (padrão dos inserts), troca de assunto. |
| `pop` | `bolha` | Pop curto e macio de bolha | 0,25 s | 0 s | -14,0 | Entrada do gancho (padrão), texto que aparece. |
| `click` | `clique`, `foto` | Clique seco e brilhante, tipo obturador de câmera | 0,3 s | 0,02 s | -14,0 | Flash (padrão), print, foto na tela. |
| `tick` | `toque` | Toque seco e curto de madeira | 0,07 s | 0 s | -16,7 | Item de lista. |
| `rise` | `subida` | Subida que cresce, chega no máximo e segue | 2,6 s | 1,23 s | -20,0 (3 s) | Chamada (`cta.sfx`). |
| `cash` | `caixa`, `dinheiro`, `venda` | Caixa registradora: gaveta e sino ("ching") logo no começo | 1,4 s | 0,07 s | -9,1 | Fala de venda, pix, preço ou oferta. |
| `coin` | `moeda` | Moeda caindo num copo e tilintando | 0,7 s | 0,02 s | -12,0 | Desconto, bônus, "de graça". |
| `notification` | `notificação`, `ding` | Ding curto e brilhante de vidro, tipo notificação | 1,2 s | 0,01 s | -14,0 | Venda que caiu, mensagem, pix recebido. |
| `success` | `sucesso` | Três notas subindo e o acorde soando | 0,93 s | 0,02 s | -14,0 | Deu certo, meta batida, resultado alcançado. |
| `error` | `erro` | Buzina de "errado": nota curta e outra mais grave | 0,66 s | 0,19 s | -14,0 | "O erro que...", o que não fazer. |
| `count` | `contagem` | Tique-tique agudo e rápido de contador | 0,64 s | 0 s | -14,0 | Número subindo na tela, valores, placar. |
| `keyboard` | `teclado`, `digitando` | Rajada de digitação | 1,5 s | 0,11 s | -14,0 | Alguém escrevendo, mensagem, pesquisa. |
| `snap` | `estalo de dedo`, `estalo` | Estalo de dedo com cauda curta de sala | 0,97 s | 0 s | -14,0 | "Simples assim", algo que aparece do nada. |
| `mouse_click` | `clique de mouse`, `mouse` | Clique de mouse: aperta e solta | 0,24 s | 0 s | -16,9 | "É só clicar", botão na tela, compra no site. |
| `bass_drop` | `grave`, `boom` | Grave forte que cai | 1,05 s | 0,02 s | -10,0 | Revelação grande, número importante. |
| `metal_hit` | `impacto metálico`, `metal` | Pancada de bigorna sobre um grave | 1,45 s | 0,16 s | -20,0 (3 s) | Frase de impacto, título forte. |
| `synth_hit` | `batida sintética`, `808` | Zap que cai num grave 808 | 1,5 s | 0,34 s | -20,0 (3 s) | Entrada de título, virada, corte seco. |
| `flash` | `clarão`, `brilho` | Ar subindo, estouro brilhante e baque grave | 1,4 s | 0,28 s | -10,0 | Troca de cena, antes e depois. |
| `charge` | `carga`, `energia` | Estalo elétrico e apito que sobe e some | 2,4 s | 0 s | -20,0 (3 s) | "Prepare-se", logo antes de mostrar um resultado. |
| `metal_riser` | `subida metálica`, `tensão` | Prato de trás para frente que cresce e para seco | 1,9 s | 1,65 s | -20,0 (3 s) | Suspense antes de uma revelação. |
| `wind` | `vento` | Vento grave e suave que passa | 1,3 s | 0,36 s | -12,0 | Passagem de tempo, troca de lugar, respiro. |
| `rewind` | `rebobinar`, `voltar no tempo` | Fita que acelera, sobe de tom e para | 1,5 s | 1,15 s | -20,0 (3 s) | "Voltando no tempo", "lá atrás". |
| `gears` | `engrenagens`, `mecanismo` | Catraca girando e um baque grave no meio | 1,18 s | 0,36 s | -14,0 | Método, processo, sistema funcionando. |
| `data` | `dados`, `processando` | Bipes agudos e rápidos de computador | 2,0 s | 0,03 s | -20,0 (3 s) | IA, dados, sistema trabalhando. |

- **Escolha pelo sentido da fala**, não por enfeite: `notification` quando falam de venda ou
  mensagem, `error` em "o erro que...", `count` quando aparece um número, `bass_drop` na revelação
  grande, `rewind` em "lá atrás". No `name` vale o nome em inglês ou o nome em português da tabela
  (com ou sem acento, com espaço ou `_`, maiúscula ou minúscula: `"grave"`, `"estalo de dedo"`,
  `"notificacao"`). Os outros apelidos de cada um estão em `assets/sfx/sfx.json` (`label` e
  `aliases`).
- **Ponto forte** é o momento em que o som chega na força total: o ching da caixa, o primeiro toque
  da moeda, o meio do swoosh, o topo da subida. O compose põe o ponto forte no quadro do evento
  (entrada do gancho, entrada da imagem, flash, entrada da chamada, `at` de `sfx.events`), então o
  som começa um pouco antes. Ex.: a `rise` na chamada começa 1,23 s antes e chega no máximo quando a
  chamada aparece.
- Se o evento está tão perto do começo do vídeo que o som teria que começar antes dele, o som começa
  no 0 e o ponto forte chega atrasado. Quando isso passa de um quadro sai um `aviso:` (veja
  "Problemas comuns").
- **Volume** é medido na janela de 200 ms mais forte de cada som, numa voz em -14 LUFS: marcas curtas
  em -14, o `wind` em -12, impactos em -10. Nenhum som passa de -20 LUFS na janela de 3 s (6 dB abaixo
  da voz), então os sons longos, marcados "(3 s)", são acertados por essa janela: na de 200 ms eles
  ficam entre -16 e -11 (`rise` -13,8, `metal_hit` -10,8, `synth_hit` -11,8, `charge` -11,1,
  `metal_riser` -11,9, `rewind` -12,5, `data` -16,0). O `mouse_click` para no teto de pico antes de
  chegar em -14. Com o padrão 0.6 tudo fica 4,4 dB mais baixo. Os números de cada arquivo estão em
  `assets/sfx/sfx.json` da skill, e `helpers/make_sfx.py --measure` mede de novo.
- `count` e `data` são bem agudos (perto de 12 kHz): soam mais baixos do que o número diz e somem em
  caixinha de celular ruim. Se ficarem fracos, use `"volume": 1.5` no item. Os graves (`bass_drop`,
  quase todo o `wind` e o fundo de `metal_hit`, `synth_hit` e `flash`) quase não aparecem em caixinha
  de celular (no fone de ouvido, sim).
- **Arquivo próprio** (`"assets/meu_som.wav"`; wav, mp3, m4a, aac, ogg ou flac) toca como antes: o
  começo do arquivo cai no quadro do evento, sem ajuste de volume.
- Os efeitos feitos por código ficam em `assets/sfx/sinteticos/` da skill. Nove da tabela já são de
  lá (`tick`, `error`, `bass_drop`, `synth_hit`, `flash`, `charge`, `wind`, `rewind`, `data`). Os
  outros seis de lá (versões por código de `whoosh`, `pop`, `click`, `rise`, `cash` e `coin`) servem
  como arquivo: copie para `<EDIT>/assets/` e use o caminho (ex.: `"assets/cash.wav"`).

#### `sfx.events` (som num segundo escolhido)

Para um som que não está preso a gancho, insert, flash ou chamada:

```json
"sfx": {"enabled": true, "volume": 0.6, "events": [
  {"at": 5.07, "name": "cash"},
  {"at": 12.4, "name": "coin", "volume": 0.8}
]}
```

| Campo | Padrão | O que faz |
|---|---|---|
| `at` | obrigatório | Segundo no corte (número com ponto, 0 ou mais). O ponto forte do som cai no quadro mais perto desse segundo. |
| `name` | obrigatório | Efeito pronto ou arquivo, igual ao `sfx` dos inserts. |
| `volume` | 1.0 | Multiplica o `sfx.volume` só neste som (de 0.0 a 2.0; acima de 2.0 fica em 2.0, com aviso). |
| `anchor_s` | o do efeito | Em que segundo do som fica o ponto forte. Efeito pronto já tem o seu (tabela acima); arquivo próprio usa 0 (o começo do arquivo cai no `at`). Com `anchor_s`, o som começa esse tanto antes do `at`. `0` num efeito pronto faz ele começar no `at`. |

- Para cair em cima da palavra, use o `start` dela no `captions.json`. No `cash` o ponto forte é o
  sino (ching): é ele que cai nesse quadro.
- Arquivo próprio com o ponto forte no meio (ex.: um swoosh seu com o pico em 0,3 s):
  `{"at": 9.2, "name": "assets/meu_swoosh.wav", "anchor_s": 0.3}`.
- Som que os 24 prontos não têm (virar página, porta, chuva): dá para gerar um com
  `helpers/gen_sfx.py` (opcional e pago, na conta da pessoa; veja "Trilha" mais abaixo). Ele sai em
  `assets/sfx_<nome>.mp3` e entra pelo caminho: `{"at": 7.3, "name": "assets/sfx_papel.mp3"}`.
- No máximo 2 ou 3 por vídeo, nos momentos mais fortes. Som em toda frase cansa.
- Sons com ponto forte no fim (`rise`, `metal_riser`, `rewind`) começam bem antes do `at`: deixe pelo
  menos 1,7 s de vídeo antes do `at`.
- O `cash` soa um pouco mais forte que o `whoosh` (veja os volumes acima). Se ele brigar com a fala,
  use `"volume": 0.7` nele.
- `at` no fim do vídeo ou depois é ignorado com aviso. Som que começa perto do fim toca cortado; se
  perder mais de 10% do som, sai o aviso `é cortado pelo fim do vídeo`. Deixe pelo menos 1 s depois do
  `at` para o `cash` e a `rise`.
- Cada som que tocou aparece em `verify/final_report.json`, em `layers`, com `"type": "sfx"` e
  `"why": "efeito"`. `sample` é onde o som começa; `anchor_s` mostra o ponto forte usado e `late_s`
  aparece quando o som teve que começar no 0.

#### Biblioteca de efeitos (`helpers/efeitos.py`)

Fica fora da skill (`~/.editor-video/efeitos`, ou a pasta em `EDITOR_VIDEO_EFEITOS`), com cada som cortado,
no nível do kit e com a licença anotada em `efeitos.json`. No `name` de `sfx.events` (e no `sfx` de insert):

| Nome | O que toca |
|---|---|
| `transicao`, `impacto`, `revelacao`, `interface`, `tecnologia`, `sucesso`, `erro`, `humor` | Uma versão da categoria por evento, numa ordem fixa por edição (a mesma categoria 5 vezes = 5 sons diferentes). |
| `categoria:tipo` (ex.: `impacto:boom`) | Só as versões cujo nome de origem tem o tipo: `boom`, `punch`, `metal`, `glass`, `plate`, `explosion` (impacto); `swish`, `swipe`, `whoosh`, `door`, `fireball` (transição); `riser`, `swell`, `magic` (revelação); `boing`, `cartoon`, `karate`, `stretching`, `boom808`, `whistle`, `scratch` (humor); `laser`, `glitch`, `zap`, `8-bit`, `forcefield` (tecnologia); `powerup`, `confirmation` (sucesso). |
| `humor-077` | Aquele som exato. |

Uma categoria ganha de apelido de efeito pronto com o mesmo nome (`impacto` é da biblioteca, não o
`metal_hit`). Comandos: `efeitos.py listar [--categoria C]`, `amostra --categoria C` (mp3 com todos e a lista
de tempos em `_amostras/`), `remover ID`, `restaurar ID`, `importar PASTA --origem kenney|sse|sintetico|arquivos`
e `efeitos_gerar.py PASTA` (whoosh, swipe, riser, swell, boom, boom808 e glitch feitos por código).

### `music` (trilha)

| Campo | Padrão | O que faz |
|---|---|---|
| `enabled` | true | Liga a trilha. |
| `file` | `file` ou `mood` | Ex.: `"assets/trilha_1.mp3"` (arquivo da pessoa ou feito com `gen_music.py`). Repete se for curta. Se vierem `file` e `mood`, vale o `file`. |
| `mood` | `file` ou `mood` | Um humor da biblioteca de trilhas da pessoa (`"triste"`, `"animada"`, `"Épica"`...). O compose escolhe a música (abaixo). |
| `track` | nenhum | Só com `mood`: o id de uma trilha certa da biblioteca (ex.: `"triste-2"`). |
| `volume_db` | -15 | Relativo à voz medida. Com fala o tempo todo, -15 fica uns 19 dB abaixo da voz (dá para ouvir sem brigar). -12 fica mais presente, -18 mais baixa, -22 quase some. |
| `duck` | true | Abaixa a trilha enquanto a pessoa fala (cerca de 4 dB a mais com fala contínua). |
| `fade_out` | 1.5 | Segundos de saída suave no fim. |

**Como o `mood` escolhe a música.** A biblioteca fica fora da skill (`~/.editor-video/trilhas` no Mac,
`%USERPROFILE%\.editor-video\trilhas` no Windows), com uma pasta por humor e o índice `trilhas.json`.
- Vale a favorita do humor. Sem favorita, vale a que cobre o corte sem repetir, tem menos energia na
  faixa da voz (300 a 3400 Hz) e já começa cheia.
- A música escolhida é copiada para `<EDIT>/assets/trilha_<humor>.<ext>` (nunca por cima de um arquivo
  da pessoa com o mesmo nome) e a escolha fica em `assets/trilhas_escolhidas.json`. As próximas
  montagens desta edição usam a mesma música, mesmo que a biblioteca mude ou suma. Para trocar:
  `trilhas.py escolher --humor triste --edit-dir '<EDIT>' --trocar` (ou `"track"`).
- O `edit_data.json` não é reescrito. O `verify/final_report.json` mostra em `music` o `file` usado,
  `mood`, `track_id`, `title`, `why` (por que ela), `chosen_from` (`biblioteca` ou `escolha anterior`),
  `source` e `license`. O compose imprime `trilha do humor triste: triste-2 "Piano" em assets/...`.
- Sem `mood`, a trilha sai exatamente como com um `file` comum.

Os números do volume: medidos em 16/09/2026 em dois cortes reais com fala contínua, com uma trilha
pop e uma de piano. O duck tira cerca de 4 dB durante a fala, então `volume_db` -22 deixava a trilha
uns 26 dB abaixo da voz (quase inaudível) e -15 deixa uns 19 dB abaixo. O -15 foi aprovado de ouvido
num anúncio real.

### `audio` (limpeza de ruído da voz)

| Campo | Padrão | O que faz |
|---|---|---|
| `denoise` | `"off"` | Tira o ruído de fundo da voz: `"off"` (desligado), `"leve"` ou `"forte"`. |

```json
"audio": {"denoise": "leve"}
```

- **Leve:** tira chiado, zumbido e ruído de ambiente sem mexer na voz. Em fala já limpa quase não
  muda nada. Comece por ele.
- **Forte:** para barulho alto e constante (rua, ventilador, ar-condicionado). Tira mais, mas pode
  deixar um leve "chiado de água" no fundo. Ouça o final antes de entregar.
- Não resolve eco de sala, cachorro, pássaro ou vozes ao fundo. Vento forte some só em parte.
- **Quando usar:** o `noise_check.py` da Fase 1 diz `ruído leve` (ofereça Leve) ou `ruído forte`
  (ofereça Forte). Com `limpo, não precisa`, deixe desligado, a não ser que a pessoa peça.
- **Como funciona:** o compose aprende o ruído de cada vídeo bruto nas pausas dele
  (`regions/<nome>.json`), limpa cada trecho do corte com o ruído do vídeo de onde ele veio e põe a
  voz limpa exatamente nas mesmas amostras do `cut.mp4`. Duração, sincronia e volume final (-14 LUFS)
  não mudam. O `cut.mp4` e a aprovação dele também não mudam: dá para ligar ou trocar a limpeza sem
  aprovar o corte de novo.
- **Pouca pausa:** vídeo com menos de 0,3 s de pausa usa as partes mais quietas para medir o ruído e
  o Forte vira Leve, com um aviso. Vídeo sem nenhuma parte quieta fica sem limpeza (aviso).
- **Sem os vídeos brutos** (pasta movida, arquivo apagado) ou corte mudado depois do timeline: o
  compose limpa o próprio som do corte, sempre em Leve, com um aviso. Funciona, mas limpa menos.
- O resultado fica em `verify/final_report.json`, na chave `denoise` (`noise_floor_before_db` e
  `noise_floor_after_db`: o nível dos momentos mais quietos, antes e depois), e numa linha do compose:
  `ruído de fundo: forte (fundo de -50 dB para -60 dB)`. Em fala sem pausa esse número muda pouco
  (os momentos mais quietos ainda têm voz), mesmo com o chiado bem mais baixo.

### `loudness`

`{"i": -14, "tp": -1.0}` (padrão). Não mude para Reels e TikTok.

## Da tela de estilo para o `edit_data.json`

A linha `[preview] estilo salvo` (ou o `preview_style.json`) traz as escolhas. Assim cada uma
vira campo:

| Escolha | Campo |
|---|---|
| `edit_type: clean` | Sem `split_top`/`split_bottom`. `full` ou `pip` só se a pessoa pedir. |
| `edit_type: split_top` | Inserts `split_top` nos trechos de PROVA, VIRADA ou PASSO. |
| `edit_type: split_bottom` | Inserts `split_bottom` nesses trechos. |
| `edit_type: tela_verde` (linha `tela_verde (canto direito: tela_verde.posicao "direita"; ...)`) | `"edit_type": "tela_verde"` e `tela_verde.posicao` com o valor da linha (`tela_verde_posicao` no `preview_style.json`). Mantenha os inserts já planejados: `split_top`/`split_bottom` vão para o fundo sozinhos. Faltando imagens, planeje inserts `split_top` como na tela dividida e pergunte de onde vêm (seção `tela_verde`). Sem `split_*` junto de outro tipo. |
| `caption_style` | `captions.style` (e `captions.py --style` com o mesmo id). |
| `hook_style`, `hook_text` | `hook.style`, `hook.text`. Texto vazio: escreva um e confirme. |
| `accent` | `accent`. |
| `cta_text` | `cta.text`. |
| `elements.zoom_cuts` | `camera.zoom_cuts`. |
| `elements.push_in` | `camera.push_in`. |
| `elements.flash` | Itens em `transitions` na entrada dos inserts `full` e das trocas mais fortes (no máximo 3). |
| `elements.motion` (linha `animações: …`) | `ligadas, poucas (motion.quantidade 3), visual papelaria (motion.tema "papelaria")`: escreva `"motion": {"tema": ..., "quantidade": ..., "items": [...]}` com os números da linha, rode o `motion_sugerir.py` e ponha em `items` o que a pessoa aprovar (seção `motion`). `desligadas`: sem `motion` (ou `"motion": {"enabled": false, "items": [...]}` para guardar os itens). |
| `elements.sfx` | `sfx.enabled`. Em `sfx.events`, som pelo sentido da fala: venda ou pix `cash` ou `notification`, erro `error`, número `count`, revelação `bass_drop` (tabela de efeitos). |
| `music_mood` (linha `trilha: …`) | Um humor (`trilha: triste`): `"music": {"enabled": true, "mood": "triste"}`. `arquivo` (`trilha: arquivo da pasta (assets/x.mp3)`): `"file"` com o `music_file`. `off` (`trilha: sem`): `"music": {"enabled": false}`. |
| `elements.music` | Só quando não veio `music_mood`: `music.enabled`. Pergunte de onde vem a trilha: da biblioteca (humor), arquivo dela, gerada na conta dela ou nenhuma (seção "Trilha"). |
| `elements.cta` | `cta.enabled`. |
| `elements.disclaimer` | `disclaimer.enabled`. |
| `denoise` (linha `ruído: …`) | `audio.denoise` com o mesmo valor (`off`, `leve` ou `forte`). Sem `denoise` no arquivo, siga o que foi combinado no chat. |
| `note` | Leia e aplique. Na dúvida, pergunte. |

## Estilos de legenda (`captions.style`)

| Id | Visual | Quando usar |
|---|---|---|
| `impacto` | Letra alta e condensada com contorno preto; a palavra falada acende na cor de destaque; entra com um pulinho. | Padrão. Anúncio direto, qualquer fundo. |
| `caixa_viva` | Letra reta e forte; uma caixa colorida corre de palavra em palavra. | Tutorial, fala rápida, fundo claro. |
| `palavra_gigante` | Uma palavra por vez, enorme, com sombra 3D colorida. | Vídeo curto e acelerado, gancho com energia. Cansa em vídeo longo. |
| `pulso` | Letra redonda e grossa; a palavra falada cresce e ganha cor. | UGC e depoimento, tom leve. |
| `fita` | Frase numa fita preta inclinada que abre da esquerda. | Fundo muito poluído ou colorido. |
| `legenda_limpa` | Legenda de TV em até 2 linhas sobre fundo escuro, sem animação. | Conteúdo sério, aula, público mais velho. |

## Estilos de gancho (`hook.style`)

| Id | Visual | Quando usar |
|---|---|---|
| `manchete` | Título grande e condensado com contorno; segunda linha na cor de destaque. | Padrão. Combina com `impacto`. |
| `tarja` | Cada linha numa tarja sólida (branca e colorida), levemente inclinada. | Chama muita atenção, bom para oferta. |
| `balao` | Balão branco de conversa com texto escuro. | Pergunta que o público faria. |
| `marcador` | Letra de pincel sobre marca-texto colorido. | Tom pessoal, feito à mão. |
| `vidro` | Painel escuro translúcido com detalhe colorido. | Visual limpo e premium. |

Para testar um texto antes do render:
`uv run --directory "<SKILL>" python helpers/text_render.py hook "Seu *gancho* aqui" --style manchete --bg "<EDIT>/verify/quadro.png" -o "<EDIT>/verify/gancho.png"`
(o fundo `--bg` é opcional; o comando avisa quando o texto está longo demais).

## Quando colocar inserts

- **Onde:** nos trechos em que a fala cita algo que dá para mostrar ("essa bolsa", "o painel",
  "o site"). Veja os `beat` e `quote` em `cut_timeline.json` e, para o tempo exato de uma
  palavra, o `start` das legendas em `captions.json`.
- **Encaixe no tempo:** comece e termine num corte (`out_start`/`out_end` do `cut_timeline.json`)
  ou no início de uma legenda. Um `full` que começa pouco antes de um corte e termina depois
  esconde o pulo desse corte.
- **Duração:** de 2,5 a 6 s por imagem. `full` até 2,5 s. Vídeo até o tamanho do clipe.
- **Não cubra:** o gancho (comece depois de `hook.end`) nem a chamada (termine antes dos últimos
  `cta.duration` segundos).
- **Respiro:** pelo menos 1 s entre dois inserts. A pessoa deve aparecer inteira na maior parte do
  vídeo; em anúncio, o rosto vende.
- **Rosto livre:** confira na folha de quadros. `pip` no canto oposto ao rosto. Se a tela dividida
  cortar a cabeça, troque `split_top` por `split_bottom`.
- **Tela verde:** os mesmos trechos e a mesma lista; as imagens entram no fundo, atrás da pessoa.
- **Imagem boa:** sem marca d'água, texto, logo de terceiro ou pessoa famosa. Horizontal para split
  e pip, vertical para `full`.
- **Cobrir um trecho da tela:** um `full` com imagem neutra por cima esconde a imagem do trecho (a voz
  continua). Só faça com o ok da pessoa.
- **Dado pessoal** (nome, telefone, e-mail, documento de outra pessoa) não passa sem borrão: borre a
  imagem antes de usar como insert; se estiver no próprio vídeo, cubra o trecho com um `full` ou corte.

## Flash

- Use no começo de um `full` ou numa virada forte de assunto. No máximo 2 ou 3 por vídeo.
- Não ponha flash em todo corte: o zoom nos cortes já marca o ritmo.
- Mesmo tempo do insert: `"at"` igual ao `start` dele.

## Trilha

- Só com arquivo que a pessoa tem direito de usar: dela, com licença comercial, ou gerado na conta
  ElevenLabs dela. Arquivo dela: copie para `<EDIT>/assets/`. Música da biblioteca do próprio
  Gerenciador de Anúncios é outro caminho: nesse caso deixe `music` desligado aqui.
- Sem letra por baixo da fala. Animada para UGC e oferta, calma para depoimento e aula.
- Comece com `volume_db` -15 e `duck` ligado. Se a pessoa pedir mais presença, -12; mais baixa, -18.

**Biblioteca de trilhas por humor** (`helpers/trilhas.py`). A pessoa guarda músicas prontas por
clima e, na edição, só diz "trilha triste" ou escolhe o humor na aba Estilo:

| Humor | Nome | Quando usar no anúncio |
|---|---|---|
| `animada` | Animada | energia, oferta, novidade |
| `inspiradora` | Inspiradora | conquista, virada de vida, motivação |
| `triste` | Triste | dor, problema, desabafo |
| `tensa` | Tensa | segredo, alerta, erro que as pessoas cometem |
| `calma` | Calma | explicação, tutorial, confiança |
| `epica` | Épica | revelação, resultado, grande virada |
| `divertida` | Divertida | humor, bastidor, conteúdo leve |
| `luxo` | Luxo | produto premium, sofisticação |

Pelo trecho mais forte: PROBLEMA triste ou tensa, VIRADA inspiradora ou épica, PROVA e RESULTADO
épica ou calma, PASSO calma, CHAMADA animada ou inspiradora.

```
uv run --directory '<SKILL>' python helpers/trilhas.py humores
uv run --directory '<SKILL>' python helpers/trilhas.py listar
uv run --directory '<SKILL>' python helpers/trilhas.py adicionar '/caminho/musica.mp3' --humor animada --titulo 'Pop da oferta' --favorita
uv run --directory '<SKILL>' python helpers/trilhas.py escolher --humor animada --edit-dir '<EDIT>'
```

- `adicionar` copia o arquivo (mp3, m4a, aac, ogg e opus vão como estão; wav, flac, aiff e o som de
  um vídeo viram WAV 48 kHz estéreo) e anota duração, volume (LUFS), energia ("cheia desde o
  início" ou "começa baixa e cresce aos N s"), a parte da energia na faixa da voz, origem, regra de
  uso, pedido (se veio de IA), data e favorita. `--fonte` e `--licenca` dizem de onde veio e como pode
  usar. Humor com outro nome cria um humor da pessoa.
- `escolher` faz o mesmo que o compose com `mood` e imprime qual entrou e por quê. `--trocar` pega
  outra, `--trilha ID` pega uma certa, `--duracao S` troca a duração do corte.
- `favorita ID` (`--tirar` desmarca), `remover ID` (vai para `_lixeira`, nada é apagado),
  `restaurar ID`, `onde`. Uma música que está na lixeira não entra de novo: a mensagem traz o
  `restaurar`.
- Uma trilha gerada (em `assets/` com `credits.json`, ou esperando em `_recebidas`) entra com a
  origem, o pedido e a regra de uso anotados, sem precisar de `--fonte` nem `--licenca`.

**Gerar com `gen_music.py`** (opcional e pago, com a chave e o plano da própria pessoa; para anúncio,
só com plano pago da ElevenLabs: pessoa física do Starter para cima, empresa no Scale, Business ou
Enterprise; vale para internet, não para TV, rádio ou cinema; o arquivo não pode ser repassado nem
vendido):

```
uv run --directory '<SKILL>' python helpers/gen_music.py 'upbeat modern pop instrumental, 110-120 bpm, positive and confident, light drums, no vocals, leaves room for voice' --edit-dir '<EDIT>' --dry-run
```

- Pedido em inglês: instrumental, gênero, clima, faixa de bpm, `no vocals`, `leaves room for voice`.
  Para depoimento ou aula: `calm warm acoustic instrumental, 80-90 bpm, soft piano and guitar,
  hopeful, no vocals, leaves room for voice`.
- Nada de artista, banda, compositor, música, álbum, gravadora, filme, marca ou trecho de letra no
  pedido: o helper recusa os nomes mais conhecidos antes de gastar e a ElevenLabs recusa o resto.
- O `--dry-run` mostra a duração (a do corte arredondada para cima + 2 s), o arquivo e o custo em
  créditos. Conte o custo, peça o ok e rode de novo sem `--dry-run`. `--variations 2` faz duas
  versões para a pessoa escolher (cada uma é cobrada). A trilha sai instrumental por padrão.
- O arquivo fica em `assets/trilha_1.mp3` (a próxima vira `trilha_2.mp3`), com a origem anotada em
  `assets/credits.json`. Com 2 s a mais que o corte, a trilha nunca recomeça antes do fim: o compose
  corta a sobra e faz o `fade_out`.
- Use igual a um arquivo da pessoa:
  `"music": {"enabled": true, "file": "assets/trilha_1.mp3", "volume_db": -15, "duck": true, "fade_out": 1.5}`
- **Por humor:** `gen_music.py --humor triste --edit-dir '<EDIT>' --dry-run` usa o pedido pronto do humor
  (palavras a mais vão antes das opções). `--biblioteca` guarda cada versão na biblioteca, no humor,
  com a origem e o pedido anotados. Sem `--edit-dir`, a trilha tem 60 s e vai só para a biblioteca;
  com `--edit-dir`, tem pelo menos 60 s e também fica em `assets/`. A trilha gerada é só da pessoa:
  não repasse nem junte em pacote.

## Exemplos por tipo

**Limpa** (só a pessoa, legenda, gancho, zoom e chamada):

```json
{
  "version": 1, "base": "cut.mp4", "timeline": "cut_timeline.json", "accent": "#FFC400",
  "camera": {"zoom_cuts": true, "zoom_levels": [1.0, 1.12]},
  "hook": {"enabled": true, "style": "manchete", "text": "O erro que *trava suas vendas*", "start": 0, "end": 2.6},
  "captions": {"enabled": true, "style": "impacto", "file": "captions.json", "hide_during_hook": true},
  "cta": {"enabled": true, "text": "Toque em Saiba Mais", "sub": "Aula gratuita", "start": null, "duration": 3},
  "sfx": {"enabled": true, "volume": 0.6}
}
```

**Tela dividida com imagem em cima** (provas com foto, flash no `full`):

```json
{
  "version": 1, "base": "cut.mp4", "timeline": "cut_timeline.json", "accent": "#FF3B30",
  "camera": {"zoom_cuts": true, "zoom_levels": [1.0, 1.14], "push_in": true, "push_in_amount": 0.04},
  "hook": {"enabled": true, "style": "tarja", "text": "Olha o que eu *achei pra revender*", "start": 0, "end": 2.8},
  "captions": {"enabled": true, "style": "caixa_viva", "file": "captions.json", "hide_during_hook": true},
  "inserts": [
    {"type": "split_top", "src": "assets/bolsa1.jpg", "start": 4.2, "end": 8.9},
    {"type": "split_top", "src": "assets/bolsa2.jpg", "start": 12.1, "end": 16.0},
    {"type": "full", "src": "assets/vitrine.jpg", "start": 19.4, "end": 21.4}
  ],
  "transitions": [{"at": 19.4, "type": "flash"}],
  "cta": {"enabled": true, "text": "Clique no botão abaixo", "sub": "Acesso na hora", "start": null, "duration": 3},
  "sfx": {"enabled": true, "volume": 0.6}
}
```

**Tela verde** (comentando uma página, pessoa no canto direito, legenda acima dela; as imagens são inserts
`split_top` e vão para o fundo, sem tela dividida):

```json
{
  "version": 1, "base": "cut.mp4", "timeline": "cut_timeline.json", "accent": "#FFC400",
  "edit_type": "tela_verde",
  "tela_verde": {"posicao": "direita"},
  "inserts": [
    {"type": "split_top", "src": "assets/print_pagina.png", "start": 2.6, "end": 12.0},
    {"type": "split_top", "src": "assets/produto.mp4", "start": 12.0, "end": 18.5}
  ],
  "camera": {"zoom_cuts": true, "zoom_levels": [1.0, 1.12]},
  "hook": {"enabled": true, "style": "manchete", "text": "Olha essa *página*", "start": 0, "end": 2.6},
  "captions": {"enabled": true, "style": "impacto", "file": "captions.json", "hide_during_hook": true},
  "cta": {"enabled": true, "text": "Toque em Saiba Mais", "start": null, "duration": 3},
  "sfx": {"enabled": true, "volume": 0.6}
}
```

**Imagem embaixo, cartão e trilha** (tutorial com print no cartão):

```json
{
  "version": 1, "base": "cut.mp4", "timeline": "cut_timeline.json", "accent": "#00C2FF",
  "camera": {"zoom_cuts": true, "zoom_levels": [1.0, 1.1]},
  "hook": {"enabled": true, "style": "vidro", "text": "Como eu organizo *meus pedidos*", "start": 0, "end": 3.0},
  "captions": {"enabled": true, "style": "pulso", "file": "captions.json", "hide_during_hook": true},
  "inserts": [
    {"type": "split_bottom", "src": "assets/planilha.png", "start": 6.0, "end": 11.5, "split": 0.5},
    {"type": "pip", "src": "assets/tela_app.mp4", "start": 14.0, "end": 18.0, "corner": "top_left"}
  ],
  "cta": {"enabled": true, "text": "Veja como funciona", "start": null, "duration": 2.5, "sfx": "rise"},
  "sfx": {"enabled": true, "volume": 0.5},
  "music": {"enabled": true, "mood": "calma", "volume_db": -15, "duck": true, "fade_out": 1.5}
}
```

## Conferir antes de entregar

1. `compose_final.py ... --preview` e uma folha de quadros com `contact_sheet.py --times` em:
   0.3, o meio do gancho, a entrada e o meio de cada insert, um trecho só com legenda e o último
   segundo. Olhe a imagem.
2. Leia as linhas `aviso:` do compose.
3. Na folha de quadros, confira também:
   - texto que já estava no vídeo não aparece junto com a legenda ou o gancho novos;
   - nenhum dado pessoal sem borrar (nome, telefone, e-mail, documento) e nada na tela que a pessoa não
     aprovou;
   - cada animação no meio dela: texto certo, legível, fora do rosto, com o número igual ao da fala.
4. Depois do final, abra `<EDIT>/verify/final_report.json`: `duration` igual à do `cut.mp4`,
   `loudness.i` perto de -14, `loudness.tp` até -1, `warnings` vazio ou explicado, `face_checks`
   vazio, `layers` com o que você pediu nos tempos certos. Com limpeza de ruído, `denoise.applied`
   igual ao pedido (ou o aviso que explica) e o fundo mais baixo em `noise_floor_after_db`.
   `adjustments` lista cada texto que mudou de lugar ou sumiu por um tempo para não encostar em outro.

## Problemas comuns

| Mensagem | O que fazer |
|---|---|
| `estilo de gancho 'x' não existe` | Use um id da tabela de ganchos. |
| `estilo de legenda 'x' não existe` | Use um id da tabela de legendas. |
| `inserts[N].src: arquivo não encontrado` | Caminho relativo a `<EDIT>`. Confira o nome em `assets/`. |
| `inserts[N].type inválido` | Só `split_top`, `split_bottom`, `full`, `pip`. |
| `edit_type "x" não existe` | Use `clean`, `split_top`, `split_bottom` ou `tela_verde`. |
| `tela_verde.posicao precisa ser ...` | `"direita"`, `"esquerda"` ou `"centro"`. |
| `tela_verde.fundo[N].src: arquivo não encontrado` | Caminho relativo a `<EDIT>`. Confira o nome em `assets/`. |
| `tela_verde.fundo[N].x não existe` | Campos do item: `src`, `start`, `end`, `ken_burns`, `escurecer`, `desfocar`, `encaixe`. |
| `tela_verde.fundo[N].encaixe precisa ser ...` ou `inserts[N].encaixe precisa ser ...` | `"auto"`, `"cobrir"` ou `"inteiro"`. |
| aviso `inserts[N].encaixe só vale para imagem em cima ou embaixo na tela verde` | `encaixe`, `escurecer` e `desfocar` só mudam os inserts que viram fundo. Tire do `full` ou do `pip`. |
| aviso `captions.center_y N cairia no rosto na tela verde` | A pessoa mudou de lugar. A legenda já foi para a altura livre mais perto; tire o `center_y` para ela se ajustar sozinha. |
| aviso `o cartão do insert N (pip) foi para o canto ...` | O canto pedido cairia no rosto. Nada a fazer, ou mude o tempo do `pip`. |
| Tela dividida aparecendo na tela verde | Não acontece mais: com `"edit_type": "tela_verde"` os `split_top`/`split_bottom` viram fundo. Confira o `edit_type`. |
| `a tela verde precisa da biblioteca onnxruntime` ou `o modelo do recorte da pessoa não foi encontrado` | A instalação está incompleta: rode o instalador de novo. |
| aviso `tela_verde sem fundo: usei um fundo escuro` | Planeje as imagens e vídeos como inserts `split_top` (vão para o fundo), ou use `tela_verde.fundo` ou `"fundo_original_desfocado": true`. |
| aviso `a seção tela_verde ficou de fora` | Falta `"edit_type": "tela_verde"`. |
| aviso `tela verde, trecho ...: não achei a pessoa neste trecho` | O recorte não achou ninguém ali: só o fundo aparece. Use `"sem_rosto": "original"` (padrão) ou corte o trecho. |
| aviso `recorte feito a partir do cut.mp4 em vez do vídeo bruto` | O vídeo bruto sumiu ou o corte é de uma versão antiga. O recorte funciona igual, com um pouco menos de detalhe. |
| Pedaço do cenário grudado na pessoa ou mão cortada | Recorte difícil nesse trecho (fundo da mesma cor da roupa, mão muito rápida). Troque o fundo por uma cor parecida com a gravação ou use outro tipo de edição. |
| `music está ligado mas sem 'file' nem 'mood'` | Coloque `mood` (um humor) ou `file`, ou desligue a trilha. |
| `music.mood: a biblioteca de trilhas ainda não existe` | A pessoa ainda não guardou música. Use `trilhas.py adicionar` (arquivo dela) ou `gen_music.py --humor X --biblioteca` (pago), ou um `file`. |
| `music.mood: o humor 'x' ainda não tem trilha` | Encha esse humor (a mensagem traz os comandos) ou escolha outro com `trilhas.py humores`. |
| `music.mood: o humor 'x' não existe` | Use um humor da lista (`trilhas.py humores`). |
| `music.mood: não achei a trilha 'x'` | O `track` não está na biblioteca. Veja os ids com `trilhas.py listar`. |
| `cor de destaque inválida` | Use `#RRGGBB`. |
| aviso `gancho longo demais` | Encurte o texto do gancho. |
| aviso `captions.json foi agrupado para 'x' e o vídeo usa 'y'` | Rode `captions.py --style y` de novo. |
| aviso `cut_timeline.json diz ...` | O corte mudou depois do timeline. Renderize o corte de novo. |
| aviso `insert N passava do fim do vídeo` | Ajuste `end` para antes do fim. |
| aviso `inserts N e M se sobrepõem` | Separe os tempos. |
| aviso `efeito sonoro 'x' não existe` | Use um nome da tabela de efeitos (em inglês ou em português) ou um arquivo. |
| `sfx.events[N].at precisa ser um número de segundos` | Escreva o segundo do corte como número, com ponto, sem aspas e sem sinal de menos (ex.: `5.07`). |
| `sfx.events[N] precisa de "at"` ou `precisa de "name"` | Cada item leva os dois: `{"at": 5.07, "name": "cash"}`. |
| `sfx.events[N].name: arquivo não encontrado` | Caminho relativo a `<EDIT>`. Confira o nome em `assets/`. |
| aviso `sfx.events[N] (...) passa do fim do vídeo` | O tempo é do corte. Veja `duration` em `cut_timeline.json` e ajuste o `at`. |
| aviso `sfx.events[N] (...) é cortado pelo fim do vídeo` | O som começa tão perto do fim que parte dele não toca. Adiante o `at` ou use um som mais curto (`coin`). |
| aviso `... o ponto forte chega N s atrasado` | O evento está perto demais do começo do vídeo para o som começar antes dele (comum com `rise`, que tem o ponto forte mais para dentro do som). Mude o tempo, troque o efeito ou, em `sfx.events`, use `"anchor_s": 0`. |
| `sfx.events[N].anchor_s precisa ser um número` | Segundos com ponto, 0 ou mais (ex.: `0.3`). |
| aviso `o arquivo do efeito sonoro 'x' está faltando` | A instalação da skill está incompleta. Instale de novo (`install.py`). |
| Som de caixa registradora não toca | Confira `sfx.enabled` e o `at`; procure o item em `layers` do `final_report.json`. |
| Legenda com palavra de marca errada | `captions.py --edit-dir '<EDIT>' --replace 'Cacto=Cakto'` (ou transcreva as fontes de novo com `--prompt`). |
| Linhas `confira:` no `captions.py` | As fontes e o corte ouviram palavras diferentes. Veja qual está certa e use `--replace`. |
| aviso `cut.json é de outro corte` no `captions.py` | Transcreva o `cut.mp4` de novo com `--edit-dir`. |
| `motion[N] precisa de "template"` ou `modelo de animação 'x' não existe` | Use um id da tabela de modelos (`motion_render.py --list`). |
| `motion[N]: <modelo>.params.<campo> ...` | O campo está errado para aquele modelo. Veja `motion_render.py --describe <modelo>`. |
| `motion[N].region "x" não existe` | Use `"auto"`, `"top"`, `"middle"`, `"bottom"` ou `{"x", "y", "w", "h"}`. |
| `motion[N]: use "end" ou "duration"` | Escreva só um dos dois. |
| aviso `a animação N (...) aparece junto com o gancho` ou `com a chamada` | Mude o `start` para depois do gancho ou o `end` para antes da chamada. |
| aviso `a animação N (...) ficou escondida de ... a ...` | Ela encostou na chamada ou no gancho. Mude o tempo ou a `region`. |
| aviso `a animação N (...) cobre o rosto` | Deixe `region` em `"auto"`. Com auto, use menos itens, outro modelo ou outro trecho. |
| aviso `a animação N (...) não achou lugar livre` | O rosto e os textos ocupam a tela nesse trecho. Troque o tempo ou use um modelo menor. |
| aviso `a animação N (...) não achou lugar livre de tudo` | Ela ficou fora do rosto, mas divide a tela com a chamada, o gancho ou outra animação. Mude o tempo. |
| aviso `animação N (...) acaba antes de terminar o desenho` | O `end` corta a animação antes do último item. Aumente o `end` ou tire o `end` para usar a duração sugerida. |
| aviso `as animações N e M aparecem juntas` | Uma animação por vez: separe os tempos. |
| aviso `... cobre o rosto ...` | O texto não achou lugar livre. Use a altura sugerida na própria linha ou deixe o `center_y` em auto. |
| aviso `o aviso ficou escondido em N% do tempo` | Deixe `disclaimer.center_y` em auto ou escolha outra altura. |
| Rosto cortado na tela dividida | Troque `split_top` por `split_bottom` (ou o contrário). |
| Legenda do vídeo original aparecendo junto com a nova | Desligue `captions` (`"enabled": false`) e afaste o gancho do texto antigo. |
| Dado pessoal na tela (nome, telefone, e-mail, documento) | Na imagem de um insert: borre a imagem. No próprio vídeo: cubra com um `full` ou corte o trecho (volta à Fase 1), com o ok da pessoa. |
| Palavra cortada no fim de um trecho (legenda com palavra estranha, ex.: "matrão") | O `end` do range cortou a palavra: volte ao EDL, veja no `timeline_view.py` e aumente o `end`. |
| Trilha alta demais | Diminua `volume_db` (ex.: -18 ou -20). |
| Trilha baixa demais, quase não se ouve | Suba `volume_db` (ex.: -12). Confira se não ficou um -22 antigo. |
| Mudou a favorita e a edição continua com a trilha antiga | É de propósito: a edição guarda a escolha. Rode `trilhas.py escolher --humor X --edit-dir '<EDIT>'` e monte de novo. |
| `audio.denoise precisa ser "off", "leve" ou "forte"` | Use um desses três textos, entre aspas. |
| aviso `X quase não tem pausa ... a limpeza ficou em leve` | Normal em fala sem pausa. Ouça o final; se a voz ficou abafada, desligue. |
| aviso `X: não há trecho só de ruído ... ficou sem limpeza de ruído` | O vídeo não tem um momento só de fundo. Nada a fazer; o som fica como está. |
| aviso `limpeza de ruído feita no som do corte` | Os vídeos brutos sumiram ou o corte mudou. Confira os caminhos em `sources` do EDL ou renderize o corte de novo. |
| aviso `a limpeza de ruído foi pulada: o teste do ffmpeg falhou` | O motivo vem entre parênteses (ex.: `não tem o filtro afftdn`). O ffmpeg instalado é incompleto ou antigo: peça para instalar o ffmpeg completo. O vídeo sai com o som original. |
| aviso `a limpeza de ruído foi pulada: os vídeos do corte não têm áudio` | Não há voz para limpar. Deixe `audio.denoise` em `off`. |
| Voz abafada ou com "chiado de água" depois da limpeza | Troque `forte` por `leve` (ou `off`). |
