Communitygithub.com

html-to-video

Convierte animaciones HTML/Canvas en videos MP4 fotograma a fotograma, con soporte para música, GIF y capas transparentes. Ideal para crear videos explicativos, gráficos animados, presentaciones y más.

¿Qué es html-to-video?

Esta herramienta convierte animaciones HTML/Canvas en videos MP4, GIF o con capas transparentes (ProRes 4444) fotograma a fotograma. No es un generador de video por IA, sino un renderizador determinista que utiliza un navegador sin cabeza para capturar cada fotograma de una función `frame(t)` pura y luego los codifica con ffmpeg. Viene con 11 plantillas listas para usar (explicadores, gráficos de barras/líneas, líneas de tiempo, tarjetas verticales 9:16, comparativas, etc.) que se pueden personalizar cambiando palabras clave en la sección `CFG` del archivo HTML. Soporta múltiples relaciones de aspecto (p. ej., TikTok, Xiaohongshu, YouTube), subtítulos externos (SRT/VTT/JSON), pistas de audio, tablas de tomas JSON, y paralelización para una renderización más rápida. Es especialmente útil para agentes de IA, ya que mediante el flag `--json` devuelve un resultado estructurado que se puede analizar fácilmente. Está diseñado para ser usado en entornos de línea de comandos y como parte de flujos de trabajo automatizados con LLMs.

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

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

html-to-video · Haz videos fotograma a fotograma con código

¿Qué es esto?

No es un generador de video con IA, es dibujar videos fotograma a fotograma con código:

Escribe HTML/Canvas, crea la escena como una función pura frame(t)
   → El navegador sin cabeza busca el fotograma t, captura la pantalla
   → ffmpeg lo codifica como H.264 MP4
Los subtítulos, movimientos de cámara y transiciones están en el código. Modifica el código y vuelve a renderizar.

Ventajas: Determinista (misma entrada = misma salida), control absoluto sobre cada fotograma, texto en chino perfecto, costo cero.
Ideal para: animaciones explicativas, gráficos dinámicos, demostraciones de productos, visualizaciones de datos animadas, créditos de presentación.
No es adecuado para: tomas fotorrealistas o ambientes (para eso está VideoGen).

¿Qué puedes hacer con esto? (11 plantillas en assets/templates/ listas para usar, solo cambia el texto):

PlantillaUsoSalida
explainer.htmlVideo explicativo multi-toma: intro / persona+burbuja / puntos / comparativa de barras / cierre, con transiciones entre tomas, se le puede pasar una tabla JSON de tomas; en horizontal con exactamente tres puntos clave se alinean automáticamente en tres columnas, en vertical (1080×1920) tiene otro diseño--duration=17.6
title-card.htmlIntro de video: título principal aparece letra por letra + subtítulo--duration=6
bar-chart.htmlGráfico de barras / ranking: las barras crecen una tras otra, los valores numéricos se actualizan sincronizadamente--duration=6
line-chart.htmlTendencia de líneas: la línea crece + los puntos de datos se iluminan + flotador en el último punto--duration=8
number-counter.htmlNúmeros grandes/KPI: cuenta desde 0 hasta el valor objetivo + etiqueta de subida/bajada--duration=7
timeline.htmlLínea de tiempo / flujo: el eje central crece, los nodos se iluminan uno tras otro--duration=7
vertical-card.htmlTarjeta vertical 9:16: título grande + número grande, para Douyin/WeChat Video/Reels--duration=6 --width=1080 --height=1920
compare.htmlComparativa antes/después: dos columnas enfrentadas, barra de conclusión al final--duration=7
flow-steps.htmlAnimación de diagrama de flujo: los pasos se iluminan uno tras otro, el paso actual se resalta--duration=7
subtitle-card.htmlTarjeta de explicación frase por frase: muestra una frase a la vez, con barra de progreso en la parte inferior--duration=8 + --srt
vertical-subtitle.htmlTarjeta de subtítulos vertical: resalta palabras clave, diseño nativo 9:16--duration=8 --width=1080 --height=1920 + --srt

