/screen-record — Videos de recorrido guiado por el navegador
Cuándo usarlo
El usuario quiere un video pulido de un sitio web haciendo algo, normalmente para LinkedIn, Twitter, una presentación comercial, una actualización para inversores o documentación de incorporación. El usuario describe lo que debe aparecer en pantalla, qué hace el cursor y aproximadamente lo que debe decir la narración. Tú creas un script; el motor se encarga de la animación del cursor, la síntesis de voz (TTS) con ElevenLabs, la música, los efectos de sonido de clic y la inclusión de subtítulos.
No uses esta habilidad para: grabaciones de pantalla reales de una sesión ya en ejecución (usa Captura de pantalla de macOS o QuickTime), GIFs (usa una herramienta de GIFs) o videos que necesiten una locución humana real (puedes redactar el guion y luego pedir al usuario que lo regrabe).
Cómo funciona
El motor está en engine.mjs. Exporta runDemo(config) que:
- Genera un clip de TTS por cada indicación con ElevenLabs.
- Genera una pista de música ambiental suave y un efecto de sonido de clic con ffmpeg.
- Inicia Chromium sin interfaz gráfica a 1920×1080 y graba el video.
- Ejecuta tu función
scenes(helpers)que controla el navegador. - Mezcla el audio (narración alta + música baja + clics sutiles) e incluye subtítulos con estilo ASS.
- Devuelve las rutas a los archivos finales
.mp4,.webm,.srt,.ass.
Flujo de trabajo
Paso 1 — lee la solicitud del usuario
Determina: URL a grabar, flujo (lo que debe hacer el cursor), tono (formal/informal) y duración objetivo. Si falta algo, haz una pregunta concreta en lugar de adivinar.
Paso 2 — verifica la clave API
Busca ELEVENLABS_API_KEY en este orden:
$CLAUDE_PLUGIN_ROOT/.env.local(o el.env.localde la propia carpeta de la habilidad)- El
.env.localdel proyecto actual process.env
Si no está, dile al usuario:
Necesito una clave API de ElevenLabs. Obtén una gratuita en https://elevenlabs.io/app/settings/api-keys (10.000 caracteres/mes, suficiente para ~15 demos). Puedes pegarla en el chat y la guardaré en el
.env.localde la habilidad, o añadirla tú mismo.
Si la clave está presente pero el motor lanza ApiKeyDeadError (HTTP 401/402/403), dile al usuario:
Tu clave de ElevenLabs fue rechazada (revocada, caducada o sin saldo). Renuévala en https://elevenlabs.io/app/settings/api-keys y luego actualiza
.env.local. No lo reintentaré hasta que lo hagas.
No reintentes en silencio, no recurras a say de macOS a menos que el usuario lo solicite explícitamente.
Paso 3 — elige una voz
Por defecto es Liam (TX3LPaxmHKxFdv7VOQHJ) — voz masculina estadounidense enérgica y moderna. Más ligero que George (británica) y Sarah (femenina estadounidense). Otras voces gratuitas en engine.mjs bajo FREE_TIER_VOICES. Si el usuario dice "usa la voz X", búscala en el catálogo. Si dice "grave/fuerte/suave/cálida/femenina", elige en consecuencia.
Voces de la biblioteca (Charlotte, Rachel, Adam, etc.) requieren una suscripción paga de ElevenLabs. Si el usuario pide una y está en el plan gratuito, recibirás HTTP 402 — recurre a la voz gratuita más cercana e informa al usuario.
Paso 4 — analiza la página de destino (solo si es necesario)
Si no conoces la estructura de la página, usa Playwright o curl para inspeccionar el DOM y encontrar selectores. No adivines — los selectores importan, las coincidencias ambiguas agotarán el tiempo de grabación.
Para una SPA con contenido dinámico, prefiere aria-label, role o selectores de texto sobre nombres de clase. Usa la semántica de page.locator(s).first() (los ayudantes del motor ya lo hacen).
Paso 5 — escribe un archivo de escena
Crea un archivo en /tmp/screen-record-<nombre>.mjs (no contaminar el repositorio del usuario) que importe el motor y llame a runDemo. Plantilla:
import { runDemo } from '/Users/<tu>/Desktop/Projects/skill/screen-record-skills/engine.mjs';
// o, si está instalado mediante enlace simbólico:
// import { runDemo } from '/Users/<tu>/.claude/skills/screen-record/engine.mjs';
await runDemo({
baseUrl: 'http://localhost:3000',
outputDir: './output/onboarding-demo',
outputName: 'onboarding',
splash: {
label: 'Visítanos en',
url: 'www.tuaplicacion.com',
footer: 'TuApp — flujo de incorporación',
},
cues: [
{ id: 'intro', text: 'Bienvenido a TuApp. Déjame mostrarte cómo funciona.' },
{ id: 'signup', text: 'Toca Registrarse para crear una cuenta.' },
{ id: 'form', text: 'Introduce tu correo electrónico y una contraseña.' },
{ id: 'done', text: 'Y eso es todo — ya estás dentro.' },
],
// Opcional: simula el backend para que la demo sea determinista.
routes: [
{ match: /\/api\/signup$/, fulfill: { status: 200, body: { ok: true } } },
],
// Opcional: presiembra un usuario autenticado para que la demo omita el inicio de sesión.
// initialAuth: { id: 'u1', email: '[email protected]', name: 'Usuario Demo' },
scenes: async ({ page, baseUrl, sleep, speak, awaitCue,
clickAt, typeInto, moveCursorTo, hideSplash, typeSplashUrl,
configSwitch, showSwitch, hideSwitch, setAuth }) => {
// Escena 0: pantalla de inicio (el motor ya navegó a baseUrl fuera de cámara).
speak('intro');
await typeSplashUrl('www.tuaplicacion.com');
await awaitCue('intro');
await hideSplash();
await sleep(750); // coincide con la transición de la pantalla de inicio
// Escena 1: haz clic en registrarse
speak('signup');
await clickAt('button:has-text("Registrarse")');
await awaitCue('signup');
// Escena 2: completa el formulario
speak('form');
await typeInto('input[type="email"]', '[email protected]');
await typeInto('input[type="password"]', 'CorrectHorseBattery');
await awaitCue('form');
// Escena 3: envía
speak('done');
await clickAt('button[type="submit"]');
await sleep(1000);
await awaitCue('done');
},
});
Paso 6 — ejecútalo
cd /Users/<tu>/Desktop/Projects/skill/screen-record-skills
# Solo la primera vez:
npm install --silent
# Cada vez:
node /tmp/screen-record-<nombre>.mjs
Cada ejecución dura aproximadamente el largo del video final más ~10s de overhead.
Paso 7 — informa
Dile al usuario:
- Ruta al
.mp4final (listo para LinkedIn, 1920×1080, H.264 + AAC) - Ruta al
.srt(LinkedIn lo importará automáticamente como subtítulos) - Ofrece reproducirlo con
afplay <ruta>en macOS
Referencia de ayudantes (lo que tus escenas pueden llamar)
| Ayudante | Qué hace |
|---|---|
page | Página de Playwright sin procesar si la necesitas |
baseUrl | La URL base configurada |
sleep(ms) | await sleep(800) |
moveCursorTo(x, y, duraciónMs?) | Movimiento suave a coordenadas |
moveToSelector(selector, duraciónMs?, offset?) | Movimiento suave al centro del elemento |
clickAt(selector, duraciónMs?) | Mover + pulsar + hacer clic (registra el tiempo del clic para el SFX) |
typeInto(selector, texto, porCarácter?) | Hacer clic + borrar + escribir carácter por carácter |
recordClick() | Registrar manualmente una marca de tiempo de clic (para SFX) |
speak(idDeCue) | Marcar el inicio de una indicación. El final es inicio + duraciónAudio + 0.35s |
awaitCue(idDeCue) | Dormir hasta que termine la narración de la indicación |
typeSplashUrl(texto, porCarácter?) | Escritura animada en la barra de URL de la pantalla de inicio |
hideSplash() | Desvanecer la pantalla de inicio (siempre dormir ~750ms después) |
configSwitch({ fromInitial, fromLabel, toInitial, toLabel, title, sub }) | Establecer el contenido de la tarjeta de cambio de cuenta |
showSwitch() / hideSwitch() | Alternar la superposición de cambio de cuenta |
setAuth(usuario) | Establecer localStorage[authStorageKey] a un nuevo usuario (emparejado con showSwitch para demos de cambio de cuenta) |
Reglas generales de ritmo
- Una frase por indicación. Mantenla por debajo de ~12 palabras.
- Para acciones de interfaz,
speak(cue)ANTES de la acción para que la narración suene mientras el cursor se mueve. - Para escenas con mucha escritura, la escritura toma más tiempo que la narración — usa
sleepen lugar deawaitCuepara marcar el ritmo. - La duración total del video suele ser
suma(duracionesDeAudio) + suma(sleeps). Apunta a 30–75 segundos para LinkedIn.
Selectores comunes que fallan
text=Foopor sí solo puede coincidir con muchos elementos. Usapage.locator(...).first()que es lo que el motor ya hace, O usabutton:has-text("Foo")para limitarlo a una etiqueta.- Los nombres de clase de Tailwind son frágiles. Prefiere
aria-label, role, etiqueta semántica + texto. - Modales/diálogos: espera a que aparezca
[role="dialog"]o el texto del encabezado del modal antes de hacer clic dentro.
Lo que NO debes hacer
- No escribas el texto real de la narración en el archivo de escena — mantén la narración en
cues[].textpara que los subtítulos y el audio se mantengan sincronizados. - No incrustes el cursor en el CSS de la página — el motor lo inyecta.
- No uses
await page.screenshot()en medio de una escena — capturarás el cursor en posiciones extrañas; usaextractFramesdespués si es necesario. - No subas
.env.local(está en.gitignore).