Communitygithub.com

pedrojaques99/site-reels

Graba las mejores escenas de un sitio web o app web (WebGL hero, 3D, scroll) como MP4 1080p para presentaciones, las sube a Figma Slides y construye reels de marca o producto en Remotion.

¿Qué es site-reels?

Site Reels es un skill avanzado para agentes de IA (como Claude Code, Cursor, Codex) que automatiza la captura de escenas web interactivas —incluyendo animaciones WebGL, efectos 3D y desplazamientos— y las convierte en videos MP4 de alta calidad (1920×1080, 30 fps, x264) listos para presentaciones. El skill utiliza Puppeteer-core y puppeteer-screen-recorder para grabar escenas scriptadas sin depender de la grabación de pantalla, garantizando una reproducción suave incluso en contenido complejo. Además, permite subir automáticamente las portadas de los videos a Figma Slides mediante el MCP de Figma, facilitando la integración en decks, y ofrece herramientas para crear reels de marca completos en Remotion, incluyendo placas de 60 fps deterministas, composiciones de producto filmadas y tarjetas de portafolio. El sistema soporta Windows, macOS y Linux, y requiere Node.js 18+, ffmpeg y Chrome/Chromium/Edge. Es ideal para desarrolladores y diseñadores que necesitan generar rápidamente videos profesionales de sitios web para presentaciones, lanzamientos o reels de producto.

Compatible con~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/pedrojaques99/visant-skills-public/tree/HEAD/site-reels

Preguntar en tu IA favorita

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

Documentación

Site Reels: sitio → MP4 → deck

Requisitos

Necesario paraQuéVerificar
todoWindows, macOS o Linux con GPU y escritorio visible (Chrome corre sin headless)n/a
todoNode.js 18+node -v
todoffmpeg + ffprobe en PATH (o FFMPEG=<ruta completa>)ffmpeg -version
todoChrome, Chromium o Edge (detección automática, ver abajo)node -e "import('./scripts/find-chrome.mjs').then(m=>console.log(m.findChrome()))"
record.mjspuppeteer-core + puppeteer-screen-recorder (npm, en el proyecto)npm ls puppeteer-core puppeteer-screen-recorder
snap.mjspuppeteer-core + timesnap (incluye timeweb)npm ls timesnap
showreelProyecto Remotionnpx remotion --version
auditorías (opcional)ImageMagickmagick -version
Subida a FigmaFigma MCP (use_figma, upload_assets) + skills figma-use, figma-use-slidesLista 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.png y mirar.
  • 1MB por 19s = escena en blanco. > 100MB = no entra en Figma (arreglar con crf de 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-FPS con una pausa larga al inicio/final es una falsa alarma (el contador cuenta fotogramas fijos); revisar la hoja de contactos.

Problemas conocidos

  1. 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: false a propósito: headless cae en SwiftShader y WebGL tartamudea.
  2. 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.
  3. Un selector inválido arruina toda la regla: \[ en un template literal de JS se convierte en [ y .z-[60] invalida header, nav, .z-[60]. Una regla por selector riesgoso.
  4. 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).
  5. 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.
  6. Screencast entrega ~32 fotogramas únicos/s → grabar a 30fps (60 da fotogramas repetidos irregulares). trim por defecto 0.3s elimina el primer fotograma en caché.
  7. Screencast tartamudea en una máquina cargada (Photoshop abierto, render paralelo): el MP4 dice "30fps" con 9–18 fotogramas únicos/s. record.mjs mide (mpdecimate=hi=64:lo=24:frac=0.05) y regraba hasta 2 veces por debajo de MIN_FPS (24). Para placas WebGL/3D prefiere snap.mjs.
  8. Banner de cookies: aceptarlo en prepare (clic genérico en "Accept/Aceptar/OK") antes de grabar.
  9. 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 un input sinté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 evento playing → video.play().catch() lanza error y la página se queda en blanco.
  • preserveDrawingBuffer: true forzado en getContext (desactivar con preserve: 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 MAE entre 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:

  1. Portada de cada video: ffmpeg -ss <t> -i x.mp4 -frames:v 1 -q:v 2 _capas/x.jpg.
  2. use_figma (skills figma-use + figma-use-slides): createSlideRow(idx) antes de la diapositiva final, row.name = 'WIP · Motion (oculto)', un createSlide(idx, i) por video con isSkippedSlide = true, fondo negro, un rectángulo 1920×1080 llamado VIDEO · x.mp4 y la instrucción en speakerNotes.
  3. upload_assets con los nodeIds de los rectángulos → POST multipart de las portadas.
  4. 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 eje wght variable, paleta en bandas, tipografía, gráficos en capas, 3D con título en mix-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 con magick compare -metric RMSE entre 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, Series con 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 y block: ['assets/logo.png']; el PNG en blanco debe ser transparente; no bloquear el video: un video roto congela timeweb). Usar la placa como superposición mix-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 de use_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 globalThis establecido por su propio archivo de entrada importado antes del Root.
  • Track continuo a través de cortes: trimBefore en 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.mjs con KEEP_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)

  1. Asegurar que figma-use y figma-use-slides están instalados.
  2. Crear una fila oculta solo para el agente: createSlideRow(idx) y nombrarla WIP · Motion (oculto).
  3. Por cada video: createSlide(idx, i) con isSkippedSlide = true, fondo negro, rectángulo 1920×1080 con nombre VIDEO · x.mp4.
  4. upload_assets para subir portadas (JPG) a los nodos de los rectángulos.
  5. 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|>

Skills relacionados