html-to-video · código quadro a quadro para vídeos
O que é isso
Não é geração de vídeo por IA, é desenhar cada quadro com código:
Escreva HTML/Canvas, crie a cena como uma função pura frame(t)
→ navegador headless busca quadro a quadro em t, captura screenshot
→ ffmpeg compila em H.264 MP4
Legendas / movimentos de câmera / transições tudo no código, altere e re-renderize
Vantagens: determinismo (mesma entrada = mesma saída), controle absoluto sobre cada quadro, texto chinês perfeito, custo zero. Ideal para: animações explicativas, gráficos dinâmicos, demonstrações de produtos, visualizações de dados animadas, vinhetas. Não serve para: cenas com aparência real/cinematográfica (use VideoGen).
O que você pode fazer (11 templates em assets/templates/ só trocando o texto):
| Template | Uso | Renderização |
|---|---|---|
explainer.html | Vídeo explicativo multi-cena: abertura / pessoa+balão / pontos / gráfico de barras / encerramento, cenas com transições, aceita JSON de cenas; na horizontal, quando há 3 pontos, eles são organizados em 3 colunas; vertical (1080×1920) tem layout próprio | --duration=17.6 |
title-card.html | Abertura de vídeo: título aparece letra por letra + subtítulo | --duration=6 |
bar-chart.html | Gráfico de barras/ranking: barras crescem uma a uma, números sobem junto | --duration=6 |
line-chart.html | Gráfico de linhas/tendência: linha se desenha + pontos se iluminam + rótulo no último ponto | --duration=8 |
number-counter.html | Número grande/KPI: rola de 0 até o valor alvo + etiqueta de subida/descida | --duration=7 |
timeline.html | Linha do tempo/fluxograma: eixo central cresce, pontos são ativados um a um | --duration=7 |
vertical-card.html | Card vertical 9:16: título grande + número grande, para TikTok/Reels/Shorts/Douyin | --duration=6 --width=1080 --height=1920 |
compare.html | Comparação antes/depois: duas colunas lado a lado, fechamento com conclusão | --duration=7 |
flow-steps.html | Animação de fluxograma: etapas são ativadas uma a uma, a atual destaca | --duration=7 |
subtitle-card.html | Cartão de legenda fala por fala: uma frase de cada vez, com barra de progresso na parte inferior | --duration=8 + --srt |
vertical-subtitle.html | Cartão de legenda vertical: destaque de palavras-chave, layout nativo 9:16 | --duration=8 --width=1080 --height=1920 + --srt |
Cada template é autocontido em um único arquivo, com um bloco CFG visível no topo; altere texto/dados — não precisa saber canvas para gerar vídeos. Todos os 11 templates suportam troca de tema (veja seção "Trocar esquema de cores"). Para criar algo diferente, veja assets/animation-skeleton.html e comece do zero.
Faça "um vídeo" e não "uma animação": use assets/lib/scenes.js para múltiplas cenas + transições, a tabela de cenas pode ser externa com --story=shots.json — LLM escreve JSON mais confiavelmente do que centenas de linhas de canvas. Veja seção "Múltiplas cenas e transições".
Dimensões: os templates acompanham a viewport, sem necessidade de ajustes manuais
Os 11 templates usam window.innerWidth/innerHeight para definir o canvas, portanto qualquer dimensão funciona sem cortes:
# Horizontal 1080p (padrão)
bash assets/render.sh title-card.html --duration=6 --fps=60
# Vertical 9:16
bash assets/render.sh title-card.html --duration=6 --fps=60 --width=1080 --height=1920
# 720p / 4K igual
bash assets/render.sh title-card.html --duration=6 --fps=60 --width=1280 --height=720
Internamente, os templates usam três escalas para mapear o design ao canvas real:
| Variável | Significado | 1920×1080 | 1080×1920 |
|---|---|---|---|
SX = W/1920 | Tamanho horizontal, margens laterais | 1 | 0.5625 |
SY = H/1080 | Tamanho vertical, espaçamento entre linhas | 1 | 1.7778 |
SF = √(SX·SY) | Tamanho da fonte, espessura de linha, raio, desfoque | 1 | 1 |
O tamanho da fonte usa proporção geométrica (SF) em vez de seguir a largura — ao mudar para vertical, as letras não ficam menores, o layout apenas se expande. Em 1920×1080, todos os três são iguais a 1, então a saída padrão é pixel a pixel idêntica ao antigo (PSNR = inf).
Suas próprias animações devem seguir isso, caso contrário, mudar --width/--height cortará silenciosamente o canto inferior direito:
const cv = document.getElementById('stage');
cv.width = window.innerWidth; // ← não hardcode 1920
cv.height = window.innerHeight; // ← não hardcode 1080
Agora o renderizador mede ativamente o tamanho do canvas uma vez e avisa claramente se for menor que a viewport (não mais fica quieto gerando imagens pela metade).
Instalação das dependências (uma vez apenas)
O renderizador precisa de um workspace Node (contém playwright + ffmpeg completo). Um comando resolve:
bash scripts/install-deps.sh # workspace padrão ~/.workbuddy/binaries/node/workspace
bash scripts/install-deps.sh /path/to/workspace # ou especifique o caminho
Ele detecta automaticamente o cache do navegador local, escolhe a versão do playwright compatível, instala o ffmpeg completo e grava o caminho do workspace em ~/.workbuddy/.html2video-env. Depois disso, render.sh / smoke.sh / qa.sh funcionam sem configuração.
Instalação manual também é possível (workspace precisa conter playwright + @ffmpeg-installer/ffmpeg), e então crie o arquivo de configuração:
# ~/.workbuddy/.html2video-env
WB_NODE_WORKSPACE="<caminho absoluto do seu workspace node>"
Você também pode usar uma variável de ambiente temporária:
export WB_NODE_WORKSPACE="<caminho>". Não coloque o caminho dentro de${VAR:-default}— se o nome de usuário contiver apóstrofo (ex.:O'Brien), o bash interpretará o apóstrofo como início de aspas, gerandobad substitution.
Quatro restrições rígidas (já sofridas, não repita)
-
A versão do playwright deve corresponder ao cache do navegador.
playwright-core@Xsó reconhece o número de build do chromium declarado em seubrowsers.json, se não corresponder, exibe "Executable doesn't exist".→ Não adivinhe a versão, use
scripts/install-deps.sh, que executascripts/detect-env.jspara detectar o cache e escolher automaticamente. Detecção isolada:node scripts/detect-env.js(imprimeRECOMMEND_PW=...). Mapeamento conhecido: build 1148→1.49.1, 1155→1.50.1, 1169→1.52.0, 1187→1.55.0, 1208→1.58.0.Nota: em algumas redes, o download do chromium pode ser interrompido repetidamente (aparecem diretórios corrompidos no cache). Nesse caso, priorize reutilizar o cache existente em vez de atualizar — é para isso que serve o script de detecção.
-
O ffmpeg que vem com o playwright não serve.
ms-playwright/ffmpeg-*/ffmpeg-win64.exeé compilado com--disable-everything, só tem codec VP8 + contêiner webm, sem H.264, sem GIF, nem mesmo decodificação PNG. Use o ffmpeg completo do@ffmpeg-installer/ffmpeg. -
ESM não reconhece
NODE_PATH. Em arquivos.mjs,import 'playwright'sempre geraERR_MODULE_NOT_FOUND. → Scripts auxiliares devem ser escritos em CJS (.cjs/require). -
PATH não é confiável. Especialmente no Git Bash / Windows, caminhos no estilo Windows colocados no PATH podem ser corrompidos pela conversão. → Não dependa do PATH para o ffmpeg, use a variável de ambiente
FFMPEG(o renderizador já suporta).
Fluxo de renderização
Passo 0 (o mais fácil): escolha um template e troque o texto
cp assets/templates/bar-chart.html my-video.html
# Abra my-video.html, altere apenas title / data / note no CFG no topo
bash assets/render.sh my-video.html --duration=6 --fps=60
Os templates têm consciência de área segura para legendas: o texto não cai nos 15% inferiores da tela (onde as legendas queimadas no vídeo podem sobrepor). Ao criar seu layout, siga a mesma regra.
Passo 1: escreva a animação HTML (quando os templates não forem suficientes)
Use assets/animation-skeleton.html como esqueleto. Contrato principal (o renderizador depende dele):
// Devem ser expostos:
window.__seek = (t) => { /* redesenhe completamente o quadro no tempo t */ };
window.__ready = true; // sinal de prontidão
// O renderizador injeta window.__seekRender = true,
// nesse caso, você deve **congelar o clock interno** e deixar apenas o __seek externo avançar (garantindo determinismo)
if (window.__seekRender) { renderAt(0); window.__ready = true; }
else { /* pré-visualização no navegador: loop auto-impulsionado com requestAnimationFrame */ }
Regra de ferro: cada quadro = função pura do tempo t. Não dependa do estado do "quadro anterior",
não use Math.random() / Date.now() / performance.now() — isso faria a mesma entrada gerar saídas diferentes,
e a verificação de determinismo no QA acenderia vermelho, além de workers paralelos renderizarem coisas diferentes.
O canvas é a saída única: <canvas width=1920 height=1080>, CSS com left:0; top:0,
body com margin:0. O renderizador usa clip:{x:0,y:0,width:W,height:H} para capturar o canto superior esquerdo, portanto o canvas deve preencher exatamente a viewport, sem centralização.
(O renderizador também suporta animações DOM/CSS/SVG, desde que exponha __seek — mas note: animações DOM sofrem reflow a cada quadro, sendo mais lentas que canvas puro.)
Nomes de classe proibidos (o CSS injetado pelo renderizador os esconde incondicionalmente, sumindo do vídeo):
.no-record .progress .progress-bar .counter .tCur .phases .phase-label .phase .replay .masthead .kicker .title .footer
Além disso, não coloque <button> em elementos com position:fixed/sticky, pois regras heurísticas os esconderão como controles de player.
(Para garantir que um elemento apareça no vídeo final, adicione data-record-keep="true" para escapar da ocultação heurística.)
Fontes para chinês: '"Microsoft YaHei", "PingFang SC", system-ui, sans-serif'; monoespaçada: 'ui-monospace, Consolas, monospace'.
Passo 2: Teste de fumaça (não pule)
Capture apenas alguns quadros primeiro para confirmar que o contrato está pronto, sem erros no console e sem imagens borradas, antes de renderizar tudo:
bash assets/smoke.sh animation.html
# SMOKE_T="0,1,2" SMOKE_W=1280 SMOKE_H=720 bash assets/smoke.sh animation.html
Uma forma mais rápida de iterar: renderize apenas um pequeno trecho, em segundos você vê o efeito dinâmico.
bash assets/render.sh animation.html --from=3 --to=5 --fps=30 # só renderiza de 3s a 5s
bash assets/render.sh animation.html --stills=1,3,5 # exporta apenas três quadros estáticos
Passo 3: Renderização completa
bash assets/render.sh animation.html --duration=12 --fps=60 --width=1920 --height=1080 --concurrency=4
Parâmetros comuns:
| Parâmetro | Função |
|---|---|
--duration / --from / --to | Duração / renderizar apenas um trecho (segundos). Quando um intervalo é dado, o nome do arquivo vira nome.inicio-fim.mp4, não sobrescreve o vídeo completo |
--stills=0.5,2.0 | Exporta apenas PNGs nesses momentos, sem codificar vídeo |
--fps / --width / --height | Taxa de quadros e resolução |
--preset=<nome> | Tamanhos predefinidos por plataforma: douyin / tiktok / vertical (1080×1920), xiaohongshu (1080×1440), instagram (1080×1350), square (1080×1080), bilibili / youtube / landscape (1920×1080) |
--presets=a,b,c | Uma única renderização gera todos os formatos, um vídeo por proporção (nome com sufixo -nomeDaProporção). Use para publicação multiplataforma |
--theme=<nome> | Troca de tema (midnight / paper / neon), sobrescreve CFG.theme no template sem alterar o arquivo. Nome errado gera aviso claro e usa midnight como fallback |
--out=<arquivo> | Especifica o caminho do vídeo final (padrão: mesmo diretório, mesmo nome). Use ao gerar múltiplas cores do mesmo HTML para não sobrescrever. Se o diretório pai não existir, será criado automaticamente |
--json | stdout imprime apenas uma linha JSON (com output / duration / frames / warnings etc.), logs legíveis vão para stderr. Para uso por agentes |
--concurrency | Número de workers paralelos (cada worker = uma página Chromium, cuidado com memória). Use auto para escolha baseada em CPU e memória |
--audio=<file> | Mixa áudio automaticamente após a codificação (o stream de vídeo é copiado diretamente, sem recodificação). Se --duration não for fornecido, a duração do vídeo é determinada pelo áudio (arredondando para o quadro inteiro, sem cortar o final do áudio) |
--audio-fit=trim|pad | O que fazer quando a duração do áudio e do vídeo não coincidem: trim termina com o áudio (padrão) / pad adiciona silêncio ao áudio, mantendo a duração do vídeo |
--gop=<segundos> | Intervalo entre keyframes, ex.: --gop=1 para um keyframe por segundo, facilitando busca por segundo na pós-produção |
--keep-frames | Mantém os PNGs de cada quadro mesmo em caso de sucesso (para recodificar com CRF diferente sem precisar recapturar) |
--tmpdir=<diretório> | Onde armazenar os PNGs temporários (padrão: próximo ao HTML). 1080p por minuto ~ 2.4 GB, aponte para outro disco se o disco do sistema não tiver espaço |
--resume | A renderização parou no meio? Continue, quadros já renderizados são reutilizados. Se os parâmetros forem diferentes, recusa (veja abaixo) |
--alpha | Saída com fundo transparente (ProRes 4444 .mov), para sobrepor em vídeos reais / gerados por IA, veja abaixo |
--srt=<file> | Timeline de legendas externa (SRT / VTT / JSON), injeta em window.__cues, veja abaixo. Parâmetro sinônimo: --cues |
--cue-log=<file> | Exporta CSV frame,t,cue_index,cue_start,cue_end para verificar o alinhamento das legendas |
--story=<file> | JSON da tabela de cenas, injeta em window.__story, substitui todo o roteiro (veja "Múltiplas cenas e transições") |
--scene-log=<file> | Exporta CSV da timeline de cenas; se a duração total das cenas não bater com --duration, gera aviso |
--gif[=fps,largura] | Após renderizar, gera também um GIF (padrão 15fps / largura 960), com otimização de palette em duas etapas, sem recapturar quadros |
--poster=<segundos> | Após renderizar, extrai um quadro estático .jpg para capa, para publicação em sites/redes sociais |
--batch=<file.json> | Uma lista de tarefas renderizadas em sequência, com resumo final; falha individual não interrompe as já concluídas, mas o código de saída geral é não-zero |
--headed | Renderiza com a janela do navegador visível (para debug; concorrência é automaticamente reduzida para 1) |
--allow-errors | Erros na página não interrompem a renderização (padrão: com erros → saída não-zero) |
--keep-chrome | Não esconde controles de player (para debug) |
--readytimeout=8 | Tempo (em segundos) para esperar __seek ficar pronto. Aumente para animações com fontes grandes ou muitos dados que demoram a iniciar |
--no-serve | Carrega a página via file:// (padrão: usa servidor http local, veja abaixo) |
Projetos ESM / fetch funcionam diretamente. O renderizador inicia um servidor http local vinculado a 127.0.0.1 para carregar a página —
com file://, <script type="module"> e fetch() são bloqueados por CORS, impossibilitando animações organizadas com Vite/esbuild ou leitura de JSON local;
com http, tudo funciona. No início da renderização, o endereço é impresso; cole no navegador para ver o console e depurar.
Se o servidor não iniciar, o renderizador cai para file:// com um aviso (apenas aviso, não falha).
- A saída é um
.mp4com o mesmo nome do HTML - Total de quadros =
fps × duração. 12s@60fps = 720 quadros, leva ~6 a 7 minutos (concurrency=4) - Codificação leva ~1 minuto (crf 18 / preset medium)
Se houver erros na página ao final, a saída é não-zero (mas o vídeo é gerado mesmo assim, para conferência).
Se a renderização parar no meio, não comece do zero
Um vídeo longo (60fps × 30s = 1800 quadros) parar aos 80% é frustrante. Adicione --resume para continuar:
bash assets/render.sh animation.html --fps=60 --duration=30 --resume
# ▸ resume: 1432 quadros já reutilizados, faltam 368
Três regras, todas para garantir que o vídeo retomado seja idêntico ao que seria renderizado de uma vez:
- Quadros são escritos como
.partprimeiro, depois renomeados. Se o processo for morto, aquele quadro não deixa um PNG parcial como "completo", então os quadros reutilizados são genuinamente renderizados. - Parâmetros diferentes são recusados diretamente. Se antes era 30fps e agora 60fps, os dois lotes de quadros seriam misturados na codificação, resultando em um vídeo com ritmo inconsistente sem erros — então qualquer diferença em
fps / dimensão / intervalo / modo de renderizaçãoresulta em saída imediata com mensagem "anterior vs atual", deixando você decidir se apaga ou não. - Os números dos quadros devem ser contínuos. Se faltar um quadro, o ffmpeg silenciosamente termina o vídeo mais cedo, deixando-o mais curto sem aviso. Por isso, antes da codificação, cada número no intervalo
[início,fim)é verificado; se houver lacuna, o renderizador avisa e sugere usar--resume.
O diretório temporário fica fixo ao lado do HTML: .seek-tmp-resume/ (sem --resume, ainda é um diretório por renderização, sem interferência entre concorrentes).
Após a renderização bem-sucedida, ele é limpo automaticamente; se quiser mantê-lo, use --keep-frames.
Isso é intencional: erros na página geralmente significam "tela preta / metade do conteúdo faltando"; versões anteriores silenciosamente reportavam sucesso até você revisar os quadros — agora o erro é exibido antes da codificação e o resultado é marcado como falha.
Saída com transparência: sobreponha camadas de texto em vídeos de terceiros
Fundo gerado por IA + camada de texto gerada por código é a combinação mais econômica atualmente (modelos de IA borram texto, código desenha texto com precisão de pixel).
--alpha é para isso:
bash assets/render.sh animation.html --duration=6 --fps=30 --alpha
# → animation.mov (ProRes 4444 / yuva444p10le, fundo transparente)
O template precisa cooperar: ao ler window.__transparent como verdadeiro, deve pular a renderização do fundo.
Atualmente, title-card, compare, flow-steps, subtitle-card, vertical-subtitle,
explainer já estão prontos; basta adicionar --alpha para obter transparência;
os outros 5 são gráficos de tela cheia (barras / linhas / números grandes / linha do tempo / card vertical) que precisam de fundo, então --alpha resultará em um vídeo opaco.
Para suas animações, siga este padrão:
const TRANSPARENT = !!window.__transparent;
function drawFrame(t) {
g.clearRect(0, 0, W, H);
if (!TRANSPARENT) {
// Fundo / gradiente de tela cheia / reflexos — tudo isso deve ser pulado
g.fillStyle = '#080A10';
g.fillRect(0, 0, W, H);
}
// Texto, gráficos são desenhados normalmente
}
Reflexos semi-transparentes de tela cheia também devem ser pulados. Mesmo com apenas 5% de opacidade, sobrepostos a um vídeo real, eles clareiam toda a região, e o editor pensará "este material está sujo". Testes mostram: pulando apenas o fundo, mas mantendo os reflexos, os cantos têm alpha=12 (não 0); pulando tudo, os cantos têm alpha=0, e apenas 3.7% dos pixels são opacos (apenas texto).
Custo de tamanho (960×540 / 2s / 20 quadros):
| Saída | Tamanho | Uso |
|---|---|---|
H.264 .mp4 | 118 KB | Vídeo final direto |
ProRes 4444 .mov | 760 KB (6.5×) | Arraste para software de edição e sobreponha |
Apenas ProRes, sem VP9. Testamos --pix_fmt yuva420p + -auto-alt-ref 0, o arquivo aumentou de 7.8KB para 16.8KB (dados alfa foram escritos), mas o Chromium reproduziu com alpha zero — até o texto ficou transparente;
tentamos lossless, bitrate fixo, -lag-in-frames 0, tudo igual. Como não podemos provar que está correto, não oferecemos como opção. Para material transparente pequeno, use ProRes, importe no software de edição e exporte para webm — essa é a tarefa deles.
Alinhamento de áudio: primeiro o som, depois a duração do vídeo
O fluxo real é "escreva o roteiro → grave o áudio → crie as imagens", e não fixar 6 segundos e tentar encaixar o áudio.
Portanto, quando --audio é fornecido sem --duration, o vídeo segue a duração do áudio:
bash assets/render.sh title-card.html --audio=voice.m4a --story=shots.json
# ▸ audio: áudio 12.48s, --duration não especificado → vídeo com duração do áudio (12.5s, arredondado para o quadro inteiro)
- Arredondamento para o quadro inteiro, não arredondamento matemático: meio quadro cortaria o final da última palavra.
- Se
--durationfor explicitamente fornecido e a diferença para o áudio for maior que 0.35s, um aviso claro é exibido, informando a diferença e qual lado é afetado. Versões antigas geravam silenciosamente um vídeo curto — a imagem sumia no meio, mas o log só dizia✓ Done. --audio-fit=pad: se o áudio for mais curto que o vídeo, adiciona silêncio, preservando todo o vídeo (padrãotrimtermina com o áudio).
Nota: com trim, o vídeo final não será exatamente igual à duração do áudio. O stream de vídeo é copiado com -c:v copy,
o ffmpeg só pode terminar nos limites dos pacotes de quadro; testes mostram ~0.3–0.5s extras. Para alinhamento em milissegundos, seria necessário recodificar,
o que troca qualidade por precisão — não vale a pena, já que alguns quadros extras no final geralmente não são perceptíveis.
Um vídeo, múltiplas plataformas: uma única renderização para todos os formatos
O mesmo vídeo para Douyin, Xiaohongshu, Bilibili: no software de edição, seria uma sequência por formato, exportando e monitorando cada um. Aqui, o layout é todo parametrizado, então renderizar de novo é apenas mais um loop:
bash assets/render.sh bar-chart.html --duration=8 \
--presets=douyin,xiaohongshu,square --out=dist/chart.mp4
# → dist/chart-douyin.mp4 1080×1920
# dist/chart-xiaohongshu.mp4 1080×1440
# dist/chart-square.mp4 1080×1080
| Preset | Dimensões | Para quem |
|---|---|---|
douyin / tiktok / vertical | 1080×1920 | Douyin, Kuaishou, WeChat Video, Reels, Shorts |
xiaohongshu / xhs | 1080×1440 | Xiaohongshu 3:4 |
instagram / ig | 1080×1350 | Feed do Instagram 4:5 |
square | 1080×1080 | Quadrado 1:1 |
bilibili / youtube / landscape | 1920×1080 | Horizontal 16:9 |
- Use
--preset=xiaohongshupara um único formato;--presets=a,bpara múltiplos (pelo menos dois; use--presetpara um). - Nome de preset errado falha imediatamente com lista de nomes disponíveis — não renderiza silenciosamente em 1920×1080 com formato errado.
- Se o layout aguenta é outra história: os templates se adaptam à viewport, mas 9:16 e 16:9 têm densidades de informação diferentes,
e em 3:4 a fonte pode parecer grande / o conteúdo apertado — é preciso verificar.
--presetsgarante as dimensões, não a composição bonita. - O espaço em branco na parte inferior do formato vertical é intencional, não é erro de layout: a barra de progresso e botões da plataforma ficam ali, e os templates deixam 15% de área segura para legendas queimadas.
- É mutuamente exclusivo com
--stills(um gera múltiplos vídeos, outro só captura quadros); não pode ser aninhado com--batch.
Para agentes: --json
O principal usuário desta ferramenta é um agente de IA. Humanos leem ✓ Done: xxx.mp4 (1.2 MB · 30fps · 12s) sem problemas,
agentes precisam usar regex para adivinhar — e caminhos com espaços, chinês, parênteses quebram. --json estrutura a saída:
bash assets/render.sh title-card.html --duration=6 --json
{"ok":true,"exitCode":0,"input":"…/title-card.html","output":"…/title-card.mp4",
"width":1920,"height":1080,"fps":60,"duration":6,"frames":360,"bytes":2411724,
"gif":null,"poster":null,"warnings":[],"mode":"video","theme":null,"preset":null}
- stdout tem apenas esta linha, progresso e logs legíveis vão para stderr — basta fazer
JSON.parse(stdout)diretamente. warningsé um array: conflitos de parâmetros, diferença entre áudio e vídeo, tema desconhecido entram aqui. Array vazio = limpo.durationé a duração real do vídeo final (lida pelo ffmpeg), não o valor planejado.- Com
--presets,outputvira um array, epresets: [{name,width,height,output,ok,bytes}]é incluído.
Legendas com timeline externa: áudio mudou, legendas acompanham
Escrever as legendas no CFG.lines do template é o mais fácil, mas quando o áudio é gravado, os tempos precisam seguir o áudio —
não dá para recontar segundos manualmente a cada alteração. Por isso, suportamos arquivos de legenda externos:
bash assets/render.sh subtitle-card.html --duration=8 --srt=voice.srt --cue-log=cues.csv
- Suporta SRT (
HH:MM:SS,mmm), WebVTT (MM:SS.mmmtambém funciona) e JSON ([{start,end,text}]). - O arquivo é lido uma vez e injetado em
window.__cues; o template consulta qual legenda mostrar com base no tempotdo quadro atual. Quando--srté usado,__cuestem prioridade; se não houver, usaCFG.linescomo fallback — o mesmo template funciona com ambos. --cue-loggera CSV comframe,t,cue_index,cue_start,cue_end. Isso transforma "a legenda está no tempo certo?" em um valor calculável: subtraia o tempo do primeiro quadro da legenda do início do SRT para obter o erro. Testes com 4 legendas / 80 quadros mostraram erro de 0 ms, e a cobertura de quadros corresponde exatamente ao teórico.
O template precisa de apenas duas linhas para suportar:
const cues = (window.__cues && window.__cues.length) ? window.__cues : CFG.lines;
const cur = cues.find(c => t >= c.start && t < c.end) || cues[cues.length - 1];
Não coloque legendas nos 15% inferiores da tela. Essa é a área da barra de progresso / botões de curtir das plataformas — escrever ali é desperdício.
O qa.js alerta sobre "legendas na área de segurança" e os quatro novos templates verticais já respeitam essa margem.
Troca de cores: mude um campo, sem tocar no código do canvas
assets/templates/themes.js tem três paletas, todos os 11 templates estão integrados. Duas formas de trocar:
# ① Pela linha de comando (recomendado): sem alterar nenhum arquivo, uma renderização por tema
bash assets/render.sh bar-chart.html --duration=6 --theme=paper --out=bar-paper.mp4
# ② No arquivo: defina fixo no CFG no topo do template
const CFG = {
theme: 'paper', // midnight (padrão, escuro) | paper (claro) | neon (preto e roxo neon)
...
};
A linha de comando tem prioridade sobre CFG.theme (o renderizador injeta window.__h2vTheme, o template lê h2vTheme(window.__h2vTheme || CFG.theme)). Assim, o mesmo HTML pode gerar três esquemas de cores em lote:
[{ "html": "bar-chart.html", "out": "dist/bar-midnight.mp4", "theme": "midnight" },
{ "html": "bar-chart.html", "out": "dist/bar-paper.mp4", "theme": "paper" },
{ "html": "bar-chart.html", "out": "dist/bar-neon.mp4", "theme": "neon" }]
Com --batch, você renderiza um lote de uma vez (veja seção "Renderização em lote").
O tema é mais que cor de fundo — inclui cor do texto, cor de destaque, opacidade, saturação de gradiente, e cor de sombra de texto. Veja os efeitos em cada template no diretório.
Múltiplas cenas e transições
O template explainer usa assets/lib/scenes.js para múltiplas cenas:
// scenes.js expõe uma função que gera tabela de cenas a partir de dados
const shots = buildShots({ /* dados */ });
// Cada cena: { start, end, draw: (t) => { /* desenha a cena */ } }
Para mudar o roteiro sem alterar o código, use --story=shots.json:
[
{ "start": 0, "end": 4, "title": "Abertura", "subtitle": "Bem-vindo" },
{ "start": 4, "end": 10, "title": "Problema", "items": ["Pontos","Explicação"] },
{ "start": 10, "end": 17.6, "title": "Conclusão" }
]
O template faz a lógica de transição (fade, slide, zoom) e o --scene-log exporta a timeline para conferência.
Otimizações e boas práticas
Aceleração: tabela de cenas externa + renderização paralela
Para vídeos com muitas cenas, organize a tabela como JSON e use --story + --concurrency=auto. O renderizador
detecta CPU e memória e escolhe um número adequado de workers. Cada worker renderiza uma faixa de quadros,
e o ffmpeg junta tudo.
Controle de qualidade: verificação automática de consistência
O script qa.sh executa verificações:
bash assets/qa.sh animation.html --duration=12 --fps=60
Ele verifica:
- Se o contrato
__seek/__readyestá exposto - Se não há erros no console
- Se o canvas tem as dimensões corretas
- Se o resultado é determinista (duas renderizações com mesma entrada produzem saídas idênticas)
Renderização em lote (--batch)
Para produzir múltiplos vídeos em sequência:
bash assets/render.sh --batch=batch.json
O formato do batch.json:
[
{ "html": "explainer.html", "duration": 17.6, "theme": "midnight", "out": "explainer-midnight.mp4" },
{ "html": "bar-chart.html", "duration": 6, "theme": "paper", "out": "bar-paper.mp4", "fps": 30 },
{ "html": "title-card.html", "duration": 6, "width": 1080, "height": 1920, "out": "title-vertical.mp4" }
]
Cada tarefa pode ter parâmetros diferentes. Se uma falhar, as outras continuam, e um resumo é impresso no final. O código de saída é não-zero se alguma tarefa falhar.
Detalhes técnicos
Estrutura do diretório
html-to-video/
├── assets/
│ ├── templates/ # 11 templates prontos
│ ├── lib/ # Bibliotecas (scenes.js, themes.js)
│ ├── render.sh # Script principal de renderização
│ ├── smoke.sh # Teste de fumaça
│ └── qa.sh # Verificação de qualidade
├── scripts/
│ ├── install-deps.sh # Instala dependências
│ └── detect-env.js # Detecta ambiente
└── README.md # Este arquivo
Dicas de desempenho
- Canvas puro > DOM: animações canvas são mais rápidas e consistentes que DOM/CSS.
- Evite
Math.random(): para consistência, use um gerador de números pseudoaleatórios com semente fixa. - Mantenha a lógica de desenho pequena: cada quadro deve ser desenhado em < 16ms para 60fps.
- Use
--concurrencycom cuidado: muitos workers podem consumir muita memória.autoé seguro. - Para vídeos muito longos: divida em segmentos com
--from/--toe junte com ffmpeg.
Suporte
Relate problemas ou sugestões via Issues no repositório.
Licença
MIT