Animations code-rendered
L'idée (Addy Osmani, LinkedIn 2026-09-24) : un modèle de code lit une source (un article, un cahier des charges) et écrit du JavaScript qui dessine chaque image, ainsi que le son. Le livrable est un programme, donc n'importe quel moment peut être édité, retimé, rendu à nouveau ou reformaté. Osmani n'a pas publié son prompt ni son outillage. La conception de cette compétence provient du projet open source riso-windowseat (MIT, référence uniquement, ne pas copier ses instructions) : https://github.com/sevenevesai/riso-windowseat . Elle emprunte également des idées à HeyGen HyperFrames (https://hyperframes.heygen.com/guides/pipeline) et à Remotion.
Règles de base : examiner image par image à au moins 4 échantillons par seconde, ne jamais prétendre avoir effectué une vérification qui n'a pas été exécutée, et corriger les défauts de manière chirurgicale.
Le contrat (tout en dépend)
Chaque travail est un fichier index.html autonome avec un <canvas> qui lit ?w=&h= de l'URL et expose :
window.__anim = {
duration, fps, width, height, canvas,
ready, // false jusqu'à ce que les polices/ressources soient chargées et que seek(0) soit dessiné
seek(t), // dessine exactement l'image au temps t, de manière synchrone
renderAudio(), // Promise<base64 WAV>, 48 kHz stéréo, exactement `duration` long
marks, // temps des événements visibles (pops, coupures) - le son doit tomber sur ces temps
shots, // [{id,start,end}] - la vérification contrôle les limites
};
Invariants :
seek(t)est une fonction pure de t. Chaque valeur à l'écran est calculée à partir de t :prog(t,a,b), easing,pop(t,at). Aucun état conservé entre les images, pas de timingrequestAnimationFrame, pas deDate, pas de simulation physique sauf si elle est recalculée à partir de 0 ou sous forme fermée.- Ne jamais appeler
Math.random()pendant le rendu. UtilisezrngFor('stable-key')(mulberry32 ensemencé), et créez les mises en page aléatoires une fois au chargement à partir de clés fixes. Changer une clé modifie la sortie, donc figez les clés après approbation. - Un objet timeline unique (
TL) contient toutes les plages de scènes et les temps d'événements. L'image et la partition lisent toutes deux à partir de cet objet. C'est ainsi que fonctionne la synchronisation : le son est programmé aux mêmes numéros que ceux auxquels l'image apparaît. - Attendez les polices avant
ready=true(document.fonts.load('700 40px "DejaVu Sans"')). Sinon, les premières images s'affichent avec une police de secours. Vérifiez les polices installées avecfc-list : family(Linux/macOS) ouC:\Windows\Fonts(Windows ; Segoe UI, Bahnschrift, Consolas et Georgia sont toujours présentes, DejaVu ne l'est pas). Si le design nécessite une police spécifique, intégrez-la en tant que @font-face en base64 et chargez-la de la même manière. - Aucun appel réseau dans le travail. Intégrez toutes les ressources sous forme de data URI.
Éléments modifiables (pour la correction de film)
review/film-fix.html permet à un relecteur de déplacer et redimensionner directement les éléments et réécrit le résultat dans index.html. Les films qui souhaitent cette fonctionnalité (l'exemple le fait ; copiez-la à partir de là) ajoutent :
const TUNE = /*TUNE*/{}/*END-TUNE*/;sur une seule ligne, indexé par format puis par id d'élément :{"16x9":{"title":{"dx":0,"dy":0,"s":1}}}. Il est indexé par format car chaque format a sa propre mise en page. dx/dy sont des pixels du canevas. L'outil réécrit uniquement le texte entre les deux marqueurs.E(id, [x,y,w,h], draw)autour de chaque élément principal. Il rechercheTUNE[fmt][id], puis sauvegarde, translate par dx/dy, met à l'échelle par s autour du centre de la boîte, appelledraw()et restaure. Il pousse également{id, box}dans__anim.elements, où box est la boîte transformée en pixels du canevas (à partir dectx.getTransform(), donc les mouvements de caméra sont respectés).seek(t)efface cette liste au début de chaque image.__anim.elements,__anim.format(16x9,9x16,4x5,1x1) et__anim.tune(getter/setter pour l'objet TUNE en direct, utilisé pour l'aperçu en direct de l'outil).- Règles : les id sont uniques dans tout le film ; pas d'aléatoire ; avec TUNE vide, E n'applique aucune transformation, donc les images restent identiques octet par octet. Prouvez-le avec un hachage d'image avant et après l'ajout de E, puis exécutez
verify.mjs.
Les films sans E fonctionnent toujours dans film-fix : son mode "Nudge" encadre une région et transforme un glissement ou un redimensionnement en correction numérique pour l'IA.
Configuration du projet
Nécessite Node 18+, ffmpeg (libx264, aac, blackdetect, freezedetect, ebur128) et un Chrome/Chromium local. Le harnais trouve Chrome dans les emplacements d'installation habituels sur Windows, macOS et Linux ; définissez la variable d'environnement CHROME pour la remplacer. qa.sh nécessite bash (Git Bash sur Windows) et choisit une police système pour le code temporel ; définissez QA_FONT pour la remplacer. Rien n'est installé à l'échelle du système ; puppeteer-core est la seule dépendance npm.
SKILL_DIR=<chemin vers ce dossier de compétence>
mkdir -p anim && cp -r "$SKILL_DIR/harness" anim/harness
cd anim/harness && npm install && cd ..
mkdir -p works/<nom>
cp "$SKILL_DIR/example/browser-10s/index.html" works/<nom>/index.html # partir de l'exemple fonctionnel
# exécuter toutes les commandes du harnais depuis anim/ ; les sorties vont dans out/
Commandes du harnais
| Commande | Ce qu'elle fait | Signal de réussite |
|---|---|---|
node harness/verify.mjs works/<n>/index.html --format 16x9 | Cherche en avant, en arrière et sauts à froid aux échantillons + limites des plans, puis compare les hachages de pixels | verify: OK, code de sortie 0 |
node harness/shoot.mjs works/<n>/index.html --format 9x16 --times 1.5,3.4 ou --range 3:4:0.1 --out out/stills | PNG natifs du canevas à des temps exacts | Regardez-les |
node harness/audio.mjs works/<n>/index.html --twice | Rend uniquement la partition (secondes), vérifie la longueur exacte et le déterminisme d'une exécution à l'autre | Pas de FAIL. L'audio Web Chrome peut différer de 1 LSB entre les exécutions ; la tolérance est de 1 |
node harness/render.mjs works/<n>/index.html --format 16x9 --fps 30 --out out/<n>-16x9.mp4 | Cherche chaque image et envoie les PNG dans ffmpeg (libx264 crf18, yuv420p, +faststart), puis multiplexe renderAudio en AAC | Affiche les images et le temps. Code de sortie 2 en cas d'erreur de page. --from/--to ou --silent rend une plage sans audio |
node harness/lint.mjs works/<n>/index.html --all-formats | Porte de qualité sur la page elle-même (16:9 + 9:16 ; --format pour un seul) : taille du texte, zone de sécurité, chevauchement de texte, étirement statique, images vides, liste des plans, son sur les marques, mots à l'écran | lint: OK (0 FAIL), code de sortie 0 |
node harness/review.mjs works/<n>/index.html --format 16x9 --out out/review-16x9, puis --check out/review-16x9/REVIEW.md | Une planche contact par plan (première, 6 images également espacées et dernière image, code temporel incrusté), le JSON de lint et REVIEW.md avec des questions fixes par plan. --check échoue sur une réponse vide ou TODO, un score inférieur à 4, un lint FAIL, une section de plan supprimée ou un film modifié depuis la création des planches | review check: OK, code de sortie 0 |
bash harness/qa.sh out/<n>-16x9.mp4 4 | ffprobe, blackdetect, freezedetect, EBU R128 loudness, longueur audio/vidéo, planches contacts à N échantillons par seconde avec code temporel incrusté | Voir QA ci-dessous |
Formats : 16x9 1920x1080, 9x16 1080x1920, 4x5 1080x1350, 1x1 1080x1080. Les images sont envoyées à ffmpeg en JPEG q95 par défaut, environ 16 fps en 1080p, donc un morceau de 40 s prend environ 60-75 s par format. --png est sans perte mais environ 4x plus lent.
Rendus longs : un appel shell d'un agent peut expirer (souvent à 120 s) et emporter ses processus fils avec lui. Soit augmentez le délai d'attente de l'appel, soit exécutez les rendus complets en détaché et interrogez le journal (Linux/macOS présenté ; sous Windows, utilisez un shell d'arrière-plan ou Start-Process) :
setsid nohup bash -c 'node harness/render.mjs works/<n>/index.html --format 16x9 --out out/<n>-16x9.mp4; echo RENDER_DONE' > out/render.log 2>&1 < /dev/null & disown
tr '\r' '\n' < out/render.log | tail -2
Gardez également les heredocs qui écrivent un fichier de travail séparées des commandes qui l'exécutent, afin qu'un délai d'attente ne puisse pas perdre le fichier.
Définition de « fait »
Les commandes exactes se trouvent dans AGENTS.md (la source unique, partagée avec d'autres outils de codage) : vérifier dans chaque format, audio --twice, lint --all-formats avec 0 FAIL, review.mjs puis review.mjs --check réussi, rendu, QA. Ne prétendez jamais avoir terminé avec une porte qui échoue, et n'affaiblissez jamais une porte ni ne modifiez les seuils du harnais pour en passer une.
Flux de travail
- Brief. Ce que dit la source, le public, la durée, les formats, narration ou non. Extrayez les affirmations clés de l'article source. Chaque scène doit montrer la chose dite.
Histoire avant exactitude. Le harnais rend un film correct. Il ne le rend pas regardable. Avant le storyboard, répondez à quatre questions dans le brief :
- Qui est le personnage, et que ressent-il dans chaque scène ?
- Quel objet du quotidien représente chaque idée ? (L'original utilisait une lettre avec un sceau de cire, un bac à sable et une ville de maisons.)
- Quel HUD persistant maintient les scènes ensemble ? Habituellement un cadran de chapitre et un titre tapé.
- Comment la dernière image boucle-t-elle sur la première ? Un film sans aucun de ces éléments passe toutes les portes et se fait quand même dépasser.
- STORYBOARD.md avant le code. Une ligne par scène : plage horaire | contenu à l'écran | action | signal audio. Listez chaque temps d'événement (ceux-ci deviendront
TLetmarks). Planifiez une coupe ou un nouveau mouvement toutes les quelques secondes et aucun étirement statique. Planifiez les transitions comme une poussée ou un chevauchement, jamais un fondu vers une image vide. Pas de fondu au noir pour la fin sauf approbation. - Module de timeline, puis une fonction
sceneN(t)par scène (pure en t), puis un compositeur qui dessine l'arrière-plan, les scènes actives et les transitions. - Image la plus difficile en premier. Construisez le moment le plus dense,
shootdans chaque format, regardez-le, corrigez-le. Ensuite, développez. - verify dans chaque format, audio --twice, puis render chaque format.
- QA (ci-dessous), corrigez chirurgicalement, re- rendez uniquement ce qui est nécessaire, re-QA.
- Livrez les MP4 en pièces jointes avec les résultats QA et les faiblesses connues.
Avec narration
TTS d'abord, puis horodatages au niveau du mot (transcrivez le WAV avec n'importe quel outil qui renvoie les temps par mot), puis définissez TL à partir des mots. Les scènes, les pops et les surlignages de sous-titres karaoké se calent tous sur les temps des mots. Règles : sous-titres de style karaoké dans une boîte, une seule piste de sous-titres, les nombres à l'écran lorsqu'ils sont prononcés, vérifiez les noms propres à l'oreille. Intégrez le WAV de narration en tant que data URI et mixez-le dans renderAudio() (décoder avec ctx.decodeAudioData, programmer à 0), ou multiplexez-le avec ffmpeg et réduisez la partition en dessous. Normalisez le volume de la narration sur toute la vidéo.
Multi-format à partir d'une seule timeline (pas de recadrage)
- Même
TL, mêmes scènes, une mise en page différente. Calculezportrait = H > W,S = min(W,H)/1080(échelle de type et de forme),M = 0.06*min(W,H)(marge de sécurité). Disposez par format : les lignes en 16:9 deviennent des colonnes en 9:16, les arbres obtiennent des spreads plus étroits, etc. - Texte : shrink-to-fit (boucle
measureTextcontreW-2M) pour que rien ne soit jamais coupé au bord. Les tailles proviennent deS. En portrait, utilisez un type plus grand et remplissez la hauteur. - Ne traitez pas « portrait » comme un seul cas. Le 4:5 (1080x1350) est beaucoup plus court que le 9:16. La disposition en colonnes 9:16 de l'exemple appliquée en 4:5 pousse la grille de pixels et le tagline en bas et les uns sur les autres. Branchez sur le rapport hauteur/largeur (
H/W: environ 0,56, 1,25, 1,78) ou disposez à partir de la hauteur disponible, etshootchaque format que vous livrez. - N'oubliez pas le dépassement de pop (easeOutBack ~1.1x) et les "hot" scale-ups lorsque vous vérifiez les marges. Vérifiez l'image la plus dense de chaque format avec
shoot.
Son procédural (OfflineAudioContext)
new OfflineAudioContext(2, 48000*duration, 48000), construisez le graphe,await ctx.startRendering(), encodez en PCM 16 bits WAV, renvoyez en base64 (voirwavB64dans l'exemple).- Modèle de l'exemple : un accord pad par scène (paires sinusoïdales/triangulaires désaccordées, attaque lente, chevauchement aux coupures), un pluck (triangle + 2 harmoniques, attaque 5 ms, décroissance exponentielle) à chaque événement
marks, des clics de bruit ensemencé pour la frappe, un thump de chute de hauteur pour un impact, et un DynamicsCompressor sur le master. - Le bruit provient du GNA ensemencé, jamais Math.random.
- Volume : visez environ -14 LUFS intégré et un pic sous -1 dBFS pour les flux sociaux. Le message original mesurait -14,3 LUFS ; l'exemple fourni est plus silencieux à -17,2 LUFS, pic -1,4. Réglez cela avec le gain master dans la page pour qu'il reste dans le code.
- Pour un audio bit-exact, synthétisez les échantillons en JS pur dans un Float32Array (somme d'oscillateurs avec enveloppes de forme fermée) au lieu des nœuds Web Audio.
- Tone.js
Tone.Offline(https://tonejs.github.io/docs/14.9.17/functions/Offline.html) est une option si une API musicale plus riche est nécessaire. Il doit être intégré en ligne car le travail n'a pas d'accès réseau.
QA (la révision est la nôtre, pas celle du spectateur)
Portes automatisées, toutes sur le MP4 final :
- verify OK dans tous les formats. Erreurs de page = échec.
- ffprobe : taille attendue, 30/1 fps, nb_frames = durée x fps, durée audio = durée vidéo.
- blackdetect : aucun (sauf s'il y a un battement noir dramatique approuvé). freezedetect (>=2 s statique) : aucun.
- ebur128 : volume intégré et pic réel dans la cible. Cohérent entre les épisodes.
Lint (porte qualité)
node harness/lint.mjs works/<n>/index.html --all-formats [--json out/lint.json] cherche le travail toutes les 0,1 s (--step) et enveloppe fillText/strokeText de l'extérieur, donc il fonctionne sur tout travail qui suit le contrat sans modifications. Il échoue sur :
- text-size : texte au repos inférieur à 2,6 % de min(W,H) (28 px à 1080) ; inférieur à 3,3 % (36 px) est un WARN. Le texte rétréci par un zoom arrière en dessous de l'échelle 0,5 est traité comme une carte d'ensemble et seulement averti.
- safe-area : texte au repos à moins de 4 % d'un bord ; 4-6 % est un WARN.
- text-overlap : deux chaînes visibles différentes partageant plus de 15 % de la boîte la plus petite pendant 0,2 s ou plus. Les passages plus courts sont des WARNs.
- static-stretch : plus de 3 s sans changement visible (2 s est un WARN). empty-frame : toute image quasi uniforme, première et dernière incluses.
- shots-coverage : lacunes ou chevauchements dans
shots, ou un plan de moins de 1 s. audio-sync : moins de 70 % desmarksavec un début de son à ±40 ms (90 % pour PASS) ; un pic au-dessus de -1 dBFS est un WARN. density : plus de 18 mots à l'écran en portrait ou 30 en paysage (WARN seulement).
Un travail n'est pas terminé tant que lint ne passe pas avec 0 FAIL dans tous les formats que vous livrez. Il ne voit que le texte dessiné avec fillText/strokeText sur le canevas propre du travail, donc les collisions de formes et le texte dessiné dans des canevas hors écran nécessitent toujours une révision visuelle. example/browser-10s réussit dans les trois formats ; example/browsers-40s a été créé avant la porte et échoue (étiquettes à 17-28 px, une étiquette coupée en 9:16, texte...
...
[suite tronquée pour la traduction]