Site Reels: sitio → MP4 → deck
Requisitos
| Necesario para | Qué | Verificar |
|---|---|---|
| todo | Windows, macOS o Linux con GPU y escritorio visible (Chrome corre sin headless) | n/a |
| todo | Node.js 18+ | node -v |
| todo | ffmpeg + ffprobe en PATH (o FFMPEG=<ruta completa>) | ffmpeg -version |
| todo | Chrome, Chromium o Edge (detección automática, ver abajo) | node -e "import('./scripts/find-chrome.mjs').then(m=>console.log(m.findChrome()))" |
record.mjs | puppeteer-core + puppeteer-screen-recorder (npm, en el proyecto) | npm ls puppeteer-core puppeteer-screen-recorder |
snap.mjs | puppeteer-core + timesnap (incluye timeweb) | npm ls timesnap |
| showreel | Proyecto Remotion | npx remotion --version |
| auditorías (opcional) | ImageMagick | magick -version |
| Subida a Figma | Figma MCP (use_figma, upload_assets) + skills figma-use, figma-use-slides | Lista de herramientas MCP |
Detección de Chrome (scripts/find-chrome.mjs, usado por record.mjs y snap.mjs):
Variable de entorno CHROME si está definida; si no, Chrome/Edge instalado (Windows: Program Files y LocalAppData; macOS:
/Applications y ~/Applications; Linux: google-chrome, chromium, microsoft-edge en PATH);
si no, el más reciente ms-playwright/chromium-<N> (o PLAYWRIGHT_BROWSERS_PATH);
si no, se detiene y te indica cómo configurar CHROME o ejecutar npx playwright install chromium. En Windows añade
--use-angle=d3d11; en otros SOs Chrome mantiene su backend GPU por defecto.
Qué hace
Graba escenas scriptadas de un sitio ejecutándose localmente y entrega MP4 listos para Figma (1920×1080, 30fps, x264, < 100MB). Motor validado: puppeteer-core + puppeteer-screen-recorder (CDP screencast + ffmpeg). Nunca graba la pantalla.
Ejecutar
mkdir <proj>-reels && cd <proj>-reels && npm init -y && npm i puppeteer-core puppeteer-screen-recorder
mkdir -p scripts && cp <skill-dir>/scripts/{record,find-chrome}.mjs scripts/
cp <skill-dir>/scripts/scenes.example.mjs scenes.mjs # editar las escenas
# en el repositorio del sitio: npx next build && npx next start -p 3100 (build de producción: WebGL más fluido)
OUT="<output folder>/Videos" node scripts/record.mjs scenes.mjs [scene ...]
Ejecuta los scripts copiados en el proyecto: ESM resuelve puppeteer-core desde la carpeta del script,
no desde el cwd.
Env: BASE (por defecto http://localhost:3100), OUT, CHROME, FFMPEG, W, H, DPR,
MOBILE=1 (emulación táctil), KEEP_UI=1 (mantiene la navegación), MIN_FPS (24). Teléfono 4:5:
W=360 H=450 DPR=2 MOBILE=1 da 720x900.
Cada escena = { name, path, theme?, trim?, crf?, css?, prepare(page, ctx), act(page, ctx) }.
prepare se ejecuta antes de la grabación (desplazar a la sección, esperar a que cargue el 3D); act es lo que
aparece en el video. ctx tiene wait, scrollTo(page, y|selector, ms) y
scrollThrough(page, selector|null, ms, from, to) con easing. Introducción que se ejecuta al cargar:
page.reload() dentro de act y usar trim para cortar la carga.
Guion de marca que funciona: introducción del logo/hero (claro y oscuro), intercambio de hero media, sección 3D con la cámara entrando al hacer scroll, ruta inmersiva (walk), wordmark del footer y una pasada a página completa. 5–7 escenas de 12–55s.
Auditar antes de entregar (no confiar en el código de salida)
- Hoja de contactos por video:
ffmpeg -i x.mp4 -vf "fps=6/<dur>,scale=480:-1,tile=3x2" -frames:v 1 sheet.pngy mirar. - 1MB por 19s = escena en blanco. > 100MB = no entra en Figma (arreglar con
crfde la escena; 24 para una escena con grano + trails). - Franja superior (
crop=1920:120:0:0) en varios momentos para detectar un nav que se filtra. LOW-FPScon una pausa larga al inicio/final es una falsa alarma (el contador cuenta fotogramas fijos); revisar la hoja de contactos.
Problemas conocidos
- Ventana oculta congela rAF → fotogramas congelados. El lanzamiento ya pasa
--disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusion.headless: falsea propósito: headless cae en SwiftShader y WebGL tartamudea. - El CSS de Ocultar-UI desaparece al recargar y cuando la hidratación de React limpia
<head>→ el script vuelve a adjuntar un<style id="reels-hide">al body cada 100ms. - Un selector inválido arruina toda la regla:
\[en un template literal de JS se convierte en[y.z-[60]invalidaheader, nav, .z-[60]. Una regla por selector riesgoso. - No ocultar
[data-cursor]: los sitios etiquetan el canvas 3D y los carruseles con esto. Apuntar al elemento cursor personalizado (fixed + mix-blend + pointer-events-none). - 3D cargado perezosamente (IntersectionObserver + import dinámico): esperar 5–7s después de llegar a la
sección. Leer el canvas WebGL devuelve vacío sin
preserveDrawingBuffer: usar una espera fija o una captura de pantalla. - Screencast entrega ~32 fotogramas únicos/s → grabar a 30fps (60 da fotogramas repetidos irregulares).
trimpor defecto 0.3s elimina el primer fotograma en caché. - Screencast tartamudea en una máquina cargada (Photoshop abierto, render paralelo): el MP4 dice
"30fps" con 9–18 fotogramas únicos/s.
record.mjsmide (mpdecimate=hi=64:lo=24:frac=0.05) y regraba hasta 2 veces por debajo deMIN_FPS(24). Para placas WebGL/3D prefieresnap.mjs. - Banner de cookies: aceptarlo en
prepare(clic genérico en "Accept/Aceptar/OK") antes de grabar. - Una página de herramienta sin scroll (generador, editor de efectos) graba estático: la escena debe
interactuar (subir un archivo a
input[type=file], hacer clic en ajustes por texto del botón). Un slider personalizado no responde a uninputsintético.
Placas bloqueadas a 60fps: snap.mjs (determinista)
scripts/snap.mjs inyecta timeweb (reloj virtual de timesnap: rAF, performance.now,
setTimeout y video), avanza 1/60s por fotograma y captura pantallas. Independiente de la carga de la máquina.
Escena: { name, path, theme, css, dur, wait (ms reales para cargar 3D), pre (ms virtual antes de el desplazamiento, 2000; **0 para una intro que se ejecuta al cargar**), start (s virtual saltados), scroll: { sel, from, to, at, len } | { bottom: [from, to] in screen heights }, frame(page, t) (por ejemplo, mouse) }. El desplazamiento es función del tiempo virtual.
npm i puppeteer-core timesnap && mkdir -p scripts
cp <skill-dir>/scripts/{snap,find-chrome,patch-timeweb}.mjs scripts/
npm pkg set scripts.postinstall="node scripts/patch-timeweb.mjs" && npm run postinstall
FPS=60 OUT=... node scripts/snap.mjs scenes-snap.mjs [scene]
- No usar
timesnap()directamente: con Next + R3F + Lenis grabó una página en blanco y dejó tramos congelados en el hero WebGL (rAF no se ejecuta). El bucle propio (goTo + scrollTo + screenshot) funcionó en todas partes: 60/60 fotogramas únicos. - El parche de timeweb es obligatorio: reemplaza
video.play()por una versión sin Promise y sin eventoplaying→video.play().catch()lanza error y la página se queda en blanco. preserveDrawingBuffer: trueforzado en getContext (desactivar conpreserve: false).- Los fotogramas van al directorio temporal del SO, nunca a una unidad sincronizada (miles de jpg sincronizando).
- No trabajo paralelo de GPU (render de Remotion, Photoshop): WebGL pierde su contexto y graba
segundos solo de fondo. El script advierte
FLASH(fotograma que parpadea por luminancia); un tramo largo en blanco solo aparece en la hoja de contactos. Mirar. - Auditoría de suavidad real:
magick compare -metric MAEentre fotogramas vecinos a resolución completa. 0 = fotograma repetido. - Entrega:
scripts/deliver.example.sh(copiar y renombrar las placas;V=requerido) construye reveals, loops (loop.sh) y 3D limpio, aumentando crf en 3 hasta < 95MB.
Subir a Figma Slides (oculto, sin molestar al equipo)
El MP4 no entra a través de MCP: use_figma no tiene red y la API de Plugin no crea
video desde bytes; upload_assets solo acepta imágenes (≤ 10MB). Por lo tanto:
- Portada de cada video:
ffmpeg -ss <t> -i x.mp4 -frames:v 1 -q:v 2 _capas/x.jpg. use_figma(skillsfigma-use+figma-use-slides):createSlideRow(idx)antes de la diapositiva final,row.name = 'WIP · Motion (oculto)', uncreateSlide(idx, i)por video conisSkippedSlide = true, fondo negro, un rectángulo 1920×1080 llamadoVIDEO · x.mp4y la instrucción enspeakerNotes.upload_assetscon losnodeIdsde los rectángulos → POST multipart de las portadas.- El usuario arrastra el MP4 sobre el rectángulo (reemplaza la portada con el video). Decir esto claramente; nunca prometer un video insertado.
getSlideGrid() omite diapositivas omitidas: revisar la estructura mediante SLIDE_GRID.children[i].children.
Showreel de marca (Remotion): placas + escenas diseñadas
Para un showreel de branding (logo, colores, tipografía, diseños, manifiesto, mockups), grabar solo
placas limpias del sitio (sin texto: CSS por escena oculta título/nav) y componer el resto en
Remotion (render fotograma a fotograma, sin vibraciones). Bloques reutilizables que vale la pena tener en
src/fx.tsx: Mist, MistWords, Grain, KenBurns, Plate, una transición nevoa() (niebla),
con datos de marca en brand.ts.
- La identidad visual viene del deck, no del sitio: revisar la tipografía y paleta oficial en las diapositivas (los sitios se desvían del manual de marca).
- Escenas que funcionan: frase de apertura en niebla, reveal grabado, construcción del logo (
evolvePath+ guías medidas sobre el raster del logo), manifiesto frase por frase, pilares con ejewghtvariable, paleta en bandas, tipografía, gráficos en capas, 3D con título enmix-blend-difference, columnas de layout con scroll, montaje de mockups 1/2/3, producto sincronizado con el nombre, cierre en el wordmark. - Revisar por stills (
npx remotion still ... --frame=N+ montaje) antes de renderizar. - Renderizar con
--concurrency=3: a 8, con Photoshop abierto, una máquina de 32GB se quedó sin memoria. - Loops:
scripts/loop.sh in out [start] [dur] [crossfade]. Verificar la unión conmagick compare -metric RMSEentre el primer y el último fotograma (< 0.5% = invisible). - Placa mala es descarte: un sólido de revolución girando no muestra movimiento; una cámara encuadrada para un canvas vertical se ve pequeña en 16:9.
- Reel frenético (ritmo aprobado): 25–35s, 60fps,
Seriescon cortes duros en una rejilla de 120 BPM (30 fotogramas), ráfagas de imágenes cada 3–6 fotogramas, recorte gigante de logo sobre una rejilla, pesos de fuente parpadeantes, bloques de paleta reordenándose, retrato en un máscara circular, ráfaga gráfica, frase cinética, mockups de un beat, logo de cierre mantenido ~2.5s. Sin etiquetas: texto solo como gráficos. Un showreel lento de 1:40 fue rechazado. - Reel suave/ahumado: grabar el humo del hero sin logo ni foto, negro sobre blanco puro
(escena con
storage= parámetros en localStorage yblock: ['assets/logo.png']; el PNG en blanco debe ser transparente; no bloquear el video: un video roto congela timeweb). Usar la placa como superposiciónmix-blend-mode: multiply: abre al inicio de la escena, una versión invertida (ffmpeg -vf reverse) la cierra; el corte cae en negro. Transición en ~1.1s (ritmo 2), o la mitad de la escena se vuelve negra. - Montaje de layouts de Figma Slides: clonar la diapositiva (omitida), duplicar cada tarjeta 3x con solo
fondo / logo / texto visible, descargar cada nodo con
download_assets(escala 2) y eliminar los clones. Slides no exporta transparencia: exportar logo/texto con la diapositiva negra y luego blanca y extraer la alfa por diferencia (ImageMagick). Nunca devolver base64 a través deuse_figma(revienta el contexto).
Showreel de producto (aplicación real filmada)
Las pantallas se filman de verdad (Playwright + CDP screencast a 2x, pasos
goto/move/click/type/scroll/wait, trail del cursor guardado como <id>.cursor.json) y el cursor se
redibuja en Remotion encima.
Rechazado, no repetir:
- Pegar un reel diseñado a la pantalla real (apertura ilustrada + mitad filmada + CTA dibujado): dos lenguajes, el personaje desaparece y regresa, la tarjeta negra entre actos detiene el ritmo. Un lenguaje por video.
- Pantalla de producto pequeña y acelerada en una ventana 16:9: el feed y el ranking se vuelven un muro ilegible.
- Zoom que recorta la pantalla y texto estampado sobre el video (verbo gigante, contador 01/07, gradiente): intrusivo.
- Espera vacía en pantalla ("esperando…"): cortar directamente a la acción.
- Copia vaga: tiene que ser obvia y en el lenguaje sencillo del público.
La forma que funciona: por acción, texto solo primero, centrado y corto (2 beats), luego la pantalla completa, sin recortar, en una ventana centrada en la relación de aspecto de captura, sobre un fondo limpio. Texto discreto (peso 700, entrada por palabra con desenfoque ligero), sin contador, sin gradiente. Cortes en la rejilla del track (14 fotogramas = 128.57 BPM). 40s máximo.
Técnica que funcionó:
- Acelerar una captura:
<OffthreadVideo trimBefore={from} playbackRate={rate}>y recalcular los fotogramas de clic/ rate(sonido y anillo del cursor). - Iniciar otra composición a medio camino:
<Sequence from={X}><Sequence from={-start}><Comp/></Sequence></Sequence>. Revisar el primer fotograma del tramo: un elemento compartido "volando" aparece suelto. Saltar algunos fotogramas y recolocar el sonido de impacto que cayó antes. - Idioma por composición con una línea de tiempo a nivel de módulo: un flag
globalThisestablecido por su propio archivo de entrada importado antes del Root. - Track continuo a través de cortes:
trimBeforeen la base para que continúe desde donde el tramo anterior se detuvo. - Sonoridad: renderizar, medir y normalizar con 2 pasadas
loudnorm(-14 LUFS / -1 dBTP), copiando el video. Medir solo al final: la mezcla en bruto de Remotion supera 0 dBFS. - Auditoría: hoja de contactos con marca de tiempo (
fps=2,drawtext=%{pts\:hms},tile=8x6) de cada render, y una hoja densa (fps=6) en cada unión de tramos. - Un proyecto sin git, editado por otra sesión al mismo tiempo: nuevo corte en un nuevo archivo,
copia de seguridad
*.bak-*antes de editar un archivo compartido, y decirlo al usuario.
Tarjetas de portafolio y reel de estudio
record.mjsconKEEP_UI=1(la tarjeta muestra el sitio tal como es) y el preset de teléfono anterior.- Escenas generadas desde un archivo de inventario (solo elementos públicos con URL activa); sin URL activa = Ken Burns de la captura de pantalla.
- Paquete de tarjeta: loop con crossfade de final a inicio, tasa de bits de 2 pasadas hasta un techo de KB, VP9 WebM además, poster WebP del fotograma 1.
- Logo con alfa para la web:
-
El contenido se ha truncado en el original; se ha traducido todo el texto disponible.
Subir a Figma (MCP)
- Asegurar que
figma-useyfigma-use-slidesestán instalados. - Crear una fila oculta solo para el agente:
createSlideRow(idx)y nombrarlaWIP · Motion (oculto). - Por cada video:
createSlide(idx, i)conisSkippedSlide = true, fondo negro, rectángulo 1920×1080 con nombreVIDEO · x.mp4. upload_assetspara subir portadas (JPG) a los nodos de los rectángulos.- Indicar al usuario: arrastrar el MP4 sobre el rectángulo en Figma.
Importante: el MCP de Figma no admite subida directa de video. Solo se pueden insertar imágenes.
Notas finales
- Auditar siempre la salida antes de entregar al cliente.
- Mantener las escenas cortas y dinámicas para reels.<|im_end|>