Cada plantilla es un solo archivo autónomo con un bloque de configuración CFG visible en la parte superior. Cambia el texto y los datos y ya puedes renderizar.
No necesitas saber Canvas para usarlas. Las 11 plantillas soportan cambio de tema (consulta la sección "Cambiar combinación de colores").
Si quieres hacer algo más, mira assets/animation-skeleton.html para empezar desde cero.

Crea "un video" en lugar de "una animación": usa assets/lib/scenes.js para escribir varias tomas + transiciones.
La tabla de tomas también se puede externalizar con --story=shots.json — los LLM escriben JSON de forma más fiable que cientos de líneas de código Canvas.
Ver más abajo la sección "Múltiples tomas y transiciones".

Tamaño: las plantillas se adaptan a la ventana gráfica, no necesitas modificar nada

Las 11 plantillas configuran el lienzo según window.innerWidth/innerHeight, por lo que funcionan con cualquier tamaño sin recortar:

# Horizontal 1080p (por defecto)
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, las plantillas usan tres escalas para ajustar el diseño al lienzo real:

VariableSignificado1920×10801080×1920
SX = W/1920Tamaño horizontal, márgenes izquierdo/derecho10.5625
SY = H/1080Tamaño vertical, interlineado11.7778
SF = √(SX·SY)Tamaño de fuente, grosor de línea, radio de borde, desenfoque11

El tamaño de fuente usa el área proporcional (SF) en lugar de seguir el ancho — al cambiar a vertical, el texto no se hace más pequeño, solo se expande el diseño.
En 1920×1080, los tres valores son 1, por lo que la salida por defecto es idéntica píxel a píxel a la versión anterior (PSNR = inf en pruebas reales).

Si escribes tus propias animaciones, sigue esto, de lo contrario al cambiar --width/--height se recortará en silencio la esquina inferior derecha:

const cv = document.getElementById('stage');
cv.width  = window.innerWidth;      // ← No pongas 1920 fijo
cv.height = window.innerHeight;     // ← No pongas 1080 fijo

Ahora el renderizador mide activamente el tamaño del lienzo una vez; si es menor que la ventana gráfica, muestra una advertencia clara (ya no produce media imagen en silencio).

Instala las dependencias primero (solo una vez)

El renderizador necesita un espacio de trabajo Node (que contiene playwright + ffmpeg completo). Un solo comando lo hace:

bash scripts/install-deps.sh                       # El espacio de trabajo por defecto es ~/.workbuddy/binaries/node/workspace
bash scripts/install-deps.sh /path/to/workspace    # O especifica uno propio

Detecta automáticamente la caché del navegador de tu máquina, elige la versión de playwright compatible, instala ffmpeg completo
y escribe la ruta del espacio de trabajo en ~/.workbuddy/.html2video-env. Después, render.sh / smoke.sh / qa.sh funcionan de inmediato.

También puedes instalar manualmente (el espacio de trabajo debe contener playwright + @ffmpeg-installer/ffmpeg) y luego configurarlo tú mismo:

# ~/.workbuddy/.html2video-env
WB_NODE_WORKSPACE="<ruta absoluta a tu espacio de trabajo node>"

