videolabs : créer une vidéo (style cours terminal)
Tout se trouve dans un seul dépôt, /opt/opus/videolabs (github.com/bigzaqui/videolabs, public : ne jamais commiter d'identifiants ; exécutez scripts/check-secrets.sh avant de commiter). La source de cette compétence est skills/videolabs_create-video/ dans ce dépôt, liée dans ~/.claude/skills par scripts/setup.sh. Ses valeurs de configuration (clés ElevenLabs et Cloudflare, l'hôte de rendu) se trouvent dans le fichier .env (ignoré par git) à la racine du dépôt, voir .env.example.
Le dépôt contient : un moteur générique (outillage Remotion + ElevenLabs, un seul npm ci) et un dossier par laboratoire sous labs/. Utilisé pour « kafka - in a nutshell » (26 leçons / 18 min), « ceph - first steps » (14 leçons / 11.6 min) et, plus tôt, « ceph in a nutshell » (41 leçons / 30 min ; son script et ses visuels sont conservés dans reference/ comme exemple de cartes, comparatif, jauge, arbre, incident, états et sites). Les deux laboratoires actuels sont pratiques : chaque commande a d'abord été exécutée sur un laboratoire réel (voir « Laboratoire réel »).
/opt/opus/videolabs/
engine/ outillage générique : engine/cli.mjs <lab> <commande>, src/ (Remotion), scripts/
labs/<slug>/
lab.config.json entrée de catalogue (titre, description, tags) + apparence (marque, logo, barre d'état, noms des outils, extrait de coupe)
script/script.md narration, une scène par titre ###
visuals.ts ce que chaque scène montre + surcharges de repère
lab/ create-lab.sh, destroy-lab.sh, cap.sh, captures/, lab-meta.json
audio/raw/ sortie ElevenLabs (payante ; commitée dans git)
out/ data/ public/voiceover/ généré, ignoré par git
site/ scripts/publish-all.sh state/ le site et sa publication (compétence videolabs_publish)
script.md ──cli scènes──▶ data/scenes.json
└─ cli tts (timings de mots ElevenLabs) ─▶ audio/raw ──pace──▶ public/voiceover/* + data/{durations,captions}.json
visuals.ts + engine/src/cues.ts (révél = quand le mot est prononcé) ─▶ Remotion ──cli render──▶ out/<slug>.mp4
cli player ─▶ out/player/{chapters.json,lab.json} ──videolabs_publish──▶ videolabs.pages.dev
Toutes les commandes : node engine/cli.mjs <lab> <scenes|tts|pace|cues|stills|typecheck|studio|sample|render|player>, plus list et new <slug> (le README.md du dépôt contient le tableau). Le dossier du laboratoire est la seule chose qui diffère entre les cours ; ne copiez jamais le code du moteur dans un laboratoire.
Workflow (convenir du plan et du script avec l'utilisateur ; tout ce qui suit s'exécute sans demander)
Autorisation permanente de l'utilisateur : une fois le script approuvé, exécutez le tts ElevenLabs payant, l'audit des repères, le rendu sur l'hôte de rendu, l'étape de lecteur/affiche et le démantèlement du laboratoire sans demander la permission à chaque étape. Signalez les résultats et la nouvelle durée par la suite. Arrêtez-vous pour demander uniquement lorsque quelque chose sort de ce workflow (par exemple, une action destructive sur des éléments que le laboratoire n'a pas créés, ou la publication sur le site lorsque l'utilisateur ne l'a pas demandé).
- Sujet pratique ? Construisez et exécutez d'abord un laboratoire réel (voir « Laboratoire réel » ci-dessous). N'écrivez jamais les sorties de commandes de mémoire : la première ébauche du script ceph-first-steps était erronée à dix endroits une fois exécutée sur un vrai cluster. Rédigez ensuite le script à partir de ce qui s'est réellement passé.
- Le script d'abord, la vidéo ensuite. Proposez un plan (modules → leçons, avec le diagramme prévu par leçon), obtenez un accord, puis rédigez
labs/<slug>/script/script.md. Changer le texte est bien moins coûteux que de regénérer l'audio. Écrivez à la vitesse 1.0 avec des pauses ≈ 160 wpm : 1 500 mots ≈ 9,5 min, cartes de module incluses ; à 1.08 ≈ 168 wpm. Vérifiez la durée réelle avec la ligne « Paced … total X min » juste aprèsttset ajustez avant le rendu. Pour les commandes spécifiques à une version, vérifiez la version actuelle (docs et les téléchargements/tags d'image du projet) d'abord. Format (analysé parengine/scripts/build-scenes.mjs) :
Terminez la dernière leçon d'un module par « Point clé du module : … » — le mot point clé déclenche la fenêtre de point clé.## Module 1 — Architecture de base ### S02 · RADOS **Visuel :** notes pour le designer **Commandes :** `ceph -s` `ceph osd tree` (optionnel, à l'écran uniquement) **Narration :** Un paragraphe, écrit pour être parlé. Épelez les commandes telles qu'elles sont prononcées (« ceph dash s »). node engine/cli.mjs new <slug>génère la structurelabs/<slug>/(config, un script de trois scènes, un visuals.ts) ; remplissezlab.config.json(titre, description, tags pour le catalogue ;brand,user,logo,statusbar,statusCmd,networkLabels,session,tools,samplepour l'apparence) et exécuteznode engine/cli.mjs <slug> scenes.- Visuels dans
labs/<slug>/visuals.ts(types dansengine/src/types.ts,colorsdepuis@engine/theme), une entrée par identifiant de scène. Types de diagrammes (engine/src/components/Diagrams.tsx) :topology: hôtes (nom, ip, pastilles de démon, disques/réplicas) sur une ou deux lignes de réseau, une ligne d'état optionnelle (étiquettestatusCmd, par défaut depuis la config), et desflows(blocs se déplaçant entre les hôtes, decuejusqu'àuntil). Tout hôte/démon/disque/état peut avoir uncue(apparaît lorsqu'il est prononcé) et deschanges : [{cue, state, label}](disqueempty|osd|leader|idle|down|destroyed|new, hôteok|spare|down|maint). Les éléments sans repère sont visibles depuis le début. Construisez l'état du laboratoire avec des aides (host(n),built(n)) pour que le diagramme s'agrandisse leçon par leçon.session: un shell réaliste ; chaquecmdse tape lorsqu'il est prononcé, puis ses lignesouts'impriment ; conserve les lignes les plus récentes.hostdéfinit l'invite complète d'une ligne (par défautsession.promptdepuis la config).topologycompacte optionnelle au-dessus. La barre de commande inférieure est masquée pour les scènes de session. Autres types :title(carte fastfetch ;user,rowssupplémentaires),layers,cards,flow,tree,compare,gauge,terminal(sortie de commande avec annotations),incident(symptôme/vérification/action),checklist,states,sites;takeawayetcommandCuesoptionnels par scène. Utilisez des sorties réelles delab/captures/pour chaqueoutetsymptom.
- Examinez les visuels avant de payer pour l'audio :
node engine/cli.mjs <slug> stills(une image par scène à 60 % ;S04,S11 --at=12.5pour un instant précis) et disposez en planches contact 2×2 avec ffmpegxstackpour les regarder. Vérifiez les débordements, les sauts de ligne, les mises en page vides. - Voix :
.envà la racine du dépôt avecELEVENLABS_API_KEY/ELEVENLABS_VOICE_ID, puisnode engine/cli.mjs <slug> tts(exécutez-le directement, pas d'approbation nécessaire ; ignore les existants ;--only=S03,S04 --forcepour refaire uniquement les scènes dont la narration a changé, ce qui économise aussi des crédits). Il exécutepaceautomatiquement. Informez l'utilisateur de la nouvelle durée lorsque le script a grandi. Commitezlabs/<slug>/audio/(sortie payante). - Audit de synchronisation :
node engine/cli.mjs <slug> cues(après toute modification d'une commande, modifiez également son entréecommandCues: une puce pourdmesgtoujours synchronisée sur « smartctl » est une discordance silencieuse) — liste le mot qui révèle chaque élément et chaque ligne de commande (MISS= réparti entre les voisins). Corrigez les mauvaises correspondances avec des surcharges de repère (ci-dessous). Réexécutez après tout changement de narration ou de voix. - Prévisualisation :
node engine/cli.mjs <slug> sample(≈1 min sur le serveur de rendu), vérifiez ponctuellement les images avecffmpeg -ss <t> -frames:v 1,volumedetectpour les niveaux (pas d'écrêtage : max < -1 dB). - Rendu du cours complet (à distance, ci-dessous), puis
node engine/cli.mjs <slug> player, créez l'affichelabs/<slug>/out/spot.jpg, détruisez les machines virtuelles du laboratoire, et publiez l'ensemble du site avec la compétence videolabs_publish.
Règles de conception approuvées par l'utilisateur
- Apparence : Bureau Omarchy/Hyprland, palette Tokyo Night (
src/theme.ts; les noms sémantiques commerados,mon,cephfssont simplement des emplacements de palette). Barre Waybar avec des modules comme espaces de travail 0 à 7, un état (HEALTH_WARNsur le module de dépannage), compteur de leçon07/41. Une fenêtre en mosaïque, bordure carrée de 2 pixels, pas de rayon/lueur/dégradé. Chaque leçon s'ouvre avec une invite robbyrussell tapée➜ module-1-core-architecture git:(s03) cat the-daemons.md, puis un# Titre. Les notes s'affichent sous forme de// commentaires, les commandes sous forme de lignes❯ cmd, les points clés comme une fenêtre flottantecat sre-takeaway.md, les séparateurs de modules commecd module-N-…+ un titre énorme, l'ouverture comme fastfetch. Tout en JetBrains Mono ; Inter uniquement pour les sous-titres. Cela a remplacé une apparence « générique » à dégradé sombre que l'utilisateur n'aimait pas. - Synchronisation des révélations : jamais d'échelonnement dans le temps. Chaque élément apparaît lorsque le narrateur prononce son nom pour la première fois (
engine/src/cues.ts: le plus long jeton significatif, avec préfixe tronqué, recherché en avant à partir du repère précédent). Surcharges dans levisuals.tsdu laboratoire :cue: "=managers"mot parlé exact (les traits d'union/ponctuation sont ignorés :"=write-ahead"),cue: null= pas de mot, interpolation.pointCues(côtés comparatifs),noteCue(arbre),linkCue(sites),cues(badges de titre),commandCues(lignes de commande, par scène).- L'ordre des étiquettes dans
cueLabels()doit correspondre à l'ordre des index utilisé par le moteur de rendu dansengine/src/components/Visuals.tsx(topology/session utilisent un parcours indexé danstopologyKeys/sessionKeys). - Un curseur avant unique parcourt toutes les étiquettes d'une scène, donc une étiquette ultérieure réutilisant un mot courant (« trois », « reprise », « ajouter ») peut atterrir sur une occurrence ultérieure. Préférez un mot qui n'apparaît qu'une fois près du moment (« =répéter », « =commence »), et lisez toujours l'audit des
cues.
- Sous-titres : phrases simples, coupées aux fins de phrase/proposition, une ligne ≤ 64 caractères, pas de surbrillance karaoké mot par mot (« épuisant »).
- Respiration :
engine/scripts/pace.mjsinsère 250 ms après. ? !et 120 ms après: ;dans l'audio brute ElevenLabs et décale les timings des mots. Les points de coupure proviennent de ffmpegsilencedetect(strict -38 dB/40 ms, puis -30 dB/20 ms), sinon la fenêtre la plus silencieuse de 5 ms, avec des fondus de 8 ms — jamais à partir des seules limites de mots ElevenLabs (celles-ci atterrissent à l'intérieur des mots). AjustezSENTENCE_PAUSE/CLAUSE_PAUSE, réexécuteznode engine/cli.mjs <slug> pace(gratuit ; l'audio brute est conservée dansaudio/raw/). Le rythme dépend de la version de ffmpeg (7.1.5 et 8.0.1 ont donné respectivement 17,9 et 17,8 min pour le même audio Kafka) : rythmez là où vous effectuez le rendu, ou copiezdata/etpublic/voiceover/sur l'hôte de rendu. - Transitions : fondus enchaînés de 15 images via
@remotion/transitionsTransitionSeries; ils chevauchent uniquement le début (0,4 s) / la fin (0,8 s) silencieux.engine/src/timeline.tssoustrait les chevauchements ;engine/scripts/player.mjsetstills.mjsreflètent ces constantes — maintenez-les synchronisées. - Voix choisie par l'utilisateur : ElevenLabs « Mark »
v3p1kjzUvro6S76qmYmH(Voice Library ; fonctionne pour TTS bien queGET /v1/voices/{id}retourne 404),eleven_multilingual_v2, vitesse 1.08, point de terminaisonwith-timestamps,previous_text/next_textpour la continuité. Vérifiez toujours un identifiant de voix avec un appel TTS d'une phrase avant une exécution complète (un identifiant antérieur dans.envétait tronqué).
Paramètres par cours (labs//lab.config.json)
Les espaces de travail dans la Waybar et les compteurs de module/leçon fastfetch suivent les scènes automatiquement. Tout le reste spécifique au cours est constitué de données dans la configuration, jamais de code : brand (nom Waybar), user (invite sur la carte de titre), logo (cinq lignes de lettres en bloc), statusbar (texte ok/warn, texte right, warnModule = le module qui affiche l'avertissement), statusCmd et networkLabels (valeurs par défaut des scènes de topologie), session.prompt/session.title (sessions terminal), tools (mots de commande ignorés lors de l'auto-repérage d'une ligne de commande), sample (identifiants de scène de l'extrait Sample), plus title, description, tags pour le catalogue. Un nouveau laboratoire avec une apparence différente ne nécessite aucune modification du moteur ; un nouveau type de diagramme ou d'état en nécessite (engine/src/types.ts, components, cues.ts) et bénéficie ensuite à chaque laboratoire.
Pratiques Remotion appliquées (issues des compétences remotion-*)
Polices via @remotion/google-fonts (ne jamais compter sur les polices système) ; <Audio> depuis @remotion/media avec from ; TransitionSeries ; calculateMetadata dimensionne la composition à partir des durées audio ; animez avec interpolate/Easing.bezier et translate (pas de transitions CSS). Non adopté intentionnellement : Interactive.create/compositions connectées (utiles uniquement pour l'édition manuelle dans Studio), minima de texte pour les vidéos sociales volumineux, effets sonores de mèmes.
Rendu
VIDEOLABS_RENDER_HOST(cible ssh du serveur de rendu et de laboratoire, clés déjà configurées) provient du.envà la racine du dépôt : exécutezset -a; . /opt/opus/videolabs/.env; set +aavant les commandes ci-dessous, et écrivez cette variable, pas une adresse littérale, dans tout ce que vous commitez. Le même hôte exécute les machines virtuelles du laboratoire.TMPDIR=/var/tmpest défini par le CLI :/tmpest un petit tmpfs RAM sur ces hôtes et un long mixage audio échoue avec « No space left on device » à la toute fin.- Boîte locale (4 vCPU) ≈ 45 min pour 30 min de vidéo. Serveur de rendu distant $VIDEOLABS_RENDER_HOST (Ubuntu 26.04, 24 threads, 31 Go de RAM, RTX 3090 Ti, KVM ; Node 22, ffmpeg, rsync et les bibliothèques Chromium d'apt) effectue le rendu du cours ceph de 11,6 min en 3 min et du cours kafka de 18 min en 5 min avec une simultanéité de 12 à 16. Le dépôt se trouve sur
/opt/opus/videolabssur les deux machines. Flux :(Pour garder un timing identique à ce que vous avez examiné, rsyncrsync -az --delete --exclude node_modules --exclude out --exclude .env --exclude .git --exclude site --exclude state ./ $VIDEOLABS_RENDER_HOST:/opt/opus/videolabs/ # depuis /opt/opus/videolabs ssh $VIDEOLABS_RENDER_HOST 'cd /opt/opus/videolabs/engine && npm ci && cd .. && node engine/cli.mjs <slug> scenes && node engine/cli.mjs <slug> pace && node engine/cli.mjs <slug> render --concurrency=12 --log=error' rsync -a $VIDEOLABS_RENDER_HOST:/opt/opus/videolabs/labs/<slug>/out/<slug>.mp4 labs/<slug>/out/ && node engine/cli.mjs <slug> playerlabs/<slug>/dataetpublic/voiceoverau lieu d'exécuterscenes/pacesur le serveur.) Ne copiez jamais.envlà-bas. Exécutez les rendus longs de manière détachée (nohup bash script &démarré avecssh -f -n) et attendez avec un Moniteur sur la lignerealdetime; ne cherchez pas « error » dans le journal (la ligne de commande a--log=error). Si le code de la Waybar/du diagramme change, vérifiez les deux laboratoires (stills), car ils le partagent. - Vérification d'une modification du moteur : effectuez le rendu de l'extrait Sample avec l'ancien et le nouveau code et comparez avec
ffmpeg -i a.mp4 -i b.mp4 -lavfi ssim -f null -; lesscenes/durations/captions.jsonregénérés et l'audit descuesdoivent être identiques.
Laboratoire réel (labs/<slug>/lab/)
labs/<slug>/lab/
create-lab.sh construit les VMs, configure tout via cloud-init, copie les clés ssh, rapporte les IP
destroy-lab.sh détruit tout proprement (virsh destroy, undefine, zfs destroy les volumes)
cap.sh [cmd] exécute la commande sur un nœud du lab, l'enregistre dans captures/ avec un saut de ligne et un préfixe `❯ ` ajouté ; un seul appel par capture ; donne la sortie exacte à utiliser dans script.md
captures/ une capture par commande ; fichier plat avec le préfixe `❯ ` sur la ligne cmd, puis la sortie brute
lab-meta.json généré par create-lab.sh, utilisé par la remédiation ; contient les IP, le nom du nœud de contrôle, l'image de l'OS, le type de distribution, la version de l'outil, l'état du lab (« up »/« recreated »/« destroyed »)
Le laboratoire KVM (QEMU/libvirt, Ubuntu 26.04, ZFS) du ler suivi : 1 contrôleur + 3 nœuds de données avec Ceph Reef installé via cephadm. Il est sur le même $VIDEOLABS_RENDER_HOST (donc son cpu, sa ram et son stockage ZFS). Avant la reconstruction, vérifiez que des captures de sortie valides peuvent être faites sur le lab existant : si lab-meta.json indique « up », testez une commande courte comme ceph -s via ssh -i labs/<slug>/lab/ssh-key <IP> ceph -s — si elle réussit et montre la version prévue, vous pouvez exec cap.sh sur le lab réutilisé plutôt que d'attendre 150 s de cloud-init. Utilisez nohup bash create-lab.sh & pour les constructions longues ; une reconstruction complète « from scratch » est coûteuse (150 s de cloud-init), préférez create-lab.sh --recreate qui conserve les VMs mais les remet à zéro en 30 s en détruisant et rechargeant les snapshots ZFS. N'exécutez pas destroy dans l'étape de construction du lab. cap.sh n'extrait la sortie que du nœud de contrôle. Les VMs du lab n'ont pas de sortie internet — si une capture est sur le réseau public (p. ex. apt, docker pull), exécutez-la localement.
Pour un sujet pratique
- Demandez à l'utilisateur si le lab doit être reconstruit (cela prend 2-3 min pour le recréer, 30 s pour le remettre à zéro). Une fois que l'utilisateur accepte, exécutez
create-lab.sh. - Les sorties de commandes sont indexées par la commande :
cap.sh ceph -scrée un fichier parlant qui devient votre source de vérité. Chaqueoutdansscript.mddoit correspondre à un fichier de capture. N'inventez pas de sorties. - La résilience du lab : si le lab tombe en panne ou que des VMs meurent pendant la capture,
lab-meta.jsoncontient les adresses IP pour une recréation. Exécutezcreate-lab.sh --recreatepour réinitialiser. Les nouvelles sorties remplacent les anciennes captures. - Lorsque la capture est terminée et que le script est validé, détruisez toujours les VMs via
destroy-lab.shdans l'étape de démantèlement, après le rendu.
(Le matériel de laboratoire est conçu pour le dépannage vit et doit être maintenu avec le script de laboratoire ; un lab qui ne correspond pas à la démo n'est pas un bug de la compétence, mais un script obsolète.)
Règles de la boîte à outils de l'agent
- Pré-remplissage : à l'ouverture de la compétence, si le répertoire de la compétence a été récemment mis à jour (git log -1 dans /opt/opus/videolabs — stockez le hash), alors imprimez « MISE À JOUR PRÊTE —— dernières modifications : ». Utilisé pour signaler les changements dans le moteur commun et les modèles.
- Usure de la clé API : après chaque appel
ttsréussi, enregistrezelevenlabs_calls_todayet le coût estimé (nombre de caractères * 0,3 $ / 1 000) dansstate/{date}.json. Au départ, avant tout tts, lisezstate/{date}.json. Si la somme du jour dépasse 25 $, imprimez « ATTENTION — Limite de dépense ElevenLabs dépassée. Les prochains TTS seront refusés. Voulez-vous continuer ? » et attendez confirmation. Par défaut, l'utilisateur a accepté. - Nouvelle compétence : enregistrement local d'erreur et mesure des performances (
state/errors.json) — un message d'erreur 'ÉCHEC (sortie #N) : ', pas de panique. - Gardez toujours les messages de notification de progression dans le flux de travail ci-dessus. Ne cachez pas la sortie. N'inventez pas de nouvelles étapes ou de nouveaux drapeaux.
Dépannage et erreurs courantes
| Symptôme | Cause probable | Action |
|---|---|---|
| Le rendu échoue avec « No space left on device » à 99 % | TMPDIR non défini sur /var/tmp | export TMPDIR=/var/tmp ; les longues vidéos poussent /tmp d'un hôte (tmpfs RAM) à la limite |
| Les sous-titres sont désynchronisés après un nouveau pace | data/durations/captions.json obsolète après pace ; nécessite scenes | Exécutez node engine/cli.mjs <slug> scenes pour régénérer ou rsync depuis le rendu |
L'audio tts saute des mots ou a des clics | Ralentissement du réseau ; limite de taux ElevenLabs | Réexécutez tts avec --only=<scène> — le service peut reset un fichier corrompu sur nouvelle tentative |
| Le rendu distant « claque » la fenêtre ssh | Connexion interrompue ; nohup non utilisé | Utilisez nohup ; reconnectez-vous et jobs / fg ; le rendu continue sur le serveur |
| Le lab ne répond pas ou les VMs sont mortes | Cloud-init a échoué ; lab-meta.json manquant | Exécutez create-lab.sh --recreate et annulez destroy-lab.sh dans l'étape de capture |
| La sortie de la commande dans la vidéo ne correspond pas à la narration | commandCues obsolètes dans visuals.ts | Exécutez l'audit cues ; mettez à jour la ligne de commande et son repère |
| Le rendu local est très lent | Limité par CPU ; pas de GPU | Utilisez le serveur distant $VIDEOLABS_RENDER_HOST avec le GPU ; il fait 3 min vs 45 min |
| Le rendu prend beaucoup plus de temps que prévu | Concurrence trop basse ; ou scène unique très longue | Définissez --concurrency=12 ; les scènes > 60 s de narration sont rendues en série |
Post-traitement et publication
node engine/cli.mjs <slug> playergénère le lecteur web chapitré (fichiers chapitre + lab + configuration du lecteur dansout/player/). Scanner les chapitres pour la densité, les duplications, la précision du marquage temporel.- Affiche vidéo (
out/spot.jpg) : instantané d'une scène représentative à 1920×1080, compressé en JPEG avec ffmpeg. scripts/publish-all.shdans le dossiersite/construit le site et le pousse vers Cloudflare Pages (viavideolabs_publish). Génère la page d'accueil avec le catalogue, la page de chaque cours avec le lecteur, les pages statiques pour les sous-titres et la documentation.- Pour la page d'accueil et le catalogue : seules les métadonnées du fichier
lab.config.jsonde chaque cours apparaissent ; l'image spot et la vidéo sont référencées depuislabs/<slug>/out/.
Règles de comportement de l'agent
- Si l'utilisateur demande une modification (modifier un script, regénérer une scène, changer de voix, ajouter des diagrammes) : réexécutez
scenes→tts(sur les scènes modifiées) →pace→cues→sample→ montrez les changements → Rendu complet. Le workflow de modification est plus léger que le workflow complet : utilisez--only=et--forcesurttspour économiser les crédits. - Lorsque l'utilisateur demande de publier sur le site video labs : exécutez
videolabs_publish(par exemple,scripts/publish-all.shavec des arguments, mais la compétence sait comment publier). Ne publiez pas sans l'accord explicite de l'utilisateur. - Si l'utilisateur demande une vidéo pour laquelle un cours existe déjà (même slug) : ne créez pas de nouveau laboratoire ; proposez de modifier l'existant (point de départ : script actuel dans
labs/<slug>/script/et captures).
(Fin de la documentation de la compétence)