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):
| Plantilla | Uso | Salida |
|---|---|---|
explainer.html | Video 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.html | Intro de video: título principal aparece letra por letra + subtítulo | --duration=6 |
bar-chart.html | Gráfico de barras / ranking: las barras crecen una tras otra, los valores numéricos se actualizan sincronizadamente | --duration=6 |
line-chart.html | Tendencia de líneas: la línea crece + los puntos de datos se iluminan + flotador en el último punto | --duration=8 |
number-counter.html | Números grandes/KPI: cuenta desde 0 hasta el valor objetivo + etiqueta de subida/bajada | --duration=7 |
timeline.html | Línea de tiempo / flujo: el eje central crece, los nodos se iluminan uno tras otro | --duration=7 |
vertical-card.html | Tarjeta vertical 9:16: título grande + número grande, para Douyin/WeChat Video/Reels | --duration=6 --width=1080 --height=1920 |
compare.html | Comparativa antes/después: dos columnas enfrentadas, barra de conclusión al final | --duration=7 |
flow-steps.html | Animación de diagrama de flujo: los pasos se iluminan uno tras otro, el paso actual se resalta | --duration=7 |
subtitle-card.html | Tarjeta 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.html | Tarjeta 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:
| Variable | Significado | 1920×1080 | 1080×1920 |
|---|---|---|---|
SX = W/1920 | Tamaño horizontal, márgenes izquierdo/derecho | 1 | 0.5625 |
SY = H/1080 | Tamaño vertical, interlineado | 1 | 1.7778 |
SF = √(SX·SY) | Tamaño de fuente, grosor de línea, radio de borde, desenfoque | 1 | 1 |
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 (comoO'Brien),
bash tratará el apóstrofe como el inicio de una comilla y dará un errorbad substitution.
Cuatro restricciones estrictas (las hemos sufrido, evítalas)
-
La versión de Playwright debe coincidir con la caché del navegador.
playwright-core@Xsolo reconoce el número de compilación de chromium que declara en subrowsers.json.
Si no coincide, da error "Executable doesn't exist".→ No adivines la versión, usa
scripts/install-deps.sh, que ejecutascripts/detect-env.jspara detectar la caché y elegir la versión automáticamente.
Detección individual:node scripts/detect-env.js(muestraRECOMMEND_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. -
El ffmpeg que viene con Playwright no sirve.
ms-playwright/ffmpeg-*/ffmpeg-win64.exeestá 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. -
ESM no reconoce
NODE_PATH.
import 'playwright'en un archivo.mjsdará inevitablementeERR_MODULE_NOT_FOUND.
→ Los scripts auxiliares deben escribirse en CJS (.cjs/require). -
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 entornoFFMPEG(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ámetro | Descripción |
|---|---|
--duration / --from / --to | Duració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.0 | Solo exporta PNG en esos momentos, sin codificar |
--fps / --width / --height | Fotogramas 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,c | Un 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 |
--json | stdout 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 |
--concurrency | Nú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|pad | Qué 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-frames | Conserva 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) |
--alpha | Salida 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 |
--headed | Renderiza 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-errors | Los errores de la página no se consideran fallos (por defecto: con errores → salida distinta de cero) |
--keep-chrome | No oculta los controles de reproducción (para depuración) |
--readytimeout=8 | Segundos de espera para que __seek esté listo (se puede aumentar para fuentes grandes o datos grandes que tardan en cargar) |
--no-serve | Carga 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
.mp4con 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
.party 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, sifps / tamaño / rango / modo de renderizadoes 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--resumepara 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):
| Salida | Tamaño | Uso |
|---|---|---|
H.264 .mp4 | 118 KB | Video final listo para enviar |
ProRes 4444 .mov | 760 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
--durationexplí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 defectotrimfinaliza 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
| Preajuste | Tamaño | Para quién |
|---|---|---|
douyin / tiktok / vertical | 1080×1920 | Douyin, Kuaishou, WeChat Video, Reels, Shorts |
xiaohongshu / xhs | 1080×1440 | Xiaohongshu 3:4 |
instagram / ig | 1080×1350 | Feed de IG 4:5 |
square | 1080×1080 | Cuadrado 1:1 |
bilibili / youtube / landscape | 1920×1080 | Horizontal 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.
--presetssolo 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. warningses un array: conflictos de parámetros, discrepancia entre audio y video, nombre de tema desconocido, todo entra aquí. Array vacío = limpio.durationes la duración real del video final (leída por ffmpeg), no el valor planificado.- Con
--presets,outputse convierte en un array, conpresets: [{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énMM:SS.mmm) y JSON ([{start,end,text}]). - Se analiza una vez y se inyecta en
window.__cues. La plantilla consultatdel fotograma actual para decidir qué frase mostrar.
Cuando se usa--srt, la plantilla prioriza__cues; si no existe, recurre aCFG.lines.
Así, la misma plantilla funciona con ambos métodos. --cue-logexporta un CSV conframe,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).