# Projeto-modelo do editor de vídeo

Projeto HyperFrames que monta o vídeo final em cima do corte da skill (Fase 1): a cena de cada trecho (tela
cheia, prova, moldura, tela verde), a legenda, as animações do catálogo, as cenas sob medida, o texto atrás
da pessoa e os efeitos sonoros, tudo no tempo das palavras. Quem edita é a IA, seguindo
`references/enquadramento.md`, `references/animacao.md` e `references/som-e-trilha.md` da skill. A pessoa vê
tudo no estúdio do HyperFrames, com uma faixa por camada na linha do tempo; o render fica para o fim.

## O que precisa

- Python 3.8 ou mais novo e o `ffmpeg` (o corte da skill, Fase 1, já pede os dois).
- Node 22 ou mais novo (o HyperFrames roda pelo `npx`; a primeira vez baixa sozinho). O instalador da skill põe
  o Node em `~/.editor-video/node`, e o `build.py` procura lá primeiro.
- A pasta `edit` do corte da skill (Fase 1), já aprovado, com `cut.mp4`, `cut_timeline.json`,
  `transcripts/cut.json` e `face_layout.json`.

## Como começar

1. Crie o projeto a partir do modelo que está dentro da skill:
   `python3 '<SKILL>/modelo-projeto/build.py' novo '<VIDEOS>/projeto' --edit '<VIDEOS>/edit'`.
   Ele copia o modelo (sem sobras de outro projeto) e anota no `projeto.json` a skill de onde veio (`"skill"`)
   e a pasta `edit`. As ferramentas usam essa skill, e não outra instalada no computador. Nunca edite o modelo
   original. Copiou a pasta à mão? Escreva `"skill": "<SKILL>"` no `projeto.json` (ou deixe o
   `EDITOR_VIDEO_DIR` apontando para a skill: o primeiro build anota).
2. No `projeto.json`, confira o `formato` (`9:16`, `16:9`, `4:5` ou `1:1`) e o `estilo` (`papelaria`, `noite` ou `laranja`).
3. `python3 build.py corte`: o corte inteiro só com o vídeo (enquadrado pelo rosto, tela cheia) e a legenda
   simples. `python3 build.py estudio` e a pessoa revisa o corte no estúdio, antes de qualquer animação.
4. `python3 build.py palavras` mostra a fala com os tempos e os cortes.
5. Escreva os `blocos`, as `animacoes` e as `cenas_proprias` (exemplo abaixo).
6. Amostra (uns 8 s em edição simples, uns 15 s com motion, uns 5 s para texto atrás da pessoa):
   `python3 build.py amostra 17.9 27.9`. O estúdio aberto recarrega sozinho. Vídeo curto, do tamanho da amostra
   (um criativo de 10 s): a amostra já é o vídeo inteiro (o build diz numa linha `nota:`); confira e mostre esta
   mesma, e depois do ok o `completo` sai igual, sem outra rodada.
7. `python3 build.py hf lint` e `python3 build.py hf check` (os dois sem erro).
8. A pessoa aprova a amostra no estúdio. Vídeo todo: `python3 build.py completo`, `hf lint`, `hf check` e
   estúdio de novo.
9. Sons (seção "Sons", abaixo): `plano_sfx.py --projeto` e o `completo` de novo.
10. Render só no fim, com a aprovação: `python3 build.py hf render --quality draft --output renders/amostra.mp4`
    para conferir e `python3 build.py hf render --quality delivery --output renders/final.mp4` para entregar.
    Olhe o arquivo renderizado: ele é a referência, não a prévia do estúdio. A trilha entra por último, no
    arquivo renderizado (`helpers/mixar_trilha.py`).

`python3 build.py preparar` baixa o GSAP para `assets/` (render sem internet). Sem ele, a página usa o GSAP
do CDN.

## Projeto antigo, modelo novo

O `build.py` do projeto é uma cópia do modelo do dia em que o projeto nasceu. Quando o modelo da skill muda no meio
do trabalho, leve o projeto para o modelo novo, sem refazer nada:

```
python3 '<SKILL>/modelo-projeto/build.py' atualizar '<PROJ>'
```

