Communitygithub.com

johnwhoyou/screen-video

Graba la pantalla de una aplicación web en ejecución con Playwright, añadiendo subtítulos, narración opcional, cursor suave y sonidos de teclado y clic. Entrega el video como archivo .mp4, o directamente en GitHub, Linear, Slack, o en un tutorial para clientes.

¿Qué es screen-video?

Esta habilidad está diseñada para grabar videos de demostración de aplicaciones web en ejecución, utilizando Playwright para capturar la pantalla del navegador a 60 fps en formato H.264 (.mp4). La grabación incluye características avanzadas como subtítulos sincronizados, narración por voz (mediante TTS de Google Gemini o OpenAI), un cursor que se desliza suavemente, sonidos de teclado y clic, y efectos de zoom y paneo para resaltar detalles. Es ideal para crear walkthroughs de UI, demos de nuevas funcionalidades, videos para PRs o issues, tutoriales para clientes o reproducción de bugs. El flujo de trabajo implica escribir un script de flujo con pasos definidos, grabarlo con calidad ajustable (2×–4K), narrarlo si es necesario y entregarlo según el destino: archivo .mp4, GitHub (subida inline mediante navegador), Linear (a través de MCP), Slack (con MCP o manualmente) o cualquier otro lugar. La habilidad también incluye herramientas para verificar la grabación, gestionar datos demo y evitar errores comunes como datos reales en producción o problemas con el estado del navegador.

Compatible con~Claude Code✓Codex CLI✓Cursor✓Gemini CLI
npx skills add https://github.com/johnwhoyou/screen-video/tree/HEAD/plugins/screen-video/skills/screen-video

Preguntar en tu IA favorita

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

Vista previa

Del README del skill

walkthrough.mp4

Documentación

Screen video

Chrome's screencast graba el navegador en sí mismo, reconstruido como un .mp4 H.264 a 60 fps. Los subtítulos, el cursor, el zoom y los sonidos se añaden después, por lo que la grabación se mantiene fluida independientemente de lo que esté haciendo la página.

Los scripts resuelven playwright desde el node_modules del repositorio actual, así que ejecútalos desde la raíz del repositorio. <skill-dir> es la carpeta de esta habilidad.

0. Preguntar primero

A menos que la solicitud ya lo diga, pregunta en un solo mensaje:

  1. ¿Qué historia? ¿Qué aplicación y URL, qué páginas, los 3–6 momentos que el espectador debería ver? ¿Quién la ve (compañeros de equipo, un revisor, un cliente)?
  2. ¿Voz? Solo subtítulos, o narrada (locución por IA)? Los videos para clientes siempre son narrados.
  3. ¿A dónde va? Solo el archivo .mp4 (por defecto), un PR o issue de GitHub, un issue de Linear, Slack, o algún otro lugar. Pregunta por el PR/issue/canal; nunca crees un ticket en nombre de otra persona.
  4. ¿Qué datos? ¿Qué base de datos local o cuenta usar para la grabación? Nunca producción, nunca datos reales de clientes.

1. Grabar

  1. Ejecuta la aplicación localmente y abre cada página una vez primero: la primera compilación de un servidor de desarrollo terminaría en el video.

  2. Escribe un script de flujo basado en scripts/record-lib.mjs (copia examples/playwright-site/flow.mjs, una grabación ejecutable de un sitio público). Usa step(id, caption, action) (o caption() sin acción) para poner una línea de subtítulo en pantalla para cada paso: centrada abajo, elevada sobre la barra de reproducción, translúcida (captionStyle: 'top' para un banner). Mantenlo en aproximadamente 1 minuto. Haz clic con click(locator) / hover(locator) del flujo, no con locator.click(): deslizan el cursor dibujado hacia el objetivo primero. Un locator.click() simple aún funciona, pero el cursor salta. zoom(locator, { scale }) se acerca suavemente a un objetivo, unzoom() vuelve a la vista normal, y click(locator, { zoom: true }) se acerca antes de hacer clic. El zoom se mantiene hasta que lo muevas; un clic fuera de la vista ampliada se desplaza primero allí.

    Presupuesto de zoom: debe sentirse como una persona inclinándose, no un reflejo.

    • Haz zoom solo cuando el texto pequeño sea el punto: un diff, un valor que cambió, el mensaje de un diálogo. Nunca para navegación, abrir un menú o presionar un botón que cualquiera pueda ver.
    • Aproximadamente un zoom cada 15–20 segundos. Dos seguidos: desplázate entre ellos (llama a zoom en el siguiente objetivo) en lugar de salir y volver a entrar.
    • Mantén el zoom mientras las siguientes acciones ocurren en la misma área; unzoom() cuando la historia se mueva a otro lugar.
    • Escala 1.3–1.5. Ve a 2 solo para algo muy pequeño, como un solo icono. Mantén al menos 2 segundos.
    • Prefiere una pausa narrada sobre un zoom: si la línea se lee bien a tamaño completo, no hagas zoom.

    Escribe con type(locator, text, { clear }): escribe al ritmo de una persona. click y type registran cada clic y pulsación de tecla en <video>.sfx.json, y narrate.mjs coloca los sonidos grabados bajo la voz: muestras de clic de ratón y teclado mecánico desde sounds/. SFX_VOLUME las escala (por defecto 1), SFX=0 para ninguna. page.keyboard.type y locator.click() simple no hacen sonido.

  3. node flow.mjs imprime la ruta del .mp4. Necesita ffmpeg ($FFMPEG o PATH; ¿sin ffmpeg? npm i --prefix <scratch> ffmpeg-static).

