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:
- ¿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)?
- ¿Voz? Solo subtítulos, o narrada (locución por IA)? Los videos para clientes siempre son narrados.
- ¿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. - ¿Qué datos? ¿Qué base de datos local o cuenta usar para la grabación? Nunca producción, nunca datos reales de clientes.
1. Grabar
-
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.
-
Escribe un script de flujo basado en
scripts/record-lib.mjs(copiaexamples/playwright-site/flow.mjs, una grabación ejecutable de un sitio público). Usastep(id, caption, action)(ocaption()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 conclick(locator)/hover(locator)del flujo, no conlocator.click(): deslizan el cursor dibujado hacia el objetivo primero. Unlocator.click()simple aún funciona, pero el cursor salta.zoom(locator, { scale })se acerca suavemente a un objetivo,unzoom()vuelve a la vista normal, yclick(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
zoomen 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.clickytyperegistran cada clic y pulsación de tecla en<video>.sfx.json, ynarrate.mjscoloca los sonidos grabados bajo la voz: muestras de clic de ratón y teclado mecánico desdesounds/.SFX_VOLUMElas escala (por defecto 1),SFX=0para ninguna.page.keyboard.typeylocator.click()simple no hacen sonido. -
node flow.mjsimprime la ruta del.mp4. Necesita ffmpeg ($FFMPEGo 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)
- Escribe
script.json:[{ "id": "intro", "say": "…" }, …], una línea hablada por cadastepid. Escríbelo para el oído: oraciones cortas, números en palabras ("mil quinientos"). GOOGLE_API_KEY=… node <skill-dir>/scripts/tts.mjs script.json <audio-dir>escribe un WAV por línea ydurations.json. Gemini 3.8 Flash TTS (TTS_MODEL,TTS_VOICEpara sobrescribir) sonó más natural que OpenAIgpt-4o-mini-tts. Obtén una clave en aistudio.google.com.- Graba con
{ narration: '<audio-dir>/durations.json' }. Cadastepespera hasta que su línea haya terminado, y los tiempos de inicio aparecen en<video>.mp4.marks.json. node <skill-dir>/scripts/narrate.mjs <video.mp4> <audio-dir> <out.mp4>mezcla los clips y sonidos (re-codifica enCRF, 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
systemInstructiondevuelve 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.mjsextrae 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 depressSequentially, 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 delocator.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 = undefinedantes 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.comy números555. 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=1en 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.mjspara las líneas que los describen. - zsh no separa
$VARen palabras. Pasa los tiempos de fotograma aframes.mjsliteralmente, o ejecuta en un bucle bajobash.
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 })desdescripts/gmail-scene.mjs. Aliéntalo con el correo renderizado real de la aplicación. Reescribetarget="_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. Pasamailbox: 'sam'para varias bandejas de entrada (servidas en/samy/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 disparanload. 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=1la 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.
prepare_attachment_uploadconissue,filename,contentType(video/mp4) ysize(bytes exactos:stat -f%z <file>en macOS,stat -c%sen Linux).- Dentro de 60 s,
curl -X PUT --data-binary @<file> "<uploadRequest.url>"con cada entradauploadRequest.headersenviada textualmente. Cambiar las mayúsculas de un encabezado u omitir uno da un 403. Sube un archivo a la vez. create_attachment_from_uploadconissueyassetUrl.- Para que se reproduzca en el hilo,
save_commentconmás un pie de foto de una línea. Abre el issue una vez para confirmar que se renderiza como un reproductor. - Reemplazar un video: sube el nuevo, edita el mismo comentario (
save_commentconid), luegodelete_attachmentel antiguo.
Slack
Con el MCP de Slack, después de que el usuario confirme el canal (compartir no se puede deshacer):
slack_get_file_upload_urlconfilenameycontent_length(bytes exactos).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.slack_complete_file_uploadconfile_id,channel_id(un idC…/D…, no un nombre; búscalo conslack_search_channels), opcionalmentethread_tsyinitial_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.