Communitygithub.com

html-to-video

Renderiza quadros de animações HTML/Canvas em MP4 (com suporte a áudio, GIF e saída com canal alfa). Use quando o usuário disser "fazer vídeo com código", "HTML para vídeo", "exportar MP4/GIF", "criar animação explicativa/data viz" ou "renderização quadro a quadro".

O que é html-to-video?

O `html-to-video` é uma ferramenta de renderização de vídeos baseada em código que transforma animações HTML/Canvas em arquivos MP4, GIF ou MOV com canal alfa (transparência). Diferente de geração por IA, ele desenha cada quadro como uma função pura `frame(t)`, garantindo determinismo, controle preciso sobre cada frame e zero custos de API. Suporta 11 templates prontos para uso, incluindo vídeos explicativos multi-cena, cartões verticais 9:16 (para TikTok/Reels/Shorts), gráficos de barras e linhas, cronogramas animados, cartões de legendas com SRT externo e muito mais. A renderização é feita via Playwright + ffmpeg, com suporte a paralelismo (`--concurrency`), retomada de renderização interrompida (`--resume`), saída com canal alfa (`--alpha`) para sobreposição em vídeos reais, mixagem de áudio automática com ajuste de duração (`--audio-fit`), e múltiplos presets de resolução para redes sociais (TikTok, Instagram, YouTube, etc.). É ideal para agentes de IA automatizarem a produção de vídeos explicativos, demonstrações de produto, animações de dados e peças promocionais sem depender de interfaces gráficas.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/wx4766/html-to-video/tree/main

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

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):

TemplateUsoRenderização
explainer.htmlVí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.htmlAbertura de vídeo: título aparece letra por letra + subtítulo--duration=6
bar-chart.htmlGráfico de barras/ranking: barras crescem uma a uma, números sobem junto--duration=6
line-chart.htmlGráfico de linhas/tendência: linha se desenha + pontos se iluminam + rótulo no último ponto--duration=8
number-counter.htmlNúmero grande/KPI: rola de 0 até o valor alvo + etiqueta de subida/descida--duration=7
timeline.htmlLinha do tempo/fluxograma: eixo central cresce, pontos são ativados um a um--duration=7
vertical-card.htmlCard vertical 9:16: título grande + número grande, para TikTok/Reels/Shorts/Douyin--duration=6 --width=1080 --height=1920
compare.htmlComparação antes/depois: duas colunas lado a lado, fechamento com conclusão--duration=7
flow-steps.htmlAnimação de fluxograma: etapas são ativadas uma a uma, a atual destaca--duration=7
subtitle-card.htmlCartão de legenda fala por fala: uma frase de cada vez, com barra de progresso na parte inferior--duration=8 + --srt
vertical-subtitle.htmlCartã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ávelSignificado1920×10801080×1920
SX = W/1920Tamanho horizontal, margens laterais10.5625
SY = H/1080Tamanho vertical, espaçamento entre linhas11.7778
SF = √(SX·SY)Tamanho da fonte, espessura de linha, raio, desfoque11

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, gerando bad substitution.

Quatro restrições rígidas (já sofridas, não repita)

  1. A versão do playwright deve corresponder ao cache do navegador. playwright-core@X só reconhece o número de build do chromium declarado em seu browsers.json, se não corresponder, exibe "Executable doesn't exist".

    → Não adivinhe a versão, use scripts/install-deps.sh, que executa scripts/detect-env.js para detectar o cache e escolher automaticamente. Detecção isolada: node scripts/detect-env.js (imprime RECOMMEND_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.

  2. 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.

  3. ESM não reconhece NODE_PATH. Em arquivos .mjs, import 'playwright' sempre gera ERR_MODULE_NOT_FOUND. → Scripts auxiliares devem ser escritos em CJS (.cjs / require).

  4. 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âmetroFunção
--duration / --from / --toDuraçã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.0Exporta apenas PNGs nesses momentos, sem codificar vídeo
--fps / --width / --heightTaxa 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,cUma ú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
--jsonstdout imprime apenas uma linha JSON (com output / duration / frames / warnings etc.), logs legíveis vão para stderr. Para uso por agentes
--concurrencyNú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|padO 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-framesManté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
--resumeA renderização parou no meio? Continue, quadros já renderizados são reutilizados. Se os parâmetros forem diferentes, recusa (veja abaixo)
--alphaSaí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
--headedRenderiza com a janela do navegador visível (para debug; concorrência é automaticamente reduzida para 1)
--allow-errorsErros na página não interrompem a renderização (padrão: com erros → saída não-zero)
--keep-chromeNão esconde controles de player (para debug)
--readytimeout=8Tempo (em segundos) para esperar __seek ficar pronto. Aumente para animações com fontes grandes ou muitos dados que demoram a iniciar
--no-serveCarrega 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 .mp4 com 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 .part primeiro, 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ção resulta 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ídaTamanhoUso
H.264 .mp4118 KBVídeo final direto
ProRes 4444 .mov760 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 --duration for 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ão trim termina 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
PresetDimensõesPara quem
douyin / tiktok / vertical1080×1920Douyin, Kuaishou, WeChat Video, Reels, Shorts
xiaohongshu / xhs1080×1440Xiaohongshu 3:4
instagram / ig1080×1350Feed do Instagram 4:5
square1080×1080Quadrado 1:1
bilibili / youtube / landscape1920×1080Horizontal 16:9
  • Use --preset=xiaohongshu para um único formato; --presets=a,b para múltiplos (pelo menos dois; use --preset para 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. --presets garante 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, output vira um array, e presets: [{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.mmm també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 tempo t do quadro atual. Quando --srt é usado, __cues tem prioridade; se não houver, usa CFG.lines como fallback — o mesmo template funciona com ambos.
  • --cue-log gera CSV com frame,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/__ready está 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 --concurrency com cuidado: muitos workers podem consumir muita memória. auto é seguro.
  • Para vídeos muito longos: divida em segmentos com --from/--to e junte com ffmpeg.

Suporte

Relate problemas ou sugestões via Issues no repositório.

Licença

MIT

Habilidades Relacionadas