Ele troca o `build.py`, este LEIA, o `package.json`, o `hyperframes.json`, as `ferramentas/`, os kits de cena
(`compositions/cenas/2d`, `compositions/cenas/3d` e o LEIA das cenas) e as fontes e o three.js de `assets/`. O que
mudou vai antes para `.antes-de-atualizar/<data>/` (volte um arquivo copiando de lá). O `projeto.json` (menos o
`"skill"`, que passa a ser a skill do modelo), os outros `assets` (sons, imagens, pessoa recortada), as cenas
próprias, o `eventos.json` e os `renders` ficam como estão. A saída diz o que foi trocado, o que é novo, o que só o
projeto tem (fica) e as novidades do modelo desde a versão do projeto (`MODELO_VERSAO` e `NOVIDADES` no
`build.py`). Depois: `python3 build.py completo` (ou a amostra), `hf lint` e `hf check`.

## Uma versão só do HyperFrames

O `package.json` fixa a versão (`hyperframes@0.8.79`). Chame sempre pelo `build.py hf`, que repassa ao `npx`
nessa versão: `python3 build.py hf lint`, `hf check`, `hf snapshot ...`, `hf render ...`. O estúdio usa a
mesma. `npx hyperframes ...` sem versão baixa a mais nova, e aí o lint, o check e o render rodam numa versão e o
estúdio em outra.

## Comandos

| comando | o que faz |
|---|---|
| `python3 build.py novo <PASTA> [--edit <EDIT>]` | (rodado no modelo dentro da skill) cria o projeto e anota a skill e o `edit` no `projeto.json` |
| `python3 build.py atualizar <PASTA>` | (rodado no modelo dentro da skill) leva um projeto antigo para o modelo novo e diz o que mudou (seção "Projeto antigo, modelo novo") |
| `python3 build.py palavras [de] [ate]` | a fala com os tempos, os cortes e os blocos |
| `python3 build.py corte` | `index.html` do corte inteiro só com o vídeo e a legenda simples; blocos, animações, cenas, sons e cartão final ficam de fora |
| `python3 build.py amostra <inicio> <fim>` | `index.html` só desse trecho, com tudo |
| `python3 build.py completo` | `index.html` do vídeo inteiro (com o fim parado e o cartão final, se ligados) |
| `python3 build.py hf <comando> [...]` | o HyperFrames na versão do `package.json`: `lint`, `check`, `snapshot`, `render`. O `snapshot` roda sem as chaves de IA do ambiente (seção "Snapshot sem mandar quadros para fora") |
| `python3 build.py estudio [--porta N] [--sem-abrir]` | sobe o estúdio em segundo plano (porta livre a partir de 3002), confere que responde e escreve a URL com `#project/<pasta>`. Se já estiver no ar (na mesma versão), só mostra a URL |
| `python3 build.py estudio --parar` | derruba o estúdio deste projeto e confere que a porta fechou (os de outros projetos continuam) |
| `python3 build.py preparar` | baixa o GSAP para `assets/` |

## O projeto.json

