film-engine: películas dibujadas desde código, un fotograma a la vez
Dónde está el kit: ejecuta cada comando de esta skill desde la carpeta
reels-maker(el clon de github.com/thativanng/reels-maker que contienetemplate/ytools/). Si el proyecto no lo tiene, configúralo primero siguiendo el SETUP.md de ese repositorio.
La idea en un párrafo
Una película aquí es una página HTML con una regla: window.seek(t) debe dibujar exactamente el fotograma para el tiempo t, cada vez, a partir de nada más que t. Nada en la imagen puede leer un reloj real. Luego un renderizador avanza t a 30 fps, captura cada fotograma a 2x y lo envía a ffmpeg. Como la página es determinista, un re-renderizado coincide (en promedio con una fracción de nivel; el ruido de la GPU puede mover píxeles individuales hasta unos 60 niveles), un fotograma fijo en 17.3 s es el fotograma que mostrará el video, y cada verificación (film-audit) puede examinar cualquier momento sin reproducir la película.
La ruta (no te saltes ninguna estación)
- Brief y look primero. reel-intake termina en un brief firmado y un storyboard (formato, duración, sonido, qué nunca aparece); reel-style termina en un look elegido. No abras los pasos de construcción de esta skill sin ambos.
- Storyboard. reel-intake lo escribe (tiempo · palabras en pantalla · sonido y subtítulo · por qué; references/brief.md), y la persona lo firma antes de que construyas. Un estilo con banner añade una columna de banner.
- Tira de prueba. Construye unos primeros 10 s en el look elegido (una película de estilo caminata puede necesitar 15–30 s para mostrar un capítulo), toma fotogramas fijos, obtén un sí. Una construcción completa sobre un look no firmado es trabajo desperdiciado.
- Construye la película completa (el pack de estilo dice cómo construir escenas).
- Puertas (film-audit): la verificación de colisiones a 40 fps, las sondas de píxeles, luego un auditor independiente. Nada llega a la persona antes de esto.
- Renderiza:
node tools/render.mjs --page reels/<name>/page.html --out reels/<name>/out/film-silent.mp4(fotogramas a 2x, escalados con lanczos, sin etiquetas de color). - Audio:
python3 template/audio.py out/film.wav(una lista de señales en el mismo reloj que la película). - Finaliza:
zsh tools/mux.sh out/film-silent.mp4 out/film.wav out/film.mp4. Esto da −14 LUFS, un limitador, un archivo H.264 sin etiquetas y una vista previa para teléfono del mismo tamaño que la película (lado corto 720 px). - Portada:
node tools/cover.mjs --page reels/<name>/page.html --out reels/<name>/out/cover.jpg. Una página con modo portada (el?coverdel starter, desde configcover) dibuja el fotograma diseñado; cualquier otra página da el fotograma enT=. Se guarda en la forma de la película y se mantiene por debajo de 2 MB, más el recorte central 3:4 para una película 9:16 (la cuadrícula de Instagram, informada). LinkedIn toma una portada solo al publicar: después, solo se puede editar el texto de la publicación (Ayuda de LinkedIn, "Editar una publicación compartida", https://www.linkedin.com/help/linkedin/answer/a522811). Su especificación de miniatura, JPG o PNG en la forma del video y como máximo 2 MB, proviene de la ayuda de LinkedIn según lo citado por un resumen de búsqueda; la página en sí no cargaría, así que trátalo como no verificado. - Envía primero la vista previa para teléfono, luego el archivo completo y la portada.
El contrato de la página
window.seek(T): dibuja el fotograma T. Es el único lugar que anima algo.window.ready: una promesa que se resuelve después de fuentes, texturas y el primer fotograma.window.DUR: la duración de la película en segundos.window.FILM = { w, h, format, safe }: el tamaño del fotograma y la zona de seguridad en píxeles. Cada herramienta dimensiona su ventana a partir de esto, así que la página posee su formato.- Opcional:
window.cover()dibuja la portada en lugar de un fotograma, yFILM.coverAtnombra el momento en el que se asienta (tools/cover.mjs usa ambos; sin ellos toma el fotograma enT=). window.collide(T)ywindow._dbg: ganchos para film-audit (ver esa skill).- Las herramientas sirven la carpeta del kit sobre http por su cuenta (los módulos ES no cargan desde
file://). Para ver una página en vivo en un navegador,npm run servey abre http://127.0.0.1:8765/template/starter.html.
Formatos, el starter y otros idiomas
template/formats.jsoncontiene los cuatro tamaños (9:16 1080×1920 · 4:5 1080×1350 · 1:1 1080×1080 · 16:9 1920×1080), el mapa plataforma → formato, y la zona de seguridad de cada plataforma con su fuente. La línea de formato del brief lo elige.- Cada reel vive en su propia carpeta,
reels/<name>/(ignorada por git): su brief, storyboard,page.html(una copia del starter o de film.html), sureel.jsonocareer.json, y suout/. Las páginas cargan fuentes y formats.json desde la raíz del kit, por lo que una copia funciona desde allí. Apunta las herramientas hacia él:PAGE=reels/<name>/page.html. template/starter.html+template/reel.jsones el esqueleto para todo look excepto la caminata voxel. Sus palabras y tiempos viven en la configuración, se dimensiona desde?fmt=o la configuración, y mantiene cada palabra dentro del núcleo de seguridad de la plataforma.?guidesmuestra las zonas cubiertas en rojo, solo para verificar; nunca renderices con ello activado. Es un esqueleto, no un look: reel-style reescribe sus tokens y diseño.template/film.html(la plantilla voxel) es solo 4:5, y opta por no usar zonas de seguridad (safees 0): su banner y subtítulos están cerca de los bordes por diseño, por lo que la sondasafesiempre pasa allí y el ojo del auditor es la verificación. Sus cámaras están ajustadas para 4:5; otra relación significa reajustarlas y volver a ejecutar cada puerta.- Chino, japonés, coreano: el starter carga Noto Sans TC desde Google Fonts al renderizar (solo los caracteres que usa la configuración), y ejecuta texto CJK a aproximadamente 0.72× del tamaño latino (sus caracteres son un em completo). Escribe
\ndonde una línea debe romperse: el chino se rompe entre dos caracteres cualesquiera, por lo que una línea larga necesita una rotura que elijas.template/reel.zh.jsones un ejemplo trabajado. Cualquier otro script necesita su propia fuente en la pila.
Relojes y temporización
- Un reloj de película impulsa las escenas, la cámara, el banner, los subtítulos y el sonido. Cuando insertes un tiempo después sin re-temporizar todo, usa un re-mapeo: el reloj de la escena se pausa en el punto de inserción mientras las superposiciones mantienen el reloj real:
El script de audio necesita la misma tabla: escribe unconst INS = [[24.0, 5.0, 'detail']]; // en tiempo de escena 24.0, reproduce un tiempo insertado de 5 s function remap(T) { let sh = 0; for (const [a, len, id] of INS) { if (T < a + sh) return { t: T - sh, ins: null }; if (T < a + sh + len) return { t: a, ins: id, u: T - sh - a, len }; sh += len; } return { t: T - sh, ins: null }; }RT(t)que suma las longitudes insertadas a un tiempo de escena, y coloca cada señal enRT(t), o cada señal después del inserto se desvía. (La plantilla no tiene insertos, por lo que su audio.py no necesita uno.) - Suavizado:
eiopara movimientos de cámara,eopara cosas que llegan,trap(velocidad trapezoidal) para caminar y conducir, para que un objeto en movimiento no "chasquee" en los extremos. - Fotos de cámara son absolutas
A(pos, target)o de seguimientoF(offset, targetOffset)desde el punto de tierra del héroe. Mezcla entre fotos coneio. Mantén un momento que importe; un plano que se mueve todo el tiempo se lee como nervioso.
Las superposiciones (en DOM, sobre el canvas)
- Banner: año, luego empresa, luego la píldora del rol, más una línea de tiempo cuyo punto respira sobre el año actual. El banner cambia cuando la nueva escena está EN PANTALLA (su conjunto ha subido, el atuendo ha cambiado, el héroe ha llegado), nunca cuando el código comienza a construirlo. Verifica esto en una hoja de fotogramas a ±1 s alrededor de cada cambio.
- Subtítulos: una línea de historia por tiempo, con la frase clave en negrita y acento. Usa
text-wrap: balance. Dale al lector aproximadamente 3 palabras por segundo, y desvanece un subtítulo antes de que un movimiento rápido de cámara descubra al sujeto detrás de él. - Un globo de diálogo que sigue a un personaje: proyecta el punto de la cabeza y fija el globo ≥24 px dentro del fotograma.
- La tarjeta final: nombre, un eslogan, en qué son buenos, una cita. La escena se desenfoca detrás.
Audio
- Las señales se escriben en el reloj de la película, junto al momento al que pertenecen (pasos por pierna de caminata, un timbre cuando algo aparece, un golpe cuando aterriza).
- Sin sonidos de "whoosh" en las transiciones; se leen como baratos. Usa timbres, tics e impactos suaves.
- La base por defecto en
template/audio.pyse genera a partir de ondas senoidales, por lo que es gratis de usar en cualquier lugar. Si alguien trae una pista generada por IA, verifica su licencia: algunos pesos de modelos solo permiten uso no comercial. - Volumen: normalización de dos pasos loudnorm a −14 LUFS (medir, luego aplicar linealmente), luego un limitador. Un solo paso se excede.
Hechos de renderizado que nos costaron días (más en references/gotchas.md)
- Renderiza a 2x y escala hacia abajo. EffectComposer elimina el anti-aliasing del canvas, por lo que un renderizado a 1x tiene bordes dentados por todas partes.
- Sin etiquetas de color en el MP4. Una tubería JPEG lo etiqueta como rango completo, y algunos reproductores desplazan entonces los colores.
setparams=...=unknownen el filtro. - Dos caras en un mismo plano parpadean (z-fighting). Dale a la frontal un desplazamiento de profundidad.
- Las fuentes deben cargarse (
document.fonts.load) antes de dibujar cualquier texto en el canvas, o se recurre silenciosamente a una alternativa. - GPU activada en Chromium sin cabeza (
--use-angle=metalen macOS). Sin ella, un renderizado es aproximadamente 10x más lento.
Trabajar sin un prompt en cada paso
La persona dijo sí a las reglas de permiso del kit una vez (SETUP.md paso 4); mantente dentro de ellas, o se les preguntará una y otra vez:
- Escribe y cambia archivos con las herramientas de escritura y edición: páginas, reel.json, brief, storyboard, AUDIT.md. No los generes con un script de Python o shell, un heredoc o
sed -i; cada uno de esos es un prompt de aprobación. - Mide con las herramientas del kit (stills, probe, sheet, collide, render, cover), no con fragmentos de
python3 -c. - No se necesita
mkdir, bucles ni cadenas: cada herramienta crea su propia carpeta de salida, y toma una lista de tiempos en una sola llamada (stills.mjs … 1.5,6,12). - Los scripts propios de un reel (su script de sonido, un helper) viven en
reels/<name>/y se ejecutan comopython3 reels/<name>/audio.pyonode reels/<name>/x.mjs, lo cual las reglas cubren.
Comandos
Ejecuta cada herramienta como un solo comando simple (flags, sin VAR=… al inicio, sin bucles, sin cadenas &&). Así, las reglas de permiso de la persona (node tools/*) lo cubren y no se les pide aprobar cada paso. Las variables de entorno siguen funcionando para los scripts.
npm run doctor # verifica esta máquina primero
npm run demo # la prueba de 12 segundos: out/demo/
node tools/stills.mjs "2,8.5,15" # fotogramas fijos a 2x → out/stills (la plantilla voxel)
node tools/stills.mjs --page template/starter.html --format 16:9 1.6 # el starter en otra relación
node tools/stills.mjs --page template/starter.html --cfg reel.zh.json 1.6 # otra configuración (aquí: chino)
node tools/stills.mjs --dsf 1 --out out/chk 2,8.5 # fotogramas rápidos a 1x para una hoja
python3 tools/sheet.py out/chk/sheet.jpg 6 300 out/chk/t*.jpg
node tools/render.mjs --page reels/<name>/page.html --out reels/<name>/out/film-silent.mp4
python3 template/audio.py out/film.wav
zsh tools/mux.sh out/film-silent.mp4 out/film.wav out/film.mp4