También puedes usar una variable de entorno temporal: export WB_NODE_WORKSPACE="<ruta>".
No escribas la ruta dentro de ${VAR:-valor por defecto} — si el nombre de usuario contiene apóstrofes (como O'Brien),
bash tratará el apóstrofe como el inicio de una comilla y dará un error bad substitution.

Cuatro restricciones estrictas (las hemos sufrido, evítalas)

  1. La versión de Playwright debe coincidir con la caché del navegador.
    playwright-core@X solo reconoce el número de compilación de chromium que declara en su browsers.json.
    Si no coincide, da error "Executable doesn't exist".

    → No adivines la versión, usa scripts/install-deps.sh, que ejecuta scripts/detect-env.js para detectar la caché y elegir la versión automáticamente.
    Detección individual: node scripts/detect-env.js (muestra RECOMMEND_PW=...).
    Mapeos conocidos: build 1148→1.49.1, 1155→1.50.1, 1169→1.52.0, 1187→1.55.0, 1208→1.58.0.

    Nota: En algunos entornos de red, la descarga de chromium puede interrumpirse repetidamente (se manifiesta como directorios incompletos en la carpeta de caché).
    En ese caso, prioriza reutilizar la caché existente en lugar de actualizar — esa es la razón de ser del script de detección.

  2. El ffmpeg que viene con Playwright no sirve.
    ms-playwright/ffmpeg-*/ffmpeg-win64.exe está compilado con --disable-everything,
    solo tiene codificación VP8 + contenedor webm, no tiene H.264, ni GIF, ni siquiera decodificación PNG.
    Debes usar la versión completa de @ffmpeg-installer/ffmpeg.

  3. ESM no reconoce NODE_PATH.
    import 'playwright' en un archivo .mjs dará inevitablemente ERR_MODULE_NOT_FOUND.
    → Los scripts auxiliares deben escribirse en CJS (.cjs / require).

  4. PATH no es fiable.
    Especialmente en Git Bash / Windows, las rutas de estilo Windows insertadas en PATH pueden ser malinterpretadas por la conversión.
    → No uses PATH para pasar ffmpeg, usa la variable de entorno FFMPEG (el renderizador ya la soporta).

Flujo de trabajo

Paso 0 (la forma más fácil): Elige una plantilla y cambia el texto

cp assets/templates/bar-chart.html my-video.html
# Abre my-video.html, solo modifica title / data / note en el bloque CFG de la parte superior
bash assets/render.sh my-video.html --duration=6 --fps=60

Las plantillas tienen conciencia de la zona segura de subtítulos: el texto no caerá en el 15% inferior de la pantalla (se cubriría al grabar los subtítulos).
Al diseñar tu propio diseño, sigue esta regla.

Paso 1: Escribe el HTML de la animación (cuando las plantillas no son suficientes)

Usa assets/animation-skeleton.html como esqueleto. Contrato principal (el renderizador depende de esto):

// Debes exponer estos dos
window.__seek = (t) => { /* Redibuja completamente el fotograma en el momento t */ };
window.__ready = true;                 // Señal de listo

// El renderizador inyectará window.__seekRender = true,
// en ese momento debes **congelar el reloj interno**, solo el __seek externo avanza los fotogramas (garantiza determinismo)
if (window.__seekRender) { renderAt(0); window.__ready = true; }
else { /* Vista previa en navegador: bucle auto-guiado con requestAnimationFrame */ }

Regla de hierro: cada fotograma = función pura del tiempo t.
No dependas del estado del "fotograma anterior",
no uses Math.random() / Date.now() / performance.now() — eso haría que la misma entrada dé diferentes salidas,
el control de calidad de "determinismo" se encendería en rojo, y los múltiples workers en paralelo renderizarían cada uno por su cuenta.

El lienzo es la única salida: <canvas width=1920 height=1080>, con CSS left:0; top:0,
body margin:0. El renderizador usa clip:{x:0,y:0,width:W,height:H} para capturar la esquina superior izquierda,
así que el lienzo debe ocupar exactamente toda la ventana gráfica, no estar centrado.

(El renderizador también soporta animaciones DOM/CSS/SVG, solo necesitas exponer __seek — pero ten en cuenta:
las animaciones DOM requieren reflow en cada fotograma, son más lentas; el canvas puro suele ser más estable y rápido.)

Nombres de clase prohibidos (el CSS inyectado por el renderizador los ocultará incondicionalmente, los elementos desaparecerán):
.no-record .progress .progress-bar .counter .tCur .phases .phase-label .phase .replay .masthead .kicker .title .footer
Además, no pongas <button> dentro de elementos con position:fixed/sticky, serán ocultados por reglas heurísticas como controles de reproducción.
(Si quieres que un elemento aparezca en el video final, agrégale data-record-keep="true" para eximirlo de la ocultación heurística.)

Fuentes chinas: '"Microsoft YaHei", "PingFang SC", system-ui, sans-serif'; monoespaciada: 'ui-monospace, Consolas, monospace'.

Paso 2: Prueba de humo (no te la saltes)

Primero captura solo unos pocos fotogramas para confirmar que el contrato está listo, no hay errores en la consola y la imagen no está borrosa. Luego ejecuta la renderización completa:

bash assets/smoke.sh animation.html
# SMOKE_T="0,1,2" SMOKE_W=1280 SMOKE_H=720 bash assets/smoke.sh animation.html

Una forma más rápida de iterar: renderiza solo un segmento corto, en pocos segundos verás el efecto dinámico.

bash assets/render.sh animation.html --from=3 --to=5 --fps=30   # Solo renderiza de 3s a 5s
bash assets/render.sh animation.html --stills=1,3,5            # Solo exporta tres fotogramas fijos

Paso 3: Renderización completa

bash assets/render.sh animation.html --duration=12 --fps=60 --width=1920 --height=1080 --concurrency=4

Parámetros comunes:

ParámetroDescripción
--duration / --from / --toDuración / Renderiza solo un segmento (segundos). Cuando se da un rango, el nombre de salida se convierte en nombre.inicio-fin.mp4, no sobrescribe el archivo final
--stills=0.5,2.0Solo exporta PNG en esos momentos, sin codificar
--fps / --width / --heightFotogramas por segundo y resolución
--preset=<nombre>Tamaño predefinido por plataforma: douyin / tiktok / vertical (1080×1920), xiaohongshu (1080×1440), instagram (1080×1350), square (1080×1080), bilibili / youtube / landscape (1920×1080)
--presets=a,b,cUn solo comando para todas las proporciones, cada proporción produce un video final (el nombre del archivo se añade automáticamente con el sufijo -nombre de proporción). Útil para publicación multiplataforma
--theme=<nombre>Cambia el tema (midnight / paper / neon), sobrescribe CFG.theme en la plantilla, sin necesidad de modificar el archivo. Si el nombre está mal, se muestra una advertencia clara y se revierte a midnight
--out=<archivo>Especifica la ruta del video final (por defecto, mismo directorio con el mismo nombre). Útil cuando un mismo HTML produce varios esquemas de color, de lo contrario el último sobrescribirá al anterior; si el directorio padre no existe, se crea automáticamente
--jsonstdout solo muestra una línea de resultado JSON (output / duration / frames / warnings, etc.), los registros legibles para humanos van a stderr. Para que los agentes lo analicen
--concurrencyNúmero de workers paralelos (cada worker usa una página de Chromium, ten en cuenta la memoria). Con auto elige según CPU y memoria
--audio=<archivo>Mezcla el audio después de la codificación (el flujo de video se copia directamente, sin recodificar). Si no se da --duration, la duración del video se ajusta automáticamente a la longitud del audio (redondeando al fotograma completo, sin recortar el final del sonido)
--audio-fit=trim|padQué hacer cuando la duración del audio y el video no coinciden: trim finaliza con la pista de audio (por defecto) / pad agrega silencio al audio, manteniendo la duración del video
--gop=<segundos>Intervalo de fotogramas clave, por ejemplo --gop=1 un fotograma clave por segundo, facilita la búsqueda por segundo en la edición posterior
--keep-framesConserva los PNG de cada fotograma incluso si tiene éxito (útil para recodificar con CRF diferente sin tener que volver a capturar)
--tmpdir=<directorio>Disco donde se almacenan los PNG de cada fotograma (por defecto, junto al HTML). Un minuto a 1080p ocupa aproximadamente 2.4 GB, si el disco del sistema no tiene espacio, apunta a otro disco
--resume¿La renderización anterior se interrumpió a la mitad? Reanuda, los fotogramas ya renderizados se reutilizan directamente (si los parámetros son diferentes a los anteriores, se rechazará, ver más abajo)
--alphaSalida con fondo transparente (ProRes 4444 .mov), para superponer sobre imágenes reales / generadas por IA, ver más abajo
--srt=<archivo>Línea de tiempo de subtítulos externa (SRT / VTT / JSON), inyecta window.__cues, ver más abajo. Parámetro sinónimo --cues
--cue-log=<archivo>Exporta CSV frame,t,cue_index,cue_start,cue_end para verificar si los subtítulos están sincronizados con el eje de tiempo
--story=<archivo>Tabla de tomas JSON, inyecta window.__story, reemplaza todo el guión (ver "Múltiples tomas y transiciones")
--scene-log=<archivo>Exporta CSV del eje de tiempo de las tomas; si la duración total de la tabla de tomas no coincide con --duration, se muestra una advertencia
--gif[=fps,ancho]Después de la salida, genera un GIF (por defecto 15fps / ancho 960), optimización de paleta en dos etapas, sin rerenderizar
--poster=<segundos>Después de la salida, extrae un fotograma fijo de portada .jpg, para sitios/redes sociales
--batch=<archivo.json>Una serie de tareas se renderizan en orden, al final se genera una tabla resumen; si una falla, no afecta a las ya renderizadas, pero la salida general es distinta de cero
--headedRenderiza con la ventana del navegador visible (para depuración; la concurrencia se reduce automáticamente a 1 para evitar que aparezcan múltiples ventanas)
--allow-errorsLos errores de la página no se consideran fallos (por defecto: con errores → salida distinta de cero)
--keep-chromeNo oculta los controles de reproducción (para depuración)
--readytimeout=8Segundos de espera para que __seek esté listo (se puede aumentar para fuentes grandes o datos grandes que tardan en cargar)
--no-serveCarga la página con file:// (por defecto usa un servidor http local, ver más abajo)

Los proyectos ESM / fetch se pueden renderizar directamente. El renderizador, por defecto, inicia un servidor http local que solo se vincula a 127.0.0.1 para cargar la página.
Con file://, <script type="module"> y fetch() serán bloqueados por CORS.
Las animaciones organizadas con Vite/esbuild o que lean archivos JSON locales no funcionarían.
Usando http, estos problemas no existen. Al inicio de la renderización se imprime esta dirección,
que puedes pegar directamente en el navegador para ver la consola y depurar.
Si el servidor no puede iniciarse, se revierte automáticamente a file:// y muestra un aviso (solo advertencia, no se considera fallo).

  • La salida es un archivo .mp4 con el mismo nombre que el HTML
  • Número de fotogramas = fps × duración. 12s@60fps = 720 fotogramas, aproximadamente 6~7 minutos (concurrencia=4)
  • Codificación aproximadamente 1 minuto (crf 18 / preset medium)

Si hay errores en la página al finalizar la renderización, la salida será distinta de cero (pero el video final se genera igual, para que puedas verificarlo).

Si la renderización se interrumpe a la mitad, no empieces desde cero

Un video largo (60fps × 30s = 1800 fotogramas) que se interrumpe al 80% es frustrante si hay que empezar de nuevo. Añade --resume para continuar:

bash assets/render.sh animation.html --fps=60 --duration=30 --resume
# ▸ resume: se han reutilizado 1432 fotogramas, faltan 368 por renderizar

Tres reglas, todas para que "el video continuado sea idéntico al renderizado de una sola vez":

  • Los fotogramas se escriben primero como .part y luego se renombran.
    El fotograma que se interrumpió no dejará un PNG a medias que se haga pasar por "completado",
    por lo que los fotogramas reutilizados son los que realmente se dibujaron.
  • Si los parámetros cambian, se rechaza directamente.
    Si antes era 30fps y ahora 60fps, se mezclarían dos lotes de fotogramas,
    la consistencia del video se perdería sin error.
    Por lo tanto, si fps / tamaño / rango / modo de renderizado es diferente, se sale y muestra "anterior vs actual"
    para que decidas si eliminar o no.
  • Los números de fotograma deben ser consecutivos.
    Si falta uno, ffmpeg finalizará silenciosamente antes, acortando el video.
    Por lo tanto, antes de codificar, se verifica uno por uno en el rango [inicio, fin).
    Si hay un hueco, se avisa y se sugiere añadir --resume para completar.

El directorio temporal se fija al lado del HTML como .seek-tmp-resume/ (sin --resume sigue siendo un directorio único,
las renderizaciones concurrentes no interfieren entre sí).
Se elimina automáticamente al finalizar; si quieres conservarlo, usa --keep-frames.
Esto es intencionado: los errores de página a menudo significan "pantalla completamente negra / falta media pantalla".
Versiones anteriores lo reportaban como éxito hasta que revisabas los fotogramas.
Ahora muestra el error antes de la codificación y lo marca como fallo al final.

Salida transparente: superpone capas de texto sobre imágenes de otros

Fondo generado por IA + capa de texto generada por código es la combinación más económica actualmente
(los modelos difuminan el texto, el código lo dibuja con precisión de píxel).
--alpha está diseñado para este camino:

bash assets/render.sh animation.html --duration=6 --fps=30 --alpha
# → animation.mov (ProRes 4444 / yuva444p10le, fondo transparente)

La plantilla debe cooperar con una cosa: al leer window.__transparent como verdadero, omitir el dibujo de la capa de fondo.
Actualmente, title-card, compare, flow-steps, subtitle-card, vertical-subtitle y explainer
(6 plantillas) ya están escritas así; solo añade --alpha para tener efecto transparente.
Las otras 5 son gráficos a pantalla completa (barras / líneas / números grandes / línea de tiempo / tarjeta vertical),
que necesitan un color de fondo, por lo que con --alpha solo obtendrías un video opaco.
Si escribes tu propia animación, sigue este patrón:

const TRANSPARENT = !!window.__transparent;
function drawFrame(t) {
  g.clearRect(0, 0, W, H);
  if (!TRANSPARENT) {
    // Fondo / degradado a pantalla completa / destellos de luz — todo esto debe omitirse
    g.fillStyle = '#080A10';
    g.fillRect(0, 0, W, H);
  }
  // Texto, gráficos se dibujan normalmente
}

Los destellos de luz semitransparentes a pantalla completa también deben omitirse.
Aunque tengan solo un 5% de opacidad, al superponerse sobre una imagen real blanquearán toda el área,
y el editor pensará "este material está sucio".
Prueba real: al omitir solo el fondo pero mantener los destellos, las esquinas tenían alpha=12 (no 0);
al omitir todo, las esquinas tenían alpha=0, solo el 3.7% de los píxeles eran opacos (solo texto).

Costo en tamaño (960×540 / 2s / 20 fotogramas en pruebas):

SalidaTamañoUso
H.264 .mp4118 KBVideo final listo para enviar
ProRes 4444 .mov760 KB (6.5×)Arrástralo al software de edición para superponer

Solo ProRes, no VP9.
Probamos --pix_fmt yuva420p + -auto-alt-ref 0, el archivo aumentó de 7.8KB
a 16.8KB (los datos alpha se escribieron), pero Chromium reprodujo todo el fotograma con alpha=0 —
incluso el texto se volvió transparente.
Probamos lossless, tasa de bits fija, -lag-in-frames 0, todo igual.
Como no podemos demostrar que sea correcto, no lo publicamos como opción.
Para materiales transparentes de tamaño pequeño, usa ProRes, impórtalo al software de edición y luego exporta como webm; esa es otra tarea.

Sincronización de audio: primero el sonido, luego la duración del video

El flujo de trabajo real es "escribir guión → grabar audio → ajustar video", no fijar 6 segundos primero y luego intentar meter el audio.
Por lo tanto, cuando se da --audio sin --duration, el video se ajusta a la longitud del audio:

bash assets/render.sh title-card.html --audio=voice.m4a --story=shots.json
# ▸ audio: audio 12.48s, no se especificó --duration → el video se ajusta a la duración del audio (12.5s, redondeado al fotograma superior)
  • Redondeo hacia arriba al fotograma completo, no redondeo: medio fotograma menos eliminaría el sonido final de la última palabra.
  • Si se da --duration explícitamente y la diferencia con el audio supera los 0.35s, se muestra una advertencia clara
    indicando cuánto es la diferencia y qué lado se ve afectado.
    En versiones anteriores, esto producía silenciosamente un video corto: la segunda mitad de la imagen desaparecía por completo,
    pero el registro solo mostraba ✓ Done.
  • --audio-fit=pad: si el audio es más corto que el video, añade silencio para mantener toda la imagen (por defecto trim finaliza con la pista de audio).

Nota: con trim, el video final no será exactamente igual a la duración del audio.
El flujo de video se copia directamente con -c:v copy, ffmpeg solo puede finalizar en el límite del paquete de fotogramas.
En pruebas, se quedan 0.3–0.5s adicionales. Para una alineación de milisegundos, habría que recodificar,
lo que intercambia calidad por precisión, no vale la pena — esos fotogramas extra al final suelen pasar desapercibidos.

Una publicación, múltiples plataformas: un solo comando para todas las proporciones

El mismo video para Douyin, Xiaohongshu, Bilibili.
En el software de edición, necesitas una secuencia por proporción y exportar vigilando cada una.
Aquí el diseño es paramétrico desde el principio, renderizar una vez más es solo un bucle más:

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
PreajusteTamañoPara quién
douyin / tiktok / vertical1080×1920Douyin, Kuaishou, WeChat Video, Reels, Shorts
xiaohongshu / xhs1080×1440Xiaohongshu 3:4
instagram / ig1080×1350Feed de IG 4:5
square1080×1080Cuadrado 1:1
bilibili / youtube / landscape1920×1080Horizontal 16:9
  • Para una sola proporción, usa --preset=xiaohongshu; para múltiples, --presets=a,b (al menos dos, si solo quieres una, usa --preset).
  • Si el nombre del preajuste está mal, falla directamente y muestra los nombres disponibles — no renderizará silenciosamente una versión con proporción incorrecta en 1920×1080.
  • Que el diseño aguante es otra cuestión: las plantillas se adaptan a la ventana gráfica, pero la densidad de información de 9:16 y 16:9 es diferente.
    En 3:4, el texto puede ser demasiado grande / el contenido demasiado apretado; debes revisarlo tú mismo.
    --presets solo garantiza el tamaño, no que la composición sea bonita.
  • El espacio en blanco en la parte inferior de las verticales es intencionado, no es un error de diseño:
    las barras de progreso y los botones de las plataformas se colocan allí,
    las plantillas dejan un 15% de zona segura para los subtítulos grabados.
  • Es mutuamente excluyente con --stills (uno produce múltiples videos, el otro solo captura fotogramas);
    no se puede anidar con --batch.

Para uso con agentes: --json

El principal usuario de esta herramienta son los agentes de IA.
Leer ✓ Done: xxx.mp4 (1.2 MB · 30fps · 12s) está bien para humanos,
pero un agente solo puede adivinarlo con expresiones regulares — con espacios, chino o paréntesis en la ruta, falla.
--json estructura el resultado:

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 solo contiene esta línea, el progreso y los registros legibles para humanos van a stderr.
    JSON.parse(stdout) directamente.
  • warnings es un array: conflictos de parámetros, discrepancia entre audio y video, nombre de tema desconocido, todo entra aquí. Array vacío = limpio.
  • duration es la duración real del video final (leída por ffmpeg), no el valor planificado.
  • Con --presets, output se convierte en un array, con presets: [{name,width,height,output,ok,bytes}] adicional.

Subtítulos con línea de tiempo externa: el audio cambia, los subtítulos lo siguen

Escribir los subtítulos en CFG.lines de la plantilla es lo más sencillo,
pero una vez que hay que sincronizar con el audio, los puntos de tiempo deben seguir al audio —
no puedes contar los segundos manualmente cada vez que cambias una frase.
Por eso, se admite pasar directamente un archivo de subtítulos:

bash assets/render.sh subtitle-card.html --duration=8 --srt=voice.srt --cue-log=cues.csv
  • Soporta SRT (HH:MM:SS,mmm), WebVTT (también MM:SS.mmm) y JSON ([{start,end,text}]).
  • Se analiza una vez y se inyecta en window.__cues. La plantilla consulta t del fotograma actual para decidir qué frase mostrar.
    Cuando se usa --srt, la plantilla prioriza __cues; si no existe, recurre a CFG.lines.
    Así, la misma plantilla funciona con ambos métodos.
  • --cue-log exporta un CSV con frame,t,cue_index,cue_start,cue_end.
    Convierte "¿los subtítulos están sincronizados?" en un número calculable:
    el tiempo del primer fotograma de cada frase menos el inicio del SRT da el error.
    En pruebas, 4 frases / 80 fotogramas, error 0 ms cada una, el número de fotogramas cubiertos coincide exactamente con el valor teórico.

La plantilla solo necesita dos líneas para soportarlo:

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];