| campo | o que é |
|---|---|
| `titulo` | nome do vídeo |
| `skill` | a pasta da skill de onde o projeto veio (o `novo` escreve); as ferramentas usam essa primeiro |
| `edit` | pasta edit do corte da skill (Fase 1), relativa a esta pasta |
| `formato` | `9:16` (1080x1920), `16:9` (1920x1080), `4:5` (1080x1350) ou `1:1` (1080x1080). Em 4:5 e 1:1 a zona segura é uma margem folgada, **a conferir na rede** de destino (o build avisa) |
| `estilo` | `papelaria` (papel quadriculado lilás), `noite` (azul-marinho) ou `laranja` (escuro com laranja, o visual escuro de anúncio: fundo quase preto com grade e brilho laranja, cartão escuro com fio laranja, texto claro, laranja `#ff7a1a` de destaque, verde `#35d07f` e vermelho `#ff3b30`; o texto sobre o laranja é escuro; as cenas do kit 2D em tela cheia usam o mesmo fundo, com a grade e o brilho) |
| `correcoes` | o que a transcrição errou. A chave pode ter mais de uma palavra: `{"um IA": "uma IA", "S&A": "essa IA"}` troca só a sequência inteira; sem acento, maiúscula ou pontuação também casa (a pontuação do fim fica). Valor com outro número de palavras divide o tempo entre elas. Espaço sem quebra (U+00A0) no valor cola duas palavras numa só na legenda |
| `zoom` | zoom padrão da tela cheia (1,08); também vale no `corte` |
| `moldura` | `{"caixa": [x, y, largura, altura]}` para mudar o quadro da moldura |
| `blocos` | a cena de cada trecho (`references/enquadramento.md` da skill): `tela_cheia`, `prova`, `moldura`, `tela_verde`, `contida` (a gravação inteira com margem) e `comparar` (antes e depois); `zooms`, `borrar`, `inicio` da prova, `fundo` da moldura e da contida, `"acompanhar": true` (a câmera segue a pessoa dentro da tomada) |
| `legenda` | `ligada`, `estilo` (`padrao` ou `discreta`), `max_palavras` (3), `max_letras` (14 no padrão, 22 na discreta, em 9:16), `na_moldura` (false), `na_tela_verde` (true), `esconder` ([[de, ate]]). Nenhuma palavra dita some da legenda: cada frase (entre pausas de mais de 0,6 s e pontuação) é dividida por sentido, sem terminar legenda em artigo, preposição ou "não" e sem deixar palavra sozinha quando dá ("Eu gravei" / "esse vídeo" / "uma vez só,"); a palavra que piscaria menos de 0,3 s junta com a vizinha, mesmo passando de 3 por um instante |
| `animacoes` | as peças do catálogo (`references/animacao-catalogo.md` da skill), inclusive o texto atrás da pessoa |
| `cenas_proprias` | cenas escritas à mão: `[{"arquivo": "compositions/cenas/s03_orcamento.html", "de": 12.3, "ate": "cortou"}]` (`compositions/cenas/LEIA.md`). `"sobre_a_pessoa": true` quando a cena é um cartão pequeno acima da cabeça com a pessoa em tela cheia (`"area": "acima_do_rosto"` nas cenas `k2d_medidor`, `k2d_comando`, `k2d_busca` e `k2d_notas`): a câmera viva fica em zoom 1,0 e não aproxima nesse trecho (rode o `camera_viva.py` depois de marcar); o build ignora o campo |
| `sons` | os efeitos sonoros (seção "Sons"); quem escreve é o `plano_sfx.py --projeto` |
| `segura_fim` | segundos com o último quadro parado no fim, a voz já terminada, e as peças e cenas continuando (tempo de leitura do fim). 1 a 2 s é o comum. Peça ou cena própria cujo `sai`/`ate` cai no fim do corte (até 0,25 s antes) continua até o fim do quadro parado, legível até o último quadro (a saída dela cai depois do fim); o fundo desfocado ou a cor da contida também. Sem `segura_fim`, a peça que vai até o fim do corte também fica até o último quadro |
| `cartao_final` | `ligado`, `segundos` (1,8), `titulo`, `sub`, `botao`, `logo`. Com `segura_fim`, entra depois do fim parado |

Tempo é sempre em segundos do `cut.mp4`, ou a palavra (`"em": "produtos", "depois_de": 20`). Com o
`palavras_onda.json` na pasta `edit` (`uv run --directory '<SKILL>' python helpers/palavras_na_onda.py '<EDIT>'`), a
palavra usa o começo medido na onda, e não o do Whisper, que adianta até 0,3 s: as âncoras das peças, a legenda e as
`palavras` das cenas próprias. O `build.py palavras` diz qual tempo está valendo. Arquivo de outro corte é ignorado
com aviso (rode o helper de novo depois de cada corte).

Exemplo:

```json
{
  "titulo": "Criativo produtos digitais",
  "skill": "/caminho/da/skill/editor-video",
  "edit": "../edit",
  "formato": "9:16",
  "estilo": "papelaria",
  "correcoes": {"um IA": "uma IA"},
  "blocos": [
    {"cena": "tela_cheia", "de": 0, "ate": 14.4, "zoom": 1.1, "zooms": [{"em": "nunca", "escala": 1.3, "ate": "vendi"}]},
    {"cena": "prova", "de": 14.4, "ate": 18.3, "arquivo": "assets/reel.mp4", "inicio": 9.9, "rotulo": "Reel *publicado*",
     "borrar": [{"caixa": [0.05, 0.1, 0.5, 0.06], "de": 15.0, "ate": 16.2}]},
    {"cena": "moldura", "de": 21.933, "ate": 28.033},
    {"cena": "tela_cheia", "de": 28.033, "zoom": 1.1}
  ],
  "legenda": {"ligada": true, "estilo": "padrao", "max_palavras": 3, "na_moldura": false},
  "animacoes": [
    {"id": "fim", "tipo": "atras_da_pessoa", "texto": "O FIM", "em": "fim", "sai": "editores"},
    {"id": "sem", "tipo": "lista_check", "titulo": "Você *não precisa* de", "marca": "x", "depois_de": 22, "sai": 27.9,
     "itens": [{"texto": "Espaço", "em": "espaço"}, {"texto": "Estoque", "em": "estoque"}, {"texto": "Frete", "em": "frete"}]}
  ],
  "cenas_proprias": [
    {"arquivo": "compositions/cenas/s04_orcamento.html", "de": "orçamento", "ate": 34.2, "esconde_legenda": true}
  ],
  "sons": [],
  "segura_fim": 1.2,
  "cartao_final": {"ligado": false}
}
```

