18 septembre 2026 · 9 min de lecture
Une skill d'agent ne livre pas un résultat, elle livre un chantier
Retour d'expérience chiffré sur la skill scroll-world : contourner la chaîne payante, huit coutures mesurées au PSNR, et quatre pièges trouvés en production.
Les skills d'agent se multiplient. On en installe une, on lui donne un sujet, et elle produit quelque chose. La question intéressante n'est pas de savoir si ça marche, mais ce qu'il reste à faire une fois que ça a marché.
Ce site a été refait avec scroll-world, une skill qui construit une page où le défilement pilote une caméra traversant des décors générés. Le travail a été mené en binôme, une direction humaine et un agent aux commandes. Voici ce que le chantier a réellement coûté, avec les mesures.
Le contexte en trois lignes
La skill décrit une chaîne complète : générer des décors, générer les vols de caméra entre eux, puis câbler un moteur qui rejoue le tout au défilement. Le point critique, celui qu'elle répète, est que les raccords entre deux clips doivent être identiques à la frame près. Sinon le spectateur voit une coupure et l'illusion du vol continu s'effondre.
Premier écart : la chaîne payante n'était pas installée
La skill fait passer les vidéos par un service payant, de l'ordre de 27 dollars pour une poignée de scènes. Les deux CLI concernés étaient absents de la machine, et leur authentification est interactive.
Plutôt que d'installer et de payer, les neuf clips (cinq plongées et quatre traversées) ont été rendus localement avec ffmpeg, à partir des décors fixes générés par ailleurs. Un zoompan qui avance dans l'image, une fondue pour passer d'un décor au suivant.
Ce que ça change sur les raccords
C'est ici que le contournement devient plus intéressant que la voie officielle. Quand une IA génère un clip, la dernière frame du vol et la première frame du suivant sont deux images produites séparément. Elles se ressemblent, elles ne sont pas identiques. La skill le sait et donne une calibration : sur un build vérifié bon, un raccord correctement verrouillé se mesure autour de 18 à 25 dB de PSNR, le reste étant du fourmillement de détail.
Avec un rendu local, la dernière frame d'un clip et la première du suivant sont calculées par la même formule, avec les mêmes paramètres, depuis la même source. Elles sont identiques par construction. Le seul écart résiduel est le bruit de quantification du codec.
Mesure sur les huit coutures de la chaîne desktop :
| Couture | Plongée vers connecteur | Connecteur vers plongée suivante |
|---|---|---|
| 1 | 35,51 dB | 37,09 dB |
| 2 | 37,63 dB | 37,31 dB |
| 3 | 38,39 dB | 37,10 dB |
| 4 | 37,09 dB | 36,47 dB |
Toutes les valeurs sont très au-dessus de la fourchette de référence. Le contournement ne dégrade pas le résultat, il le rend meilleur que ce que la chaîne prescrite peut garantir.
La leçon générale vaut au delà de cette skill : quand une procédure impose un service externe pour produire un asset, il vaut la peine de se demander si l'asset ne serait pas mieux obtenu par un calcul déterministe. Une IA excelle à inventer un décor. Elle n'apporte rien à un mouvement de caméra, qui est de la trigonométrie.
Le piège des couches CSS
Le moteur de la skill encapsule tout son CSS dans une couche nommée, @layer sw. Intention louable : les styles du projet hôte doivent pouvoir le surcharger sans bataille de spécificité.
Sauf que le Preflight de Tailwind, lui, n'est pas layerisé. Et la règle de la cascade est sans appel : du CSS non layerisé l'emporte toujours sur une couche, quelle que soit la spécificité. Un sélecteur de classe dans une couche perd contre un sélecteur d'élément hors couche.
Résultat mesuré dans le navigateur, pas deviné :
- les titres du moteur retombaient à 16 px, parce que Preflight remet
font-size: inheritsur les balises de titre ; - toutes les bordures disparaissaient, parce que Preflight pose
border-width: 0sur l'univers ; - les images et vidéos passaient en hauteur automatique, 817 px au lieu des 936 px attendus, laissant apparaître le fond sous la scène.
Le correctif tient en une vingtaine de lignes qui redéclarent, hors couche, exactement ce que Preflight casse. Rien de plus, sinon on neutralise au passage les règles mobiles du moteur, ce qui est arrivé une première fois : la colonne de texte est restée à 46 % de la largeur sur un écran de 390 px, soit 179 px, parce qu'une surcharge desktop écrasait la mise en page mobile prévue par le moteur.
Si vous intégrez un composant tiers qui utilise @layer dans un projet Tailwind, c'est le premier endroit à regarder.
Le fantôme : une heure perdue sur un bug qui n'existait pas
Au premier chargement, le décor ne s'affichait pas. Écran vide, texte visible, image absente. Le fichier était bien téléchargé, décodé, positionné, avec une opacité de 1. Un défilement d'un cran le faisait apparaître.
Deux correctifs ont été écrits et déployés avant que la bonne question soit posée. La réponse tenait dans une ligne :
document.visibilityState // "hidden"
L'onglet piloté par l'automatisation n'était pas visible, et un navigateur bride requestAnimationFrame à zéro dans un onglet masqué. Le moteur pilote ses recherches d'image dans une boucle rAF. Rien ne tournait, donc rien ne se repeignait. Les deux correctifs ont été annulés.
La leçon n'est pas sur ce bug précis. Elle est sur l'ordre des opérations : avant de corriger, vérifier que l'environnement d'observation ne fabrique pas lui-même le symptôme. Une heure de perdue, et deux commits inutiles dans l'historique si personne ne les avait retirés.
Le piège MIME, invisible en local
Le moteur charge chaque clip avec fetch() puis blob(), et joue le blob. Le type du blob est celui de l'en-tête Content-Type de la réponse.
En local, le serveur de développement sert video/mp4. En production, Azure Static Web Apps servait :
Content-Type: application/octet-stream
X-Content-Type-Options: nosniff
Le second en-tête interdit explicitement au navigateur de deviner le vrai type. Un type non reconnu peut faire refuser le décodage à l'élément vidéo, Safari étant le plus strict sur ce point.
Correctif : déclarer le type dans la configuration de l'hébergeur.
"mimeTypes": { ".mp4": "video/mp4", ".webm": "video/webm" }
Honnêteté sur ce point : il n'a pas été possible de prouver que la lecture était réellement cassée, certains navigateurs reniflant tout de même le contenu d'un blob. Mais servir un mp4 en flux d'octets avec nosniff est faux, et c'était le risque le plus sérieux sur iPhone.
Ce défaut n'existait qu'en production. Aucun test local ne pouvait le révéler.
Le gâchis du recadrage mobile
Les clips étaient en 16:9. Sur un téléphone, ils sont affichés en object-fit: cover, donc recadrés. Le calcul, sur un écran de 390 par 844 points avec un clip de 720 par 406 :
- pour couvrir, l'image est mise à l'échelle sur la hauteur, facteur 844 / 406, soit environ 2,08 ;
- la largeur affichée devient 720 × 2,08, soit environ 1497 points ;
- la part réellement visible est 390 / 1497, soit 26 % ;
- sur un écran à densité 3, ces 390 points valent 1170 pixels, tirés d'environ 187 pixels source, soit un agrandissement de 6,3 fois.
Autrement dit : on décodait environ 292 000 pixels par image pour en afficher environ 76 000. Trois quarts du travail de décodage partaient à la poubelle, et le quart restant était étiré au point d'être flou. Sur un téléphone, dont le décodeur est la ressource rare, c'est le pire des deux mondes.
La skill prescrit d'ailleurs une chaîne portrait native et qualifie le recadrage 16:9 de solution de repli. Le repli avait été livré, signalé comme tel, et pas corrigé. Il l'a été : une chaîne 540 par 960 à 15 images par seconde.
Le détail contre-intuitif, c'est que le coût par recherche d'image augmente : environ 1,55 million de pixels contre 1,17 million, soit 33 % de plus. Mais il n'y a plus un seul pixel gaspillé, l'agrandissement tombe de 6,3 à 2,6 fois, et il y a moitié moins d'images distinctes à décoder sur un vol. Le passage à 15 images par seconde a été rendu possible par une vérification dans le code du moteur : il calcule sa position temporelle depuis video.duration, donc la cadence des clips est libre.
Autre vérification qui a évité une modification inutile : le moteur possède sa propre boucle de recherche d'image, indépendante des événements de défilement. Brider la fréquence de mise à jour du défilement n'aurait donc rien changé au coût de décodage.
Ce que la skill ne fait pas
Le moteur construit tout son DOM en JavaScript. Le HTML livré se réduisait à un conteneur vide : zéro caractère indexable. Gênant pour un site qui accueille explicitement les robots des moteurs de réponse, lesquels n'exécutent pas de script.
Ce n'est pas un défaut de la skill, c'est son périmètre. Elle produit une expérience, pas une page. Le prérendu était à construire par dessus.
Ce qu'on garde
Quatre vérifications, réutilisables sur n'importe quelle intégration de composant tiers :
- Regarder les valeurs calculées, pas le code. Une taille de police à 16 px au lieu de 54 px se voit dans l'inspecteur en dix secondes, et se cherche pendant une heure dans une feuille de style.
- Vérifier que l'environnement d'observation ne fabrique pas le symptôme avant d'écrire le moindre correctif.
- Interroger les en-têtes réels servis en production, avec un simple
curl -sI. Un déploiement au vert ne dit rien du type MIME, du cache ou de la compression. - Calculer ce qui est réellement affiché avant d'optimiser un média. La question n'est pas le poids du fichier, c'est le rapport entre pixels décodés et pixels montrés.
Et une remarque de fond sur les skills. Celle ci a fait exactement ce qu'elle promettait : une procédure, des gabarits de prompt, un moteur. Elle a fait gagner la partie difficile, la conception du mécanisme. Tout ce qui précède, les couches CSS, le MIME, le recadrage mobile, le prérendu, relève de l'intégration dans un projet réel, et aucune skill ne peut le faire à votre place.