Communitygithub.com

johnwhoyou/screen-video

Enregistrez une capture d'écran vocale ou sous-titrée d'une application web en cours d'exécution avec Playwright — curseur fluide, sons de clavier et de clic, zoom et panoramique, netteté 2×–4K — et livrez-la là où elle est nécessaire : un simple fichier .mp4, directement dans une PR ou un ticket GitHub, un ticket Linear, sur Slack, ou un tutoriel client.

Qu'est-ce que screen-video ?

Cette compétence permet de produire des vidéos de démonstration ou de parcours d'interface utilisateur de haute qualité à partir d'une application web exécutée localement. Elle utilise Playwright pour orchestrer les étapes (clics, saisies, zooms) et génère une vidéo H.264 fluide à 60 images par seconde, avec sous-titres et, optionnellement, une narration vocale générée par IA. La compétence est utile pour créer des vidéos de PR, des tickets, des démos clients ou des tutoriels. Elle est conçue pour être utilisée avec Claude Code, Cursor, Codex ou d'autres agents capables de lancer des scripts Node.js. Le flux de travail comprend l'enregistrement, la vérification (via des captures d'écran), et la livraison vers GitHub, Linear, Slack, ou un fichier local. Elle nécessite que l'utilisateur ait Playwright et ffmpeg disponibles, et prend en charge la synthèse vocale via Google Gemini TTS ou OpenAI TTS.

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Aperçu

Depuis le README du skill

walkthrough.mp4

Documentation

Screen video

Le screencast de Chrome enregistre le navigateur lui-même, reconstruit en H.264 .mp4 à 60 images par seconde. Les sous-titres, le curseur, le zoom et les sons sont ajoutés après coup, de sorte que la vidéo reste fluide quelle que soit l'activité de la page.

Les scripts résolvent playwright à partir du node_modules du dépôt actuel, exécutez-les donc depuis la racine du dépôt. <skill-dir> est le dossier de cette compétence.

0. Demander d'abord

À moins que la demande ne le précise déjà, demandez en un seul message :

  1. Quelle histoire ? Quelle application et URL, quelles pages, les 3 à 6 étapes clés que le spectateur doit voir. Qui regarde (collègues, un relecteur, un client) ?
  2. Voix ? Uniquement des sous-titres, ou une narration (voix-off IA) ? Les vidéos clients sont toujours narrées.
  3. Où va-t-elle ? Juste le fichier .mp4 (par défaut), une PR ou un ticket GitHub, un ticket Linear, Slack, ou ailleurs. Demandez la PR/le ticket/le canal ; ne créez jamais un ticket pour le compte d'autrui.
  4. Quelles données ? Quelle base de données locale ou quel compte utiliser pour l'enregistrement. Jamais de production, jamais de vraies données clients.

1. Enregistrer

  1. Exécutez l'application localement et ouvrez chaque page une fois d'abord : la première compilation d'un serveur de développement atterrirait autrement dans la vidéo.

  2. Écrivez un script de flux basé sur scripts/record-lib.mjs (copiez examples/playwright-site/flow.mjs, un enregistrement exécutable d'un site public). Utilisez step(id, caption, action) (ou caption() nue) pour placer une ligne de sous-titre à l'écran pour chaque étape : centré en bas, surélevé au-dessus de la barre de lecture d'un lecteur, translucide (captionStyle: 'top' pour une bannière). Gardez-le à environ 1 minute. Cliquez avec le click(locator) / hover(locator) du flux, pas avec locator.click() : ils font glisser le curseur dessiné vers la cible d'abord. Un locator.click() nu fonctionne toujours mais le curseur saute. zoom(locator, { scale }) se rapproche doucement d'une cible, unzoom() revient en arrière, et click(locator, { zoom: true }) zoome avant un clic. Le zoom reste en place jusqu'à ce que vous le déplaciez ; un clic en dehors de la vue zoomée y effectue un panoramique d'abord.

    Budget de zoom : cela doit donner l'impression qu'une personne se penche, pas un réflexe.

    • Zoomez uniquement lorsque du petit texte est le sujet : un diff, une valeur qui a changé, le message d'une boîte de dialogue. Jamais pour la navigation, l'ouverture d'un menu ou un bouton que tout le monde peut voir.
    • Environ un zoom toutes les 15 à 20 secondes. Deux à la suite : faites un panoramique entre eux (appelez zoom sur la cible suivante) au lieu de sortir et rentrer.
    • Restez zoomé pendant que les actions suivantes se produisent dans la même zone ; unzoom() lorsque l'histoire se déplace ailleurs.
    • Échelle 1,3–1,5. Allez à 2 uniquement pour quelque chose de minuscule, comme une seule icône. Maintenez au moins 2 secondes.
    • Préférez une pause narrée à un zoom : si le texte est lisible en taille réelle, ne zoomez pas.

    Tapez avec type(locator, text, { clear }) : il tape au rythme d'une personne. click et type enregistrent chaque clic et frappe dans <video>.sfx.json, et narrate.mjs place les sons enregistrés sous la voix : des échantillons de clic de souris et de clavier mécanique enregistrés depuis sounds/. SFX_VOLUME les ajuste (par défaut 1), SFX=0 pour aucun. page.keyboard.type et locator.click() nu ne produisent aucun son.

  3. node flow.mjs affiche le chemin du fichier .mp4. Il nécessite ffmpeg ($FFMPEG ou PATH ; pas de ffmpeg ? npm i --prefix <scratch> ffmpeg-static).

Qualité

record(flow, { scale }) dispose la page à la taille size (par défaut 1280×800 px CSS, pour une UI normale) et la rend à scale× la densité de pixels : 2 (par défaut) donne 2560×1600, 3 donne 3840×2400. Le texte devient plus net, pas plus petit. Attendez-vous à environ 15–40 Mo par minute à 2 et 2–3× cela à 3. Pour une destination avec une limite de taille (plan gratuit GitHub 10 Mo, aperçus Slack), utilisez scale: 1 ou CRF=24.

Narration (lorsqu'une voix est demandée)

  1. Écrivez script.json : [{ "id": "intro", "say": "…" }, …], une ligne parlée par identifiant d'step. Écrivez-le pour l'oreille : phrases courtes, nombres en toutes lettres ("mille cinq cents").
  2. GOOGLE_API_KEY=… node <skill-dir>/scripts/tts.mjs script.json <audio-dir> écrit un WAV par ligne et durations.json. Gemini 3.8 Flash TTS (TTS_MODEL, TTS_VOICE pour remplacer) a semblé plus naturel qu'OpenAI gpt-4o-mini-tts. Obtenez une clé sur aistudio.google.com.
  3. Enregistrez avec { narration: '<audio-dir>/durations.json' }. Chaque step attend que sa ligne soit terminée, et les heures de début atterrissent dans <video>.mp4.marks.json.
  4. node <skill-dir>/scripts/narrate.mjs <video.mp4> <audio-dir> <out.mp4> multiplexe les clips et les sons (ré-encode à CRF, par défaut 18). Livrez <out.mp4>.

Pièges du TTS :

  • Envoyez la ligne uniquement. Un préfixe "Dis chaleureusement : …" est lu à voix haute, et systemInstruction renvoie une erreur 400.
  • Gemini renvoie un WAV entier plus un bloc de provenance ; l'envelopper dans un second en-tête produit des parasites aux deux extrémités de chaque ligne. tts.mjs extrait le bloc de données pour cette raison ; conservez-le.
  • Pour vérifier la synchronisation, transcrivez l'audio mp4 avec des horodatages (OpenAI whisper-1, response_format=verbose_json) et comparez avec les marques.

Pièges qui coûtent une reprise

  • Effacez une entrée préremplie avec .fill('') avant pressSequentially, qui ne fait qu'ajouter.
  • Chaque prise modifie les données (publie, accepte, sauvegarde). Réinitialisez les données entre les prises.
  • Attendez la fin des sauvegardes automatiques et des remplissages IA en arrière-plan avant l'étape suivante, sinon l'UI saute.
  • Le défilement par molette peut replier un en-tête réductible et cacher le bouton suivant. Préférez el.scrollIntoView() via locator.evaluate.
  • Ne laissez jamais la page passer en plein écran du navigateur : les images sont redimensionnées et la vidéo recadrée en haut à gauche. Exécutez document.documentElement.requestFullscreen = undefined avant un bouton plein écran.

Données de démonstration

  • Initialisez exactement l'état dont l'histoire a besoin dans une base de données locale, avec des personnes crédibles chez example.com et des numéros en 555. Jamais un vrai client, jamais une base de données de prévisualisation ou de production.
  • Les tables d'historique, d'audit et d'événements sont en ajout seul : chaque reprise ajoute des lignes qui font paraître la prise suivante mise en scène. Utilisez un enregistrement frais par prise, ou limitez l'histoire à des actions qui ne laissent pas de trace.
  • Faites d'abord un essai à sec : une branche DRY=1 dans le flux clique sur Annuler à chaque confirmation destructive. Capturez des images à quelques étapes, corrigez les localisateurs et le cadrage, puis faites la seule vraie prise.
  • Après la prise, vérifiez que l'état final de la base de données correspond à ce que les sous-titres ou la narration affirment.
  • Modifier les données signifie ré-exécuter tts.mjs pour les lignes qui les décrivent.
  • zsh ne fait pas la séparation par mots de $VAR. Passez les temps d'images à frames.mjs littéralement, ou bouclez sous bash.

Vidéos de tutoriel client (intégration, mode d'emploi)

  • Toujours narrées. Gardez les sous-titres pour des phrases courtes ; la voix porte le détail.
  • Une étape d'e-mail va dans Gmail : mountGmail(page, { emailHtml, subject, sender, me }) depuis scripts/gmail-scene.mjs. Fournissez-lui le vrai rendu de l'e-mail de l'application. Il réécrit target="_blank" en _top, car une popup laisserait la prise en attente sur la mauvaise page. La réponse est intégrée : #reply-btn, #reply-text, #send-btn. Passez mailbox: 'sam' pour plusieurs boîtes aux lettres (servies à /sam et /sam/message). Une réponse n'atteint jamais l'application, alors écrivez son effet directement dans la base de données locale.
  • Attendez les éléments, pas waitForURL : les chaînes de redirection d'authentification annulent les navigations (ERR_ABORTED) et certaines pages ne déclenchent jamais load. Donnez aux clics de navigation { noWaitAfter: true }.
  • Après avoir (re)démarré un serveur de développement, préchauffez chaque page et route API que la prise utilise.
  • Réinitialisez avant chaque prise. Les jetons de lien magique sont à usage unique, donc une tentative échouée les brûle.

2. Vérifier

node <skill-dir>/scripts/frames.mjs <video.mp4> 10 30 50 écrit frame-<t>.jpg à côté de la vidéo. Regardez-les avant de livrer. Pour la narration, demandez à l'utilisateur d'écouter une fois : les images ne peuvent pas détecter les défauts audio. Vérifiez l'absence de données clients réelles (noms, e-mails, téléphones) : leur téléchargement les publie à tous ceux qui peuvent voir la destination.

3. Livrer

Juste le fichier (par défaut)

Affichez le chemin absolu du fichier .mp4 final. Fait.

PR, ticket ou commentaire GitHub (lecture en ligne)

GitHub ne lit une vidéo en ligne qu'à partir d'une URL https://github.com/user-attachments/assets/<uuid> sur sa propre ligne, et cette URL provient uniquement du téléchargement web de GitHub : gh, l'API REST et les fichiers commités donnent un lien de téléchargement à la place. Les téléchargements sont privés pour les membres du dépôt et ne sont jamais commités.

node <skill-dir>/scripts/attach-video.mjs <URL de PR ou ticket GitHub> <video.mp4>
  • Ouvre une fenêtre Chromium visible. Si la connexion enregistrée a expiré, elle montre la page de connexion GitHub : l'utilisateur se connecte. Ne demandez jamais le mot de passe et ne lisez jamais les cookies du navigateur.
  • Télécharge via la zone de commentaire de la page ; le commentaire n'est jamais posté. Affiche ATTACHMENT_URL https://github.com/user-attachments/assets/<uuid>.
  • Se souvient de la connexion dans ~/.cache/screen-video/gh-profile. FORGET_SESSION=1 le supprime après le téléchargement. Un téléchargement concurrent attend le profil (WAITING_FOR_PROFILE).

Placez l'URL seule sur sa propre ligne dans le corps ou le commentaire (gh pr edit <n> --body-file <file>, gh issue comment <n> --body-file <file>). Pour confirmer, vérifiez le body_html de la PR pour une balise <video src=…private-user-images…> ; un curl -sI sur l'URL de l'actif peut renvoyer 404 même quand elle se lit. Limite de taille : 10 Mo sur les plans gratuits, 100 Mo sur les plans payants.

Ticket Linear

Le MCP Linear télécharge directement ; pas besoin de navigateur.

  1. prepare_attachment_upload avec issue, filename, contentType (video/mp4) et size (octets exacts : stat -f%z <file> sur macOS, stat -c%s sur Linux).
  2. Dans les 60 s, curl -X PUT --data-binary @<file> "<uploadRequest.url>" avec chaque entrée uploadRequest.headers envoyée textuellement. Changer la casse d'un en-tête ou en omettre un donne une erreur 403. Téléchargez un fichier à la fois.
  3. create_attachment_from_upload avec issue et assetUrl.
  4. Pour le faire lire dans le fil, save_comment avec ![walkthrough.mp4](<assetUrl>) plus une légende d'une ligne. Ouvrez le ticket une fois pour confirmer qu'il s'affiche comme un lecteur.
  5. Remplacer une vidéo : téléchargez la nouvelle, modifiez le même commentaire (save_comment avec id), puis delete_attachment l'ancienne.

Slack

Avec le MCP Slack, après que l'utilisateur a confirmé le canal (le partage ne peut pas être annulé) :

  1. slack_get_file_upload_url avec filename et content_length (octets exacts).
  2. URL='<upload_url>' && curl -X POST "$URL" -H 'Content-Type: video/mp4' --data-binary @<file>. Conservez l'URL octet par octet ; une erreur 401 signifie qu'elle a été altérée ou a expiré, alors obtenez-en une nouvelle.
  3. slack_complete_file_upload avec file_id, channel_id (un identifiant C…/D…, pas un nom ; recherchez-le avec slack_search_channels), thread_ts et initial_comment optionnels.

Sans le MCP, donnez à l'utilisateur le chemin du fichier à glisser-déposer.

Ailleurs

Donnez le chemin du fichier et sa taille ; mentionnez scale: 1 / CRF=24 si la destination limite la taille.

Skills associés