Tipos de animação: `rotulo`, `carimbo`, `lista_check`, `passo_a_passo`, `contador`, `notificacao`,
`polaroide`, `objeto_na_mao`, `hud` (com `tamanho` e `topo`), `atras_da_pessoa` (com `frente`) e `lettering`. Campos
de cada um: `references/animacao-catalogo.md` da skill.

## Sons

Um caminho só, sem colar nada no `index.html` (o build reescreve o arquivo a cada vez):

1. `python3 build.py completo` grava `sons_sugeridos.json` (o som que cada animação, zoom, troca de cena e
   entrada de prova pede, com o tempo do pico e o nível, `db`, em dBFS de pico). Os eventos das cenas próprias
   vão num `eventos.json` na pasta do projeto (`[{"t": 12.4, "tipo": "pop"}]`).
2. `uv run --directory '<SKILL>' python helpers/plano_sfx.py --projeto '<PROJ>'`: monta o plano (um som por
   animação, 0,65 s de espaço mínimo, o nível que a peça pediu), copia os sons para `assets/sfx` (com
   `LICENCAS.txt`) e escreve o campo `sons` do `projeto.json`. Os níveis que o build pede seguem a tabela de sons:
   pequeno -18 (pop, clique, tique, whoosh), médio -15, destaque -12 (carimbo, número final, notificação) e baixo
   -20. Destaque é no máximo um a cada 6 s: o que vem logo depois de outro desce para médio no
   `sons_sugeridos.json`. No `eventos.json`, o `"nivel"` do evento manda.
3. `python3 build.py completo` de novo: os sons entram no `index.html`, com faixa própria no estúdio, que toca
   todos eles. O render sai com os sons.
4. Render. A trilha entra por último, no arquivo renderizado: `helpers/mixar_trilha.py`.

Cada som do `projeto.json`:

| campo | o que é |
|---|---|
| `arquivo` | o som, a partir da pasta do projeto (`assets/sfx/pop-hand.wav`) |
| `em` | o quadro em que o pico do som cai (segundos do corte ou a palavra) |
| `pico_no_arquivo` | onde fica o pico dentro do arquivo (sem ele, o build mede o maior pico) |
| `inicio_no_arquivo` | pula o começo do arquivo (segundos) |
| `dur` | quanto toca (padrão: até 1,2 s) |
| `volume` ou `db` | ganho direto (1 = 0 dB, no máximo 3,98) ou o pico que o som deve ter, em dBFS |
| `fade`, `fade_entrada` | descida no fim e subida no começo (segundos) |
| `tipo`, `nota`, `plano` | só para ler (o `plano: true` marca o que o `plano_sfx.py` escreveu; ele troca só esses) |

Som cujo começo cairia antes do 0 perde só a cabeça (entra com uma subida curtinha). Som que não cabe no vídeo,
sem arquivo ou sem `em` sai com uma linha `aviso:`; nenhum some calado.

## Texto atrás da pessoa (2,5D)

Uma palavra grande entre o vídeo e a pessoa recortada: a cabeça passa na frente da parte de baixo das letras,
a palavra desliza devagar de lado (paralaxe leve) e entra letra por letra na palavra falada. É composição em
planos, 2,5D: não tem 3D de verdade.

1. Só funciona com o recorte da pessoa daquele trecho. Rode antes
   `python3 ferramentas/pessoa_recortada.py --edit '<EDIT>' --de 5.5 --ate 9.5` e aprove um teste de uns 5 s
   (cabelo, mãos e borda). Sem o recorte, ou com o recorte de outro trecho, o build para e diz o comando certo.
2. `{"id": "fim", "tipo": "atras_da_pessoa", "texto": "O FIM", "em": "fim", "sai": "editores"}` em
   `animacoes`. Só em tela cheia.
3. O build mede o topo da cabeça no próprio recorte e põe o meio das letras ali. Se o rosto medido cobrir mais
   de uns 40% da largura das letras, ele avisa: suba o texto (`"altura": "topo"`), use uma palavra mais
   comprida ou outro trecho.