Calidad

record(flow, { scale }) diseña la página en size (por defecto 1280×800 px CSS, para que la UI se vea normal) y la renderiza a scale× densidad de píxeles: 2 (por defecto) es 2560×1600, 3 es 3840×2400. El texto se vuelve más nítido, no más pequeño. Espera aproximadamente 15–40 MB por minuto en 2 y 2–3× eso en 3. Para un destino con límite de tamaño (GitHub plan gratuito 10 MB, vistas previas de Slack) usa scale: 1 o CRF=24.

Narración (cuando se pida voz)

  1. Escribe script.json: [{ "id": "intro", "say": "…" }, …], una línea hablada por cada step id. Escríbelo para el oído: oraciones cortas, números en palabras ("mil quinientos").
  2. GOOGLE_API_KEY=… node <skill-dir>/scripts/tts.mjs script.json <audio-dir> escribe un WAV por línea y durations.json. Gemini 3.8 Flash TTS (TTS_MODEL, TTS_VOICE para sobrescribir) sonó más natural que OpenAI gpt-4o-mini-tts. Obtén una clave en aistudio.google.com.
  3. Graba con { narration: '<audio-dir>/durations.json' }. Cada step espera hasta que su línea haya terminado, y los tiempos de inicio aparecen en <video>.mp4.marks.json.
  4. node <skill-dir>/scripts/narrate.mjs <video.mp4> <audio-dir> <out.mp4> mezcla los clips y sonidos (re-codifica en CRF, por defecto 18). Entrega <out.mp4>.

Problemas con TTS:

  • Envía solo la línea. Un prefijo "Di cálidamente: …" se leerá en voz alta, y systemInstruction devuelve 400.
  • Gemini devuelve un WAV completo más un fragmento de procedencia; envolver eso en un segundo encabezado reproduce ruido estático en ambos extremos de cada línea. tts.mjs extrae el fragmento de datos por esta razón; mantenlo.
  • Para verificar la sincronización, transcribe el audio del mp4 con marcas de tiempo (OpenAI whisper-1, response_format=verbose_json) y compáralo con las marcas.

Problemas que cuestan una regrabación

  • Limpia una entrada prellenada con .fill('') antes de pressSequentially, que solo agrega.
  • Cada grabación cambia datos (publica, acepta, guarda). Restablece los datos entre tomas.
  • Espera a que terminen los autoguardados y las inserciones de IA en segundo plano antes del siguiente paso, o la UI saltará.
  • El desplazamiento con la rueda puede plegar un encabezado colapsable y ocultar el siguiente botón. Prefiere el.scrollIntoView() a través de locator.evaluate.
  • Nunca dejes que la página entre en pantalla completa del navegador: los fotogramas se redimensionan y el video se recorta a la esquina superior izquierda. Ejecuta document.documentElement.requestFullscreen = undefined antes de un botón de pantalla completa.

Datos demo

  • Siembra exactamente el estado que la historia necesita en una base de datos local, con personas creíbles en example.com y números 555. Nunca un cliente real, nunca una base de datos de vista previa o producción.
  • Las tablas de historial, auditoría y eventos son de solo agregar: cada regrabación agrega filas que hacen que la siguiente toma parezca escenificada. Usa un registro nuevo por toma, o limita la historia a acciones que no dejen rastro.
  • Primero una prueba en seco: una rama DRY=1 en el flujo hace clic en Cancelar en cada confirmación destructiva. Extrae fotogramas en algunos pasos, corrige localizadores y encuadres, luego haz la única toma real.
  • Después de la toma, verifica que el estado final de la base de datos coincida con lo que afirman los subtítulos o la narración.
  • Cambiar los datos significa volver a ejecutar tts.mjs para las líneas que los describen.
  • zsh no separa $VAR en palabras. Pasa los tiempos de fotograma a frames.mjs literalmente, o ejecuta en un bucle bajo bash.