Los subtítulos no deben invadir el 15% inferior.
Esa es el área de la barra de progreso / botón de "me gusta" de cada plataforma; escribir allí es en vano.
qa.js mostrará una advertencia de "zona de subtítulos". Los diseños verticales de las cuatro nuevas plantillas ya tienen este margen de seguridad.

Cambiar combinación de colores: un campo, sin tocar el código del lienzo

assets/templates/themes.js contiene tres paletas de colores, accesibles en las 11 plantillas. Dos formas de cambiarlas:

# ① Desde la línea de comandos (recomendado): sin modificar ningún archivo, un tema por renderización
bash assets/render.sh bar-chart.html --duration=6 --theme=paper --out=bar-paper.mp4
# ② Modificando el archivo: escribiendo el valor en CFG de la plantilla
const CFG = {
  theme: 'paper',   // midnight (por defecto, oscuro) | paper (claro) | neon (neón negro-púrpura)
  ...
};

La línea de comandos tiene prioridad sobre CFG.theme (el renderizador inyecta window.__h2vTheme, la plantilla lee
h2vTheme(window.__h2vTheme || CFG.theme)). Así, un mismo HTML puede producir las tres combinaciones de color en lote:

[{ "h

---

## Preguntas frecuentes

### ¿Se puede usar con cualquier HTML?

Sí, siempre que el HTML exponga `window.__seek(t)` y `window.__ready`.  
El renderizador es agnóstico al contenido; solo necesita estas dos funciones para funcionar.

### ¿Qué resoluciones soporta?

Cualquier resolución. Las plantillas se adaptan automáticamente a la ventana gráfica.  
Puedes especificar `--width` y `--height` para cualquier tamaño.

### ¿Se puede usar en CI/CD?

Sí. `--json` y `--batch` están diseñados para automatización.  
El código de salida es 0 en éxito y distinto de cero en fallo, compatible con pipelines de CI.

### ¿Cómo mejorar el rendimiento?

Aumenta `--concurrency` hasta el número de núcleos de CPU.  
Usa `--presets` para renderizar múltiples formatos de una sola pasada.  
Reduce `--fps` si no necesitas 60 fps (30 fps suele ser suficiente para la mayoría de los casos).

Skills relacionados