Pular para o conteúdo
Beneverso
Equipe

Guia

O laboratório de vídeo

Como sai um vídeo vertical hoje: a stack web (não o After Effects), o pipeline script a script, a invariante que deixa o render paralelo funcionar e o vocabulário do roteiro animado.

Produção

Original no vault: _Lab_Video/LEIA_ME.md

Esta página também reúne

  • _Lab_Video/roteiro/ROTEIRO_MODELO.jsono vocabulário do roteiro animado, campo a campo
  • _Lab_Video/web/o código do motor; o pipeline está nos cabeçalhos dos scripts, não em prosa (a ressalva desta página lista quais)
Ressalva: ISTO NÃO É A TRANSCRIÇÃO DE UM DOCUMENTO SÓ. O _Lab_Video tem dois arquivos de prosa — o LEIA_ME.md e o CREDITOS.md — e nenhum dos dois descreve o pipeline que acabou existindo: ele está no cabeçalho dos scripts. São QUATRO as fontes desta página: (a) o LEIA_ME.md, citado literalmente; (b) o roteiro/ROTEIRO_MODELO.json; (c) os cabeçalhos que o próprio autor escreveu no topo de montar_v2.py, render_turbo.py, mixar_v2.py, motor.js e mod_efeitos/efeitos.js, mais o roteiro/_validar.py e o roteiro/MUDANCAS_v2.md; e (d) A MEMÓRIA DO PROJETO — conhecimento verificado que o Felipe registrou fora do disco. Nada foi inventado; o conjunto é que foi montado aqui. O QUE VEM SÓ DA MEMÓRIA, e portanto NÃO se reconfere abrindo o _Lab_Video/: o motivo de o After Effects ter perdido, o ganho de tempo do render paralelo, as três pegadinhas (fonte no Playwright, o regex de url(), o peso das variable fonts) e o bloco inteiro das fotos de candidatas do TSE — a URL do ZIP, o padrão de nome de arquivo, o índice de candidaturas e os 161×225 px. Quem procurar essas coisas em arquivo do laboratório não vai achar: não é invenção da página, é conhecimento que nunca foi escrito lá. O LEIA_ME.md AINDA ESTÁ ESCRITO COMO TESTE EM ABERTO — “comparar as duas stacks antes de casar com uma delas”. O teste já terminou: desde 18/08/2026 a stack é a web, e a peça “Senado Amigo do Povo” foi inteira renderizada nela. Não corrigi o arquivo do disco; este é o registro. DOIS RESÍDUOS DE TEMPLATE COPIADO, que não foram propagados para esta página. (1) O ROTEIRO_MODELO.json declara "projeto": "benedita_do_rio" — o Lab é Beneverso, e Benedita do Rio é marca separada que nunca compartilha ativo. É lixo de cópia, não afiliação. (2) O vocabulário de entradas do MODELO (cai_e_quica, vira_pagina, xerox…) NÃO é o que o motor entende hoje — os nomes implementados são outros, e nome desconhecido não dá erro: cai no fallback silencioso “aparecer”. As duas listas estão na página, lado a lado, exatamente por isso. ARMADILHA DE NOME DE ARQUIVO: o gerar_roteiro_v3.py grava por cima do ROTEIRO_VISUAL_v2.json. O arquivo diz v2 e o conteúdo no disco é o da v3 — conferir o cabeçalho do gerador antes de supor qual versão está ali. O VALIDADOR NÃO RODA. `python roteiro/_validar.py` estoura KeyError: 'id' na linha 31 contra o próprio JSON que está no disco — o VOCAB e a leitura de elemento dele ficaram para trás do gerar_roteiro_v3.py, e o script morre antes de validar coisa alguma. As regras dele estão na página como ESPECIFICAÇÃO ESCRITA, não como rede viva: hoje ninguém está protegido por elas. Não consertei o script — o _Lab_Video/ é a fonte, isto aqui é o espelho.

Os vídeos verticais do Beneverso saem de _Lab_Video/ desde 18/08/2026, em stack web: HTML/CSS animado → frames capturados no Chromium → MP4 no ffmpeg. O After Effects está instalado e foi testado lado a lado — e perdeu.

Por que web e não After Effects

O laboratório nasceu como comparativo. Do LEIA_ME.md, literal:

Objetivo: rodar o mesmo roteiro em duas stacks e comparar resultado + velocidade, antes de casar com uma delas.

  • web/ — stack HTML/CSS animado → frames (Playwright) → MP4 (ffmpeg)
  • ae/ — stack After Effects (ExtendScript .jsx + aerender)
  • roteiro/ROTEIRO_MODELO.json — formato do roteiro. O "_como_preencher" explica os campos.
  • assets/ — fotos, recortes, texturas e áudio do teste
  • saida/ — MP4 final das duas stacks, pra comparar lado a lado

O comparativo terminou e a escolhida é a web. Dois motivos, nesta ordem: o Felipe não quer aprender o AE, e no AE se renderiza às cegas — no navegador dá para ver a peça e ajustar em segundos. O ae/ continua no disco (plenario.jsx, plenario_AE.aep) como registro, e os dois resultados estão em saida/WEB_plenario.mp4, AE_plenario.mp4 e o COMPARA_web_vs_ae.mp4 que os põe lado a lado.

O timing: a narração manda, ninguém cronometra

Do LEIA_ME.md, e é a regra que mais economiza trabalho:

A narração define a duração. Gero o áudio primeiro (ElevenLabs), meço cada trecho, e a animação se encaixa nele. Ninguém cronometra nada na mão.

Na prática: a duração de cada beat sai de assets/audio/beats.json, que é medido do áudio já gerado. O roteiro não traz tempo digitado — traz o texto falado, e o tempo é consequência dele. Trocar uma frase da narração re-mede tudo sozinho.

O pipeline, arquivo por arquivo

PassoO que rodaO que sai
1. Roteiroroteiro/gerar_roteiro_v*.pyroteiro/ROTEIRO_VISUAL_v2.json
2. Linha de montagemweb/montar_v2.pyweb/peca_v2.html (tudo inline)
3. Renderweb/turbo/render_turbo.pyos PNG + o MP4 mudo
4. Mixagemweb/mixar_v2.pyo MP4 final, em saida/

montar_v2.py — do cabeçalho do script:

Linha de montagem da peca v2. Junta: motor.js + mod_efeitos/efeitos.js + fontes_novas/fontes.css

  • ROTEIRO_VISUAL_v2.json + beats.json -> peca_v2.html (tudo inline). Tudo inline porque a captura roda em file:// e fetch() nao funciona la.

render_turbo.py — divide a faixa de frames entre até 8 processos. Oito é teto, não número: o script faz n_work = max(1, min(8, (cpu_count() or 4) - 2)), então numa máquina de 6 núcleos são 4 workers. Cada worker abre a própria página no Playwright, chama window.render(t) só nos frames dele e salva com o índice global; no fim o ffmpeg junta tudo na ordem.

📌 O ganho de "~10 min para ~25 s" é medição RELATADA pelo autor, sem registro no disco. Não há log, tabela nem saída de benchmark no _Lab_Video/ que sustente o par de números — trate como ordem de grandeza, não como dado conferido. Quem quiser o número de verdade roda o web/turbo/bench_serial.py e anota.

mixar_v2.py — junta vídeo mudo + narração + trilha + os SFX marcados em assets/audio/novos/plano_som.json (cada evento traz arquivo, tempo_seg e volume).

A invariante que sustenta tudo

window.render(t) é função pura do tempo. Sem Date.now, sem Math.random em runtime, sem CSS animation. Do contrato escrito em web/mod_efeitos/efeitos.js:

DETERMINISMO (requisito duro)

  • zero Date.now / performance.now
  • zero Math.random em tempo de execucao
  • zero CSS animation / transition
  • toda variacao vem do PRNG por seed abaixo (hash puro, sem estado)

Mesmo dt + mesmo o.seed ==> exatamente o mesmo retorno, sempre.

E o porquê, do cabeçalho do render_turbo.py:

Isso so e seguro porque window.render(t) da peca.html e deterministico e sem estado acumulado: o unico "estado" do DOM (innerHTML da datilografia e o clip-path da fita) e reescrito por inteiro a cada chamada, em funcao apenas de t. Logo qualquer worker pode pular direto para qualquer t.

⚠️ Quem quebrar a pureza quebra o render paralelo, e o sintoma não é um erro: é frame divergindo de frame. A variação "aleatória" (o torto de cada foto, o jitter do papel) vem toda de rnd(seed), um hash puro — não de Math.random.

Três pegadinhas que já custaram um round de revisão

1. Fonte em página capturada pelo Playwright. O Chromium registra as @font-face mas não as carrega até serem exigidas, e o screenshot não espera. Sem