Campos: `texto` (`*palavra*` sai na cor de destaque), `em`, `sai`, `altura` (`cabeca`, `topo` ou o topo em
pixels), `tamanho` (px; padrão: a maior que cabe na zona segura), `cor`, `paralaxe` (1; 0 desliga),
`caixa_alta` (true), `recorte` (`assets/pessoa.webm`; outro arquivo quando o vídeo tem tela verde num trecho e
texto atrás em outro: rode a ferramenta com `--saida assets/pessoa_fim.webm`), `frente` e `fundo_anda`.

**Terceiro plano.** `"frente": "assets/pote.png"` (ou `{"imagem": ..., "em": palavra, "sai": ..., "lado":
"esquerda|direita", "tamanho": px, "onde": [x, y]}`) põe uma imagem do projeto NA FRENTE da pessoa: entra depois da
palavra (padrão: 0,35 s depois; nunca antes de 0,2 s), num canto de baixo do lado oposto ao rosto, e sai com ela.
Use png com fundo transparente (jpg vira retângulo, o build avisa). Cada plano anda numa medida própria: o fundo
(a gravação, com a pessoa junto, porque os dois saem do mesmo vídeo) se aproxima uns 1,5% e desliza 6 px, a
palavra desliza 36 px e a frente 80 px, crescendo 4%. Depois o fundo volta ao normal em 0,5 s (no corte, seco).
`"fundo_anda": false` deixa o fundo parado. O build avisa quando a frente passa na frente do rosto e a legenda
desvia dela. Frente tem uma faixa própria no estúdio ("Frente · pote.png").

## Acompanhar a pessoa dentro da tomada

O `face_layout.json` guarda um rosto por tomada, então dentro de uma tomada o enquadramento fica parado. Quando a
pessoa se inclina, anda ou muda de lugar no meio da tomada e o trecho tem zoom (moldura, tela cheia a 1,15 ou mais,
um punch-in), ponha `"acompanhar": true` no bloco (`tela_cheia` ou `moldura`):

```json
{"cena": "moldura", "de": 7.07, "ate": 10.6, "acompanhar": true}
```

- Na primeira vez, o build mede o rosto 10 vezes por segundo no `cut.mp4` com o Python da skill
  (`helpers/rastrear_rosto.py`, o mesmo detector do `face_layout.py`; uns segundos por minuto de vídeo) e grava
  `<EDIT>/rosto_trilha.json`. Corte novo: mede de novo sozinho. À mão:
  `uv run --directory '<SKILL>' python helpers/rastrear_rosto.py '<EDIT>'`.
- A trilha é suavizada: mediana de 5 medidas, zona morta de 3% da largura (tremida e micro movimento não mexem
  em nada), cada mudança vira um movimento de 0,5 s em `sine.inOut`, o último ponto bom fica quando o rosto some
  (mão na frente, virou de lado) e tudo recomeça em cada tomada.
- A câmera segue a trilha quadro a quadro (dados e laço, como o timecode: o seek em qualquer ponto acerta), fora
  das transições animadas; a transição da moldura já termina no rosto acompanhado. A pessoa recortada
  (`assets/pessoa.webm`, texto atrás da pessoa) recebe a mesma conta. Na moldura o tamanho do rosto é o da tomada
  (o quadro não "respira").
- A cabeça inteira é conferida 10 vezes por segundo dentro do quadro: quando a pessoa vai para a borda da gravação
  e o enquadramento não tem para onde ir, o build avisa com o intervalo ("não cabe"): diminua o zoom, use tela
  cheia ou `contida` nesse trecho.
- Em tela cheia a 1,08 sobra só 8% de margem para andar: o efeito aparece com zoom maior ou num punch-in.

## Gravação contida, 4:5 e 1:1

`{"cena": "contida", "de": 10.6, "ate": 17.7}` mostra a gravação inteira dentro da tela, com margem, sobre o
próprio vídeo desfocado (`"fundo": "desfocado"`, o padrão), o papel do estilo (`"papel"`) ou uma cor (`"#1b1830"`).
`margem` (px, padrão 6% do lado menor) e `raio` (os cantos). Entra e sai do jeito da moldura: no corte, troca seca;
no meio da tomada, o quadro encolhe em 0,6 s (e volta em 0,55 s). A legenda fica ligada.

É a saída quando a região obrigatória não cabe no recorte (gesto largo, gravação em pé indo para 16:9, 4:5 ou
1:1): o corte 9:16 num projeto 4:5 passa da tela em cima e embaixo, e a contida mostra ele inteiro. O build
enquadra o rosto pelo retângulo que o vídeo ocupa de verdade (um corte em pé num 4:5 não perde o rosto).

Formatos 4:5 (1080x1350) e 1:1 (1080x1080): o `formato` do `projeto.json`. A zona segura deles é uma margem
folgada (4:5: x 100 a 960, y 110 a 1220; 1:1: x 90 a 970, y 90 a 980), sem medida conferida numa rede: **a conferir
na rede** de destino antes de publicar. O build avisa em todo build. Mudar o formato de uma edição aprovada é uma
versão nova: copie a pasta do projeto e troque o `formato`.

## Comparar (antes e depois)

```json
{"cena": "comparar", "de": 10.6, "ate": 14.0, "modo": "lado_a_lado",
 "a": {"arquivo": "assets/cru.mp4", "inicio": 15.0, "rotulo": "Gravação crua"},
 "b": {"arquivo": "corte", "inicio": 10.6, "rotulo": "Corte da skill"}}
```

- Dois arquivos do projeto (vídeo ou imagem), ou dois trechos do mesmo arquivo (`inicio` diferente), ou um trecho
  do próprio corte (`"arquivo": "corte"`, `inicio` em segundos do corte). Os dois tocam juntos, na mesma
  velocidade, e aparecem no mesmo tamanho, inteiros (sem cortar nada), sobre o papel do estilo.
- `modo`: `lado_a_lado` (em linha ou empilhado, o que deixar a imagem maior) ou `alternando` (um de cada vez no
  mesmo lugar, trocando em `"trocas": [palavra ou segundos]`; sem trocas, uma no meio; cada lado fica pelo menos
  0,8 s). A troca é seca, sem efeito.
- `rotulo` é obrigatório e literal: o que aquele lado é ("Gravação crua", "Com a limpeza de voz"). Rótulo com
  opinião ("melhor", "perfeito", "ruim") gera aviso.
- `som`: `voz` (o padrão: a fala do corte continua e os dois lados ficam mudos), `a` ou `b` (só o som desse lado; a
  voz do corte sai do trecho e o build avisa quais palavras ficam mudas: deixe esse espaço no corte) ou `igualado`
  (só no `alternando`: toca o som do lado que está na tela, os dois no mesmo volume percebido, medido em LUFS; o
  mais alto desce, nenhum sobe). Nenhum lado é degradado: o build avisa quando a diferença de resolução ou de
  proporção vem do arquivo.
- A legenda sai no bloco (texto por cima do que se compara atrapalha).

## Lettering

`{"id": "fim", "tipo": "lettering", "principal": "Ficou com a *melhor*", "apoio": "sem abrir o editor",
"chamada": "Veja o resultado", "em": "ficou", "sai": 21.2, "posicao": "topo", "estilo": "bloco"}`: o texto grande
com ordem de leitura (principal, apoio, chamada), letra por letra montada já no lugar, encaixe por sentido. Cada
palavra grande (e cada palavra do apoio) entra na hora em que é dita, medida na onda (`palavras_onda.json`),
antecipando no máximo 0,15 s; sem o arquivo, no tempo do Whisper, com aviso. A legenda sai do trecho, ou, com
`"legenda_junto": true` (o padrão quando a legenda é `discreta`), continua nas palavras que não estão escritas
grandes naquele instante. A última expressão que vai até o fim do corte fica legível até o último quadro. O build
avisa quando a expressão não tem 1 s de leitura antes da próxima entrar (tire uma expressão ou junte duas) e diz
numa linha `nota:` o tamanho da letra e por que não foi maior. Campos e regras: `references/animacao-catalogo.md`
(peça 12) e `references/lettering.md` da skill. A largura das letras vem de `assets/fontes/larguras.json`, medida no arquivo da
fonte e no peso usado (`ferramentas/medir_fontes.py` gera de novo quando a fonte muda); na página, a timeline só é
montada depois que a fonte carregou, e a linha que passar da largura desce a letra o que falta.

## Mãos fora do quadro

Com `assets/maos.json` (a ferramenta `rastrear_maos.py`, seção "Ferramentas"), o build confere as mãos em cada
quadro de tela cheia, moldura e tela verde: quando uma mão que a gravação mostra sai do quadro da moldura ou da tela
(o zoom ou o recorte corta o gesto) por 0,2 s ou mais, ele avisa com o intervalo. Se a mão mostra algo ali, aumente
o quadro, diminua o zoom, use tela cheia ou `contida`.

## Snapshot sem mandar quadros para fora

Com uma chave de IA no ambiente (`GEMINI_API_KEY` e parecidas), o `hyperframes snapshot` manda os quadros para o
serviço de fora. O `python3 build.py hf snapshot ...` (e o `hf check --snapshots`, que também tira quadros) roda
sem essas variáveis e diz quais tirou. Só com o pedido da pessoa: `python3 build.py hf snapshot --com-ia ...` (os
outros comandos não mexem no ambiente).

## Kit 2D

Oito cenas prontas em `compositions/cenas/2d/` (`k2d_arquivo`, `k2d_conversa`, `k2d_esquema`, `k2d_antes_depois`,
`k2d_comando`, `k2d_medidor`, `k2d_busca`, `k2d_notas`): objeto que muda de estado mantendo o desenho e o lugar, esquema
que cresce com a fala, comparação desenhada, barra que digita o pedido para a IA, medidor com agulha, barra de busca
com o resultado e a chamada e bloco de notas com a lista que se conclui. Entram
como cena própria. Quando usar e os parâmetros: `references/animacao.md` da skill, seção Kit 2D.

**Formato.** As cenas do kit (2D e 3D) declaram a raiz em 1080x1920 e se desenham pelo tamanho do vídeo que o build
manda. Em 16:9, 4:5 e 1:1 o build põe a raiz no tamanho do vídeo no `index.html` (uma linha de aviso diz quais),
sem mexer no arquivo: a cena sai inteira, no estúdio e no render. Cena escrita à mão com posições fixas e a raiz num
tamanho diferente do vídeo sai cortada, e o build avisa (`compositions/cenas/LEIA.md`, "O tamanho da raiz").

## 3D de verdade

O kit fica em `compositions/cenas/3d/` (o motor `motor-three.js` e quatro cenas prontas, `k3d_*.html`) e o
three.js em `assets/three`. Entra como cena própria. Como usar, o teste de 5 s e a alternativa quando o WebGL
não abre: `references/3d.md` da skill. As cenas do kit aparecem no painel Compositions do estúdio; é normal.

## O estúdio

`python3 build.py estudio` sobe o estúdio do HyperFrames em segundo plano e escreve a URL, por exemplo
`http://127.0.0.1:3002/#project/projeto`. A URL precisa do `#project/<nome da pasta>`: sem ele o estúdio abre
vazio. Ele continua no ar depois que o comando termina, recarrega sozinho a cada build e só para com
`python3 build.py estudio --parar`, que confere que a porta fechou (e tenta de novo se o HyperFrames não
conseguiu, o que acontece com a máquina carregada). Ele nunca derruba o estúdio de outro projeto.

A linha do tempo mostra, de cima para baixo: o vídeo do corte, a legenda (uma faixa com um clipe por
legenda), o fim parado, uma faixa para cada peça do catálogo e para cada cena própria (com o nome dela), a
pessoa recortada, os elementos de estrutura (câmera, moldura, papel, borda, fundo desfocado) e, embaixo, a voz e
os sons. Faixa com mais de um clipe aparece como "Track N" (cada clipe continua com o nome dele).

Pegadinhas:

- O estúdio toca o vídeo e a pessoa recortada como dois vídeos separados: numa emenda ou no texto atrás da
  pessoa, uma camada pode aparecer atrasada por uma fração de segundo. No arquivo renderizado não aparece. O
  render é a referência.
- No painel Browser do app, o vídeo do corte só é desenhado com o painel à mostra: escondido, a prévia fica
  preta (a legenda e as peças aparecem). Com a máquina carregada o estúdio demora e pode dizer que "não
  recarregou": recarregue a página.
- O que for mexido no estúdio (arrastar, esticar, trocar texto) fica no `index.html` e nas `compositions/`,
  que o build reescreve. O build guarda uma cópia em `.mexido-no-estudio/` e avisa: passe a mudança para o
  `projeto.json`.
- O estúdio carimba um `data-hf-id` nos elementos quando abre o projeto, inclusive nas cenas próprias. Isso é
  normal e o build não confunde com mudança; ao editar uma cena própria depois disso, procure o trecho sem
  contar com o texto exato das tags.

## O que o build grava

- `index.html`: a composição principal (não edite à mão: rode o build de novo).
- `compositions/<id>.html`: uma sub-composição por peça do catálogo (faixa própria no estúdio). O build apaga
  e escreve de novo a cada vez; as cenas sob medida ficam em `compositions/cenas/` e nunca são mexidas.
- `assets/cut.mp4`: ligação (ou cópia) do corte. Com `borrar` num bloco, é uma cópia com o borrão queimado no
  arquivo (quem recebe a pasta do projeto não vê o dado); o original sem borrão fica no `edit/`.
- `assets/gerado/`: o que o build tira do corte quando precisa: o último quadro (`segura_fim`), o corte
  desfocado (moldura com `"fundo": "desfocado"`) e o arquivo da prova com o borrão (`<nome>_borrado`).
- `assets/gerado/` também guarda a cópia ligada de um arquivo usado nos dois lados do `comparar` (ou do próprio
  corte), o último quadro do fundo desfocado para o fim parado e as imagens das peças (polaroide, objeto na mão,
  frente, logo do cartão final) já no tamanho em que aparecem (`<nome>_<largura>x<altura>`): imagem maior que a
  caixa, reduzida pelo navegador, saía com a borda diferente de um render para outro. A original não muda.
- `<EDIT>/rosto_trilha.json` (na pasta do corte, junto do `face_layout.json`): o caminho do rosto dos blocos com
  `acompanhar`.
- `sons_sugeridos.json`: o som que cada animação pede, com o tempo do pico (para o `plano_sfx.py --projeto`).
- `.build.json`: o modo, a janela e a assinatura de cada arquivo gerado (para perceber mudança feita fora).
- `.estudio.json`: a porta e a versão do estúdio no ar (o `--parar` usa).
- `.antes-de-atualizar/<data>/`: o que o `atualizar` trocou, como estava antes.

Linhas `nota:` dizem o que o build decidiu sozinho (o tamanho da letra do lettering e o porquê, a amostra que já é o
vídeo inteiro). Linhas `aviso:` na saída do build pedem atenção: peça que não coube, item que ficou de fora, peça empurrada
para depois da moldura fechar, legenda encostando numa peça ou no rosto, som fora do vídeo, texto atrás da
pessoa ilegível, arquivo mexido no estúdio, vídeo em pé virando 16:9, cabeça ou mão saindo do quadro, lettering
sem tempo de leitura (tire uma expressão ou junte duas), zona segura a conferir na rede (4:5 e 1:1), fala que fica muda num `comparar`, cena própria
com a raiz num tamanho diferente do vídeo (ajustada ou cortada).

## Ferramentas

As duas primeiras usam primeiro a skill do `"skill"` do `projeto.json` e dizem na primeira linha qual skill usaram.

- `ferramentas/pessoa_recortada.py --edit '<EDIT>' --de 28 --ate 32.3`: a pessoa com fundo transparente para a
  tela verde e para o texto atrás da pessoa (`assets/pessoa.webm` e `assets/pessoa.json`, com o topo da cabeça
  de cada tomada), usando o recorte da skill (`helpers/matte.py`). Uns 2 a 3 minutos por minuto de vídeo na
  primeira vez; depois o recorte fica guardado. `--skill` escolhe outra skill.
- `ferramentas/rastrear_maos.py`: as mãos quadro a quadro para o objeto preso na mão e para o aviso de mão fora
  do quadro (`assets/maos.json`).
  Rode sempre assim (o mediapipe 1.x e o Python 3.13 abortam com "Service is unavailable"):
  `uv run --python 3.12 --with mediapipe==0.10.21 --with numpy python ferramentas/rastrear_maos.py --edit '<EDIT>' --de 8 --ate 13`.
  A primeira vez o uv baixa o Python 3.12 e o mediapipe: uns minutos. Avise a pessoa antes. O modelo de mãos
  fica em `~/.editor-video/modelos`.
- `ferramentas/medir_fontes.py`: mede a largura de cada letra das fontes de `assets/fontes` no peso usado
  (`assets/fontes/larguras.json`, que o lettering usa para encaixar o texto). Rode com o Python da skill quando
  trocar ou acrescentar uma fonte: `uv run --directory '<SKILL>' python '<PROJETO>/ferramentas/medir_fontes.py'`.

## Licenças

- Fontes Montserrat, Caveat e JetBrains Mono: SIL Open Font License 1.1 (`assets/fontes/OFL-*.txt`).
- three.js r181: MIT, em `assets/three` (licença em `assets/three/LICENSE`, origem em `assets/three/ORIGEM.txt`).
- GSAP (carregado do CDN ou baixado pelo `preparar`): licença padrão da GreenSock, uso gratuito.
- Efeitos sonoros copiados pelo `plano_sfx.py`: a licença de cada um em `assets/sfx/LICENCAS.txt` (os do Woven
  SFX são CC0, com `assets/sfx/LICENCA-woven.md`).
- HyperFrames: Apache 2.0. Modelo de mãos do MediaPipe: Apache 2.0 (baixado na primeira vez).
