/screen-record — Vidéos de démonstration de navigateur scénarisées
Quand l'utiliser
L'utilisateur souhaite une vidéo soignée d'un site web en action — typiquement pour LinkedIn, Twitter, une présentation commerciale, une mise à jour pour investisseurs, ou une documentation d'intégration. Il décrit ce qui doit apparaître à l'écran, ce que fait le curseur, et le contenu approximatif de la narration. Vous écrivez le script ; le moteur se charge de l'animation du curseur, de la synthèse vocale via ElevenLabs, de la musique, des effets sonores de clic et de l'incrustation des sous-titres.
N'utilisez PAS cette compétence pour : des enregistrements réels d'une session en cours (utilisez macOS Screenshot ou QuickTime), des GIFs (utilisez un outil GIF), ou des vidéos nécessitant une véritable voix humaine (vous pouvez toujours rédiger le script, puis lui faire ré-enregistrer).
Comment ça fonctionne
Le moteur se trouve dans engine.mjs. Il exporte runDemo(config) qui :
- Génère un clip TTS par réplique avec ElevenLabs.
- Génère un fond musical doux et un effet sonore de clic UI avec ffmpeg.
- Lance Chromium en mode headless en 1920×1080, enregistre la vidéo.
- Exécute votre fonction
scenes(helpers)qui pilote le navigateur. - Mixe l'audio (narration forte + musique douce + clics subtils) et grave les sous-titres stylisés ASS.
- Retourne les chemins vers les fichiers finaux
.mp4,.webm,.srt,.ass.
Flux de travail
Étape 1 — lire la demande de l'utilisateur
Identifiez : URL à enregistrer, parcours (ce que le curseur doit faire), ton (formel/décontracté), durée cible. Si quelque chose manque, posez une question ciblée plutôt que de deviner.
Étape 2 — vérifier la clé API
Cherchez ELEVENLABS_API_KEY dans cet ordre :
$CLAUDE_PLUGIN_ROOT/.env.local(ou le dossier.env.localde la compétence)- Le
.env.localdu projet actuel process.env
Si absente, dites à l'utilisateur :
J'ai besoin d'une clé API ElevenLabs. Obtenez-en une gratuite sur https://elevenlabs.io/app/settings/api-keys (10 000 caractères/mois, assez pour ~15 démos). Soit vous la collez dans le chat et je l'enregistre dans le
.env.localde la compétence, soit vous l'ajoutez vous-même.
Si une clé est présente mais que le moteur lève ApiKeyDeadError (HTTP 401/402/403), dites à l'utilisateur :
Votre clé ElevenLabs a été rejetée (révoquée, expirée, ou quota épuisé). Remplacez-la sur https://elevenlabs.io/app/settings/api-keys, puis mettez à jour
.env.local. Je ne réessayerai pas tant que vous ne l'aurez pas fait.
Ne réessayez PAS silencieusement, ne retombez PAS sur la commande macOS say sauf si l'utilisateur le demande explicitement.
Étape 3 — choisir une voix
Par défaut : Liam (TX3LPaxmHKxFdv7VOQHJ) — américain énergique masculin, moderne. Plus léger que George (britannique) et Sarah (américaine féminine). Autres voix gratuites dans engine.mjs sous FREE_TIER_VOICES. Si l'utilisateur dit « utilise la voix X », trouvez-la dans le catalogue. S'il dit « grave/profonde/légère/chaude/féminine », choisissez en conséquence.
Voix de bibliothèque (Charlotte, Rachel, Adam, etc.) nécessitent un abonnement payant ElevenLabs. Si l'utilisateur en demande une et qu'il est en offre gratuite, vous obtiendrez une erreur HTTP 402 — retombez sur la voix gratuite la plus proche et informez l'utilisateur.
Étape 4 — sonder la page cible (uniquement si nécessaire)
Si vous ne connaissez pas la structure de la page, utilisez Playwright ou curl pour inspecter le DOM et trouver les sélecteurs. Ne devinez pas — les sélecteurs sont importants, les correspondances ambiguës feront expirer l'enregistrement.
Pour une SPA avec contenu dynamique, préférez aria-label, role, ou les sélecteurs de texte plutôt que les noms de classe. Utilisez la sémantique page.locator(s).first() (les helpers du moteur le font déjà).
Étape 5 — écrire un fichier de scène
Créez un fichier dans /tmp/screen-record-<nom>.mjs (ne polluez pas le dépôt de l'utilisateur) qui importe le moteur et appelle runDemo. Modèle :
import { runDemo } from '/Users/<vous>/Desktop/Projects/skill/screen-record-skills/engine.mjs';
// ou, si installé via un lien symbolique :
// import { runDemo } from '/Users/<vous>/.claude/skills/screen-record/engine.mjs';
await runDemo({
baseUrl: 'http://localhost:3000',
outputDir: './output/onboarding-demo',
outputName: 'onboarding',
splash: {
label: 'Rendez-vous sur',
url: 'www.votreapp.com',
footer: 'VotreApp — parcours d\'intégration',
},
cues: [
{ id: 'intro', text: 'Bienvenue sur VotreApp. Laissez-moi vous guider.' },
{ id: 'signup', text: 'Appuyez sur S\'inscrire pour créer un compte.' },
{ id: 'form', text: 'Entrez votre email et un mot de passe.' },
{ id: 'done', text: 'Et voilà — vous êtes connecté.' },
],
// Optionnel : simulez un backend pour que la démo soit déterministe.
routes: [
{ match: /\/api\/signup$/, fulfill: { status: 200, body: { ok: true } } },
],
// Optionnel : pré-remplissez un utilisateur authentifié pour sauter la connexion.
// initialAuth: { id: 'u1', email: '[email protected]', name: 'Utilisateur Démo' },
scenes: async ({ page, baseUrl, sleep, speak, awaitCue,
clickAt, typeInto, moveCursorTo, hideSplash, typeSplashUrl,
configSwitch, showSwitch, hideSwitch, setAuth }) => {
// Scène 0 : splash URL (le moteur a déjà navigué vers baseUrl hors caméra).
speak('intro');
await typeSplashUrl('www.votreapp.com');
await awaitCue('intro');
await hideSplash();
await sleep(750); // correspond à la disparition du splash
// Scène 1 : cliquer sur S'inscrire
speak('signup');
await clickAt('button:has-text("S\'inscrire")');
await awaitCue('signup');
// Scène 2 : remplir le formulaire
speak('form');
await typeInto('input[type="email"]', '[email protected]');
await typeInto('input[type="password"]', 'ChevalBatterieCorrect');
await awaitCue('form');
// Scène 3 : soumettre
speak('done');
await clickAt('button[type="submit"]');
await sleep(1000);
await awaitCue('done');
},
});
Étape 6 — exécuter
cd /Users/<vous>/Desktop/Projects/skill/screen-record-skills
# Première fois uniquement :
npm install --silent
# Chaque fois :
node /tmp/screen-record-<nom>.mjs
Chaque exécution dure environ la durée de la vidéo finale plus ~10s de frais généraux.
Étape 7 — rapporter
Informez l'utilisateur :
- Chemin vers le fichier
.mp4final (prêt pour LinkedIn, 1920×1080, H.264 + AAC) - Chemin vers le fichier
.srt(LinkedIn importera automatiquement les sous-titres fermés) - Proposez de lire avec
afplay <chemin>sur macOS
Référence des helpers (ce que vos scènes peuvent appeler)
| Helper | Description |
|---|---|
page | Page Playwright brute si nécessaire |
baseUrl | L'URL de base configurée |
sleep(ms) | await sleep(800) |
moveCursorTo(x, y, duréeMs?) | Déplacement fluide vers les coordonnées |
moveToSelector(selecteur, duréeMs?, décalage?) | Déplacement fluide vers le centre de l'élément |
clickAt(selecteur, duréeMs?) | Déplacement + impulsion + clic (enregistre l'heure du clic pour l'effet sonore) |
typeInto(selecteur, texte, parCaractère?) | Clic + effacement + saisie caractère par caractère |
recordClick() | Enregistre manuellement un horodatage de clic (pour l'effet sonore) |
speak(idRéplique) | Marque le début de la réplique. La fin est début + duréeAudio + 0.35s |
awaitCue(idRéplique) | Attend la fin de la narration de la réplique |
typeSplashUrl(texte, parCaractère?) | Saisie animée dans la barre d'URL du splash |
hideSplash() | Fait disparaître le splash (attendez toujours ~750ms après) |
configSwitch({ fromInitial, fromLabel, toInitial, toLabel, title, sub }) | Définit le contenu de la carte de changement de compte |
showSwitch() / hideSwitch() | Affiche/masque la superposition de changement de compte |
setAuth(utilisateur) | Définit localStorage[authStorageKey] avec un nouvel utilisateur (associé à showSwitch pour les démos de changement de compte) |
Règles de rythme générales
- Une phrase par réplique. Limitez-vous à ~12 mots maximum.
- Pour les actions UI, appelez
speak(réplique)AVANT l'action pour que la narration soit jouée pendant que le curseur se déplace. - Pour les scènes avec beaucoup de saisie, la saisie prend plus de temps que la narration — utilisez
sleepau lieu deawaitCuepour le rythme. - La durée totale de la vidéo est généralement
somme(duréesAudio) + somme(sleeps). Visez 30–75s pour LinkedIn.
Sélecteurs courants qui posent problème
text=Fooseul peut correspondre à plusieurs éléments. Utilisezpage.locator(...).first()(ce que le moteur fait déjà), OU utilisezbutton:has-text("Foo")pour limiter à une balise.- Les noms de classe Tailwind sont fragiles. Préférez
aria-label, role, balise sémantique + texte. - Fenêtres modales/dialogues : attendez
[role="dialog"]ou le texte d'en-tête de la modale avant de cliquer à l'intérieur.
Ce qu'il ne faut PAS faire
- N'écrivez pas le texte de la narration directement dans le fichier de scène — conservez la narration dans
cues[].textpour que les sous-titres et l'audio restent synchronisés. - N'intégrez pas le curseur dans le CSS de la page — le moteur l'injecte.
- N'utilisez pas
await page.screenshot()au milieu d'une scène — vous captureriez le curseur dans des positions étranges ; utilisezextractFramesa posteriori si nécessaire. - Ne commitez pas
.env.local(il est dans.gitignore).