Videos tutoriales para clientes (incorporación, guías)

  • Siempre narrados. Mantén los subtítulos en frases cortas; la voz lleva el detalle.
  • Un paso de correo electrónico va en Gmail: mountGmail(page, { emailHtml, subject, sender, me }) desde scripts/gmail-scene.mjs. Aliéntalo con el correo renderizado real de la aplicación. Reescribe target="_blank" a _top, porque una ventana emergente deja la toma esperando en la página incorrecta. La respuesta está integrada: #reply-btn, #reply-text, #send-btn. Pasa mailbox: 'sam' para varias bandejas de entrada (servidas en /sam y /sam/message). Una respuesta nunca llega a la aplicación, así que escribe su efecto directamente en la base de datos local.
  • Espera en elementos, no en waitForURL: las cadenas de redirección de autenticación abortan las navegaciones (ERR_ABORTED) y algunas páginas nunca disparan load. Dale a los clics de navegación { noWaitAfter: true }.
  • Después de (re)iniciar un servidor de desarrollo, calienta cada página y ruta de API que la toma toca.
  • Resiembra antes de cada toma. Los tokens de enlace mágico son de un solo uso, por lo que un intento fallido los quema.

2. Verificar

node <skill-dir>/scripts/frames.mjs <video.mp4> 10 30 50 escribe frame-<t>.jpg al lado del video. Míralos antes de entregar. Para narración, pide al usuario que escuche una vez: los fotogramas no pueden detectar defectos de audio. Verifica si hay datos reales de clientes (nombres, correos, teléfonos): subirlos los publica para todos los que pueden ver el destino.

3. Entregar

Solo el archivo (por defecto)

Imprime la ruta absoluta del .mp4 final. Listo.

PR, issue o comentario de GitHub (se reproduce en línea)

GitHub solo reproduce un video en línea desde una URL https://github.com/user-attachments/assets/<uuid> en su propia línea, y esa URL proviene solo de la subida web de GitHub: gh, la API REST y los archivos comprometidos dan un enlace de descarga en su lugar. Las subidas son privadas para los miembros del repositorio y nunca se comprometen.

node <skill-dir>/scripts/attach-video.mjs <URL de PR o issue de GitHub> <video.mp4>
  • Abre una ventana visible de Chromium. Si la sesión guardada ha expirado, muestra la página de inicio de sesión de GitHub: el usuario inicia sesión. Nunca pidas la contraseña y nunca leas las cookies del navegador.
  • Sube a través del cuadro de comentarios de la página; el comentario nunca se publica. Imprime ATTACHMENT_URL https://github.com/user-attachments/assets/<uuid>.
  • Recuerda la sesión en ~/.cache/screen-video/gh-profile. FORGET_SESSION=1 la elimina después de la subida. Una subida concurrente espera al perfil (WAITING_FOR_PROFILE).

Coloca la URL sola en su propia línea en el cuerpo o comentario (gh pr edit <n> --body-file <file>, gh issue comment <n> --body-file <file>). Para confirmar, verifica el body_html del PR en busca de una etiqueta <video src=…private-user-images…>; un curl -sI en la URL del activo puede devolver 404 incluso cuando se reproduce. Límite de tamaño: 10 MB en planes gratuitos, 100 MB en planes de pago.

Issue de Linear

El MCP de Linear sube directamente; no se necesita navegador.

  1. prepare_attachment_upload con issue, filename, contentType (video/mp4) y size (bytes exactos: stat -f%z <file> en macOS, stat -c%s en Linux).
  2. Dentro de 60 s, curl -X PUT --data-binary @<file> "<uploadRequest.url>" con cada entrada uploadRequest.headers enviada textualmente. Cambiar las mayúsculas de un encabezado u omitir uno da un 403. Sube un archivo a la vez.
  3. create_attachment_from_upload con issue y assetUrl.
  4. Para que se reproduzca en el hilo, save_comment con ![walkthrough.mp4](<assetUrl>) más un pie de foto de una línea. Abre el issue una vez para confirmar que se renderiza como un reproductor.
  5. Reemplazar un video: sube el nuevo, edita el mismo comentario (save_comment con id), luego delete_attachment el antiguo.

Slack

Con el MCP de Slack, después de que el usuario confirme el canal (compartir no se puede deshacer):

  1. slack_get_file_upload_url con filename y content_length (bytes exactos).
  2. URL='<upload_url>' && curl -X POST "$URL" -H 'Content-Type: video/mp4' --data-binary @<file>. Mantén la URL byte por byte; un 401 significa que fue alterada o expiró, así que obtén una nueva.
  3. slack_complete_file_upload con file_id, channel_id (un id C…/D…, no un nombre; búscalo con slack_search_channels), opcionalmente thread_ts y initial_comment.

Sin el MCP, entrega al usuario la ruta del archivo para que lo arrastre.

Cualquier otro lugar

Entrega la ruta del archivo y el tamaño; menciona scale: 1 / CRF=24 si el destino limita el tamaño.

Skills relacionados