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 :
- 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) ?
- Voix ? Uniquement des sous-titres, ou une narration (voix-off IA) ? Les vidéos clients sont toujours narrées.
- 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. - 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
-
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.
-
Écrivez un script de flux basé sur
scripts/record-lib.mjs(copiezexamples/playwright-site/flow.mjs, un enregistrement exécutable d'un site public). Utilisezstep(id, caption, action)(oucaption()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 leclick(locator)/hover(locator)du flux, pas aveclocator.click(): ils font glisser le curseur dessiné vers la cible d'abord. Unlocator.click()nu fonctionne toujours mais le curseur saute.zoom(locator, { scale })se rapproche doucement d'une cible,unzoom()revient en arrière, etclick(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
zoomsur 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.clickettypeenregistrent chaque clic et frappe dans<video>.sfx.json, etnarrate.mjsplace les sons enregistrés sous la voix : des échantillons de clic de souris et de clavier mécanique enregistrés depuissounds/.SFX_VOLUMEles ajuste (par défaut 1),SFX=0pour aucun.page.keyboard.typeetlocator.click()nu ne produisent aucun son. -
node flow.mjsaffiche le chemin du fichier.mp4. Il nécessite ffmpeg ($FFMPEGou 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)
- É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"). GOOGLE_API_KEY=… node <skill-dir>/scripts/tts.mjs script.json <audio-dir>écrit un WAV par ligne etdurations.json. Gemini 3.8 Flash TTS (TTS_MODEL,TTS_VOICEpour remplacer) a semblé plus naturel qu'OpenAIgpt-4o-mini-tts. Obtenez une clé sur aistudio.google.com.- Enregistrez avec
{ narration: '<audio-dir>/durations.json' }. Chaquestepattend que sa ligne soit terminée, et les heures de début atterrissent dans<video>.mp4.marks.json. 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
systemInstructionrenvoie 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.mjsextrait 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('')avantpressSequentially, 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()vialocator.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 = undefinedavant 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.comet des numéros en555. 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=1dans 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.mjspour les lignes qui les décrivent. - zsh ne fait pas la séparation par mots de
$VAR. Passez les temps d'images àframes.mjslittéralement, ou bouclez sousbash.
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 })depuisscripts/gmail-scene.mjs. Fournissez-lui le vrai rendu de l'e-mail de l'application. Il réécrittarget="_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. Passezmailbox: 'sam'pour plusieurs boîtes aux lettres (servies à/samet/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 jamaisload. 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=1le 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.
prepare_attachment_uploadavecissue,filename,contentType(video/mp4) etsize(octets exacts :stat -f%z <file>sur macOS,stat -c%ssur Linux).- Dans les 60 s,
curl -X PUT --data-binary @<file> "<uploadRequest.url>"avec chaque entréeuploadRequest.headersenvoyé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. create_attachment_from_uploadavecissueetassetUrl.- Pour le faire lire dans le fil,
save_commentavecplus une légende d'une ligne. Ouvrez le ticket une fois pour confirmer qu'il s'affiche comme un lecteur. - Remplacer une vidéo : téléchargez la nouvelle, modifiez le même commentaire (
save_commentavecid), puisdelete_attachmentl'ancienne.
Slack
Avec le MCP Slack, après que l'utilisateur a confirmé le canal (le partage ne peut pas être annulé) :
slack_get_file_upload_urlavecfilenameetcontent_length(octets exacts).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.slack_complete_file_uploadavecfile_id,channel_id(un identifiantC…/D…, pas un nom ; recherchez-le avecslack_search_channels),thread_tsetinitial_commentoptionnels.
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.