await Promise.all([...document.fonts].map(f => f.load()))

antes de marcar a página como pronta, a peça inteira sai em Arial — sem erro nenhum.

2. Reescrita de url(...) no CSS. Capture a aspa DENTRO do grupo, antes do lookahead: url\(\s*['"]?(?!data:) casa antes da aspa e corrompe url("data:font/ttf.... Mais simples ainda: pular o regex quando o CSS já contém base64,.

3. Variable fonts precisam de peso explícito. Big Shoulders e Archivo VF exigem font-weight: 900; em 400 saem finas e a peça perde o soco. Alfa Slab One, Bungee e Rubik Distressed são peso fixo 400 — nelas não se mexe.

Fotos de candidatas (TSE)

  • ZIP oficial por UF: cdn.tse.jus.br/estatistica/sead/eleicoes/eleicoes2026/fotos/foto_cand2026_<UF>_div.zip
  • Nome de arquivo dentro do ZIP: F<UF><SQ_CANDIDATO>_div.jpg
  • Índice para casar nome × SQ_CANDIDATO: consulta_cand/consulta_cand_2026.zip — CSV em latin-1, separador ;
  • Resolução: 161×225 px. Serve para mosaico e grade de rostos; não serve para close.

O roteiro animado — o formato canônico

O formato é o roteiro/ROTEIRO_MODELO.json. Ele tem um meta (título, projeto, formato, resolução, fps, descrição de voz e prompt de trilha), uma lista de beats e um bloco _como_preencher que é a documentação do formato. Transcrito:

CampoO que é
narracaoO texto falado. A duração de cada beat sai daqui — o áudio é gerado primeiro e a animação se encaixa nele. Não precisa cronometrar nada.
direcao_vozOpcional. Tags do eleven_v3 inline no próprio texto: [sussurrando] [rindo] [pausa] [emocionada].
elementosO que aparece na tela nesse beat. Ordem = ordem de empilhamento (o último fica por cima).

Tipos de elemento: foto · recorte · texto · fita · carimbo · doodle · textura_fundo.

As 8 entradas do MODELO, como estão escritas nele:

EntradaDescrição no arquivo
cai_e_quicao elemento cai de cima, bate e assenta torto
desliza_de_baixosobe deslizando, como quem empurra a foto na página
vira_paginagiro 3D, folha virando
datilografatexto surge letra a letra (Courier)
rasgao papel se rasga revelando o que está atrás
cola_fitao elemento entra e uma fita crepe cai prendendo ele
xeroxaparece com flash de fotocopiadora, estourado depois assenta
carimbabaixa com impacto e leve tremida, tipo carimbo

O que o motor entende de verdade hoje

⚠️ Sete dos oito nomes acima são do modelo, não do motor. A exceção é carimba, que existe de verdade — está em ENTRADAS_BASE, no motor.js:61. Os outros sete não existem em lugar nenhum. E motor.js:69 resolve a entrada assim:

const entradaDe = nome =>
  (window.EFEITOS && window.EFEITOS[nome]) || ENTRADAS_BASE[nome] || ENTRADAS_BASE.aparecer;

Escrever cai_e_quica num roteiro não dá erro — dá um fade sem graça, porque cai no fallback aparecer. É a mesma classe de defeito da fonte que vira Arial: sai errado e sai calado.

O que está implementado em web/mod_efeitos/efeitos.js, com a descrição do próprio arquivo:

EntradaO que faz
papel_abredesdobra na horizontal, escala X 0 → 1 com overshoot
desdobra_cimaa mesma ideia no eixo vertical (papel dobrado ao meio)
quicacai e quica 3 vezes com amortecimento; squash no toque, stretch no ar
gira_entraentra girando de −25°, com overshoot, e assenta
estala1 passo seco, zero interpolação: passo 0 é pose "errada", passo 1 é a final
desliza_ladoentra pela lateral com arrasto e uma freada seca no fim
cresce_pop0 → 1,12 → 1. Carimbo, mas leve
rasga_revelaclip-path com borda irregular de papel rasgado, com empurrão lateral
balanca_entraentra pendurado pelo topo e balança até parar (pêndulo amortecido)
bate_grudaa mão bate o papel na mesa: chega grande, achata e assenta
sobe_encaixasobe de baixo do quadro e encaixa com overshoot curto
pisca_montagemposes discretas, tipo xerox de montagem — cada passo é um quadro
varre_fitaa fita crepe sendo passada, com a ponta levantada, e um tapa no fim
poe_maochega grande, como se a mão pusesse o recorte na frente da câmera
desamassao papel entra amassado e se abre, com tremor que decai
voa_de_longeentra de fora da tela, girando, e pousa

Mais os embutidos de motor.js como fallback: cair, aparecer, cresce_pop e carimba. Campos que qualquer entrada lê do elemento (todos opcionais): seed (semente determinística), dir, dur, altura e passos (passos por segundo do stop-motion).

O terceiro vocabulário: o que o validador aceita

Não são duas listas, são três, e nenhuma bate com a outra. O roteiro/_validar.py tem a própria, e ela é mais curta que a do motor:

VOCAB = {"papel_abre", "quica", "gira_entra", "estala", "desliza_lado", "cresce_pop",
         "rasga_revela", "balanca_entra", "cair", "datilografa", "cascata", "nenhuma"}
ListaOndeQuantos nomes
MODELOroteiro/ROTEIRO_MODELO.json8
motorweb/mod_efeitos/efeitos.js + os embutidos do motor.js16 + 4
validadorroteiro/_validar.py (VOCAB)12

⚠️ Oito nomes que o motor implementa seriam REPROVADOS pelo validador: desdobra_cima, bate_gruda, sobe_encaixa, pisca_montagem, varre_fita, poe_mao, desamassa e voa_de_longe. Eles rodam — o VOCAB é que ficou para trás do gerar_roteiro_v3.py e não os conhece. (Hoje isso é teórico: o validador estoura antes de chegar lá — ver a ressalva no topo desta página.)

E datilografa é o caso contrário, o mais traiçoeiro dos três: está no MODELO e no VOCAB — ou seja, passa por escrito nos dois lugares que um roteirista consulta — e não é implementado em lugar nenhum. Como entrada, cai no mesmo fallback mudo de cai_e_quica. A datilografia da peça existe, mas ela é o motor reescrevendo o innerHTML em função de t a cada frame, não uma entrada chamada datilografa.

Quem for escrever um roteiro animado novo: parta da tabela do motor — é a única das três que descreve o que vai acontecer na tela. Se for rodar o _validar.py depois, saiba que ele reprova oito daqueles nomes e aprova um que não existe. E se precisar de um gesto que não está em efeitos.js, escreva o efeito lá obedecendo ao contrato — não invente nome no JSON esperando que o motor adivinhe.

As regras de composição, e o script que deveria checá-las

roteiro/_validar.py foi escrito para rodar em 1s e sair com código ≠ 0 se alguma regra quebrasse. Hoje ele não chega a validar: estoura KeyError: 'id' na linha 31 contra o JSON que está no disco — ficou para trás do gerar_roteiro_v3.py. Vale como especificação escrita das regras da casa, não como rede viva; até alguém consertar, isto se confere à mão. O que ele cobra: terço superior nunca vazio (amostrado a cada 0,05s) · no máximo 2 blocos de texto simultâneos · texto nunca ultrapassa x=1000 · nada de camada superior cobre texto vivo · 3 a 6 elementos por beat · a ordem de camadas (0 fundo → 10 foto/mapa → 20 recorte → 30 texto → 40 fita → 50 carimbo/doodle).

As larguras de texto não são estimadas: _medir.py renderiza cada string no Chromium com a TTF do projeto e grava o getBoundingClientRect() em _medidas.json. Trocar a fonte invalida as medidas — quem trocar roda o _medir.py de novo.

Áudio

Do LEIA_ME.md:

Narração e trilha saem por MCP (ElevenLabs / ElevenLabs Music / Lyria). Gerações gastam crédito — simulo o custo antes de rodar.

A procedência dos ativos externos da peça está em Acervo de imagens e direitos.


Original completo

_Lab_Video/, na raiz de Documents/Bené/: o LEIA_ME.md, o CREDITOS.md, o roteiro/ROTEIRO_MODELO.json (formato) e o roteiro/MUDANCAS_v2.md (o diário beat a beat da peça "Senado Amigo do Povo", com o antes/depois de cada decisão). O código vive em web/motor.js, mod_efeitos/efeitos.js, montar_v2.py, turbo/render_turbo.py e mixar_v2.py. As ferramentas de imagem estática são outras, e estão em Ferramentas que rodam só na máquina.

Transcrição do vault. Mudou o arquivo lá? Atualize lib/guias.ts — o vault é a fonte, esta página é o espelho.