film-engine : des films dessinés à partir du code, image par image
Où se trouve le kit : exécutez chaque commande de cette compétence depuis le dossier
reels-maker(le clone de github.com/thativanng/reels-maker qui contienttemplate/ettools/). Si le projet n'en a pas, configurez-le d'abord en suivant le SETUP.md de ce dépôt.
L'idée en un paragraphe
Un film ici est une page HTML avec une règle : window.seek(t) doit dessiner exactement l'image pour le temps t, à chaque fois, à partir de rien d'autre que t. Rien dans l'image ne peut lire une horloge réelle. Un moteur de rendu parcourt ensuite t à 30 ips, capture chaque image en 2x et la transmet à ffmpeg. Comme la page est déterministe, un re-rendu correspond (en moyenne à une fraction de niveau près ; le bruit GPU peut déplacer des pixels uniques jusqu'à environ 60 niveaux), une image fixe à 17,3 s est l'image que la vidéo montrera, et chaque vérification (film-audit) peut examiner n'importe quel moment sans lire le film.
Le parcours (ne sautez aucune étape)
- D'abord le brief et le look. reel-intake se termine par un brief et un storyboard signés (format, durée, son, ce qui n'apparaît jamais) ; reel-style se termine par un look choisi. N'ouvrez pas les étapes de construction de cette compétence sans les deux.
- Storyboard. reel-intake l'écrit (temps · mots à l'écran · son et légende · pourquoi ; references/brief.md), et la personne le signe avant que vous ne construisiez. Un style avec une bannière ajoute une colonne de bannière.
- Bande d'essai. Construisez environ les 10 premières secondes dans le look choisi (un film de style walk peut nécessiter 15 à 30 s pour montrer un chapitre), prenez des images fixes, obtenez un oui. Une construction complète sur un look non signé est du travail perdu.
- Construisez le film complet (le pack de style indique comment construire les scènes).
- Portes (film-audit) : la vérification de collision à 40 ips, les sondes de pixels, puis un auditeur indépendant. Rien ne va à la personne avant cela.
- Rendu :
node tools/render.mjs --page reels/<nom>/page.html --out reels/<nom>/out/film-silent.mp4(images 2x, réduites avec lanczos, sans marquage couleur). - Audio :
python3 template/audio.py out/film.wav(une liste de repères sur la même horloge que le film). - Finalisation :
zsh tools/mux.sh out/film-silent.mp4 out/film.wav out/film.mp4. Cela donne −14 LUFS, un limiteur, un fichier H.264 sans balise et un aperçu téléphone de la même forme que le film (petit côté 720 px). - Couverture :
node tools/cover.mjs --page reels/<nom>/page.html --out reels/<nom>/out/cover.jpg. Une page avec un mode couverture (le?coverdu starter, à partir de la configcover) dessine l'image fixe conçue ; toute autre page donne l'image àT=. Elle est enregistrée dans la forme propre du film et maintenue sous 2 Mo, plus le recadrage 3:4 centré pour un film 9:16 (la grille Instagram, rapportée). LinkedIn prend une couverture uniquement au moment de la publication : après, seul le texte d'un post peut être modifié (Aide LinkedIn, « Modifier un post partagé », https://www.linkedin.com/help/linkedin/answer/a522811). Sa spécification de miniature, JPG ou PNG dans la forme de la vidéo et au maximum 2 Mo, provient de l'aide de LinkedIn telle que citée par un résumé de recherche ; la page elle-même ne se chargerait pas, donc traitez-la comme non vérifiée. - Envoyez d'abord l'aperçu téléphone, puis le fichier complet et la couverture.
Le contrat de la page
window.seek(T): dessine l'image T. C'est le seul endroit qui anime quoi que ce soit.window.ready: une promesse qui se résout après les polices, les textures et la première image.window.DUR: la durée du film en secondes.window.FILM = { w, h, format, safe }: la taille de l'image et la zone de dégagement en pixels. Chaque outil dimensionne sa fenêtre à partir de cela, donc la page possède son format.- Optionnel :
window.cover()dessine la couverture au lieu d'une image, etFILM.coverAtnomme le moment où elle se trouve (tools/cover.mjs utilise les deux ; sans eux, il prend l'image àT=). window.collide(T)etwindow._dbg: hooks pour film-audit (voir cette compétence).- Les outils servent le dossier du kit via http sur leur propre compte (les modules ES ne se chargent pas depuis
file://). Pour regarder une page en direct dans un navigateur,npm run serveet ouvrez http://127.0.0.1:8765/template/starter.html.
Formats, le starter et autres langues
template/formats.jsoncontient les quatre tailles (9:16 1080×1920 · 4:5 1080×1350 · 1:1 1080×1080 · 16:9 1920×1080), la carte plateforme → format, et la zone de dégagement de chaque plateforme avec sa source. La ligne de format du brief la choisit.- Chaque reel vit dans son propre dossier,
reels/<nom>/(gitignoré) : son brief, storyboard,page.html(une copie du starter ou de film.html), sonreel.jsonoucareer.json, et sonout/. Les pages chargent les polices et formats.json depuis la racine du kit, donc une copie fonctionne à partir de là. Pointez les outils vers elle :PAGE=reels/<nom>/page.html. template/starter.html+template/reel.jsonest le squelette pour tous les looks sauf le voxel walk. Ses mots et minutages vivent dans la configuration, il se dimensionne à partir de?fmt=ou de la configuration, et il maintient chaque mot à l'intérieur du cœur de dégagement de la plateforme.?guidesmontre les zones couvertes en rouge, pour vérification uniquement ; ne rendez jamais avec cela activé. C'est un squelette, pas un look : reel-style réécrit ses jetons et sa disposition.template/film.html(le modèle voxel) est uniquement 4:5, et il refuse les zones de dégagement (safevaut 0) : sa bannière et ses sous-titres sont près des bords par conception, donc la sondesafepasse toujours là et l'œil de l'auditeur est la vérification. Ses caméras sont paramétrées pour 4:5 ; un autre ratio signifie les re-paramétrer et réexécuter chaque porte.- Chinois, japonais, coréen : le starter charge Noto Sans TC depuis Google Fonts au moment du rendu (seulement les caractères que la configuration utilise), et exécute le texte CJK à environ 0,72× de la taille latine (leurs caractères font un em complet). Écrivez
\nlà où une ligne doit se couper : le chinois se coupe entre deux caractères quelconques, donc une longue ligne a besoin d'une coupure que vous choisissez.template/reel.zh.jsonest un exemple fonctionnel. Tout autre script nécessite sa propre police dans la pile.
Horloges et minutage
- Une seule horloge de film pilote les scènes, la caméra, la bannière, les sous-titres et le son. Lorsque vous insérez un beat plus tard sans tout re-timer, utilisez un remappage : l'horloge de la scène s'arrête au point d'insertion tandis que les superpositions gardent l'horloge réelle :
Le script audio a besoin de la même table : écrivez unconst INS = [[24.0, 5.0, 'detail']]; // au temps de scène 24.0, joue un beat inséré de 5 s function remap(T) { let sh = 0; for (const [a, len, id] of INS) { if (T < a + sh) return { t: T - sh, ins: null }; if (T < a + sh + len) return { t: a, ins: id, u: T - sh - a, len }; sh += len; } return { t: T - sh, ins: null }; }RT(t)qui ajoute les longueurs insérées à un temps de scène, et placez chaque repère àRT(t), ou chaque repère après l'insertion dérive. (Le modèle n'a pas d'insertions, donc son audio.py n'en a pas besoin.) - Easing :
eiopour les mouvements de caméra,eopour les choses qui arrivent,trap(vitesse trapézoïdale) pour la marche et la conduite, afin qu'un mouvement ne « clique » pas aux extrémités. - Les clés de caméra sont absolues
A(pos, target)ou suiventF(offset, targetOffset)depuis le point de sol du héros. Mélangez entre les clés aveceio. Tenez sur un beat qui compte ; un plan qui bouge tout le temps donne une impression de nervosité.
Les superpositions (dans le DOM, au-dessus du canvas)
- Bannière : année, puis entreprise, puis la pastille de rôle, plus une timeline dont le point respire sur l'année courante. La bannière change quand la nouvelle scène est À L'ÉCRAN (son décor est levé, la tenue a changé, le héros est arrivé), jamais quand le code commence à la construire. Vérifiez cela sur une feuille d'images à ±1 s autour de chaque changement.
- Sous-titres : une ligne d'histoire par beat, avec la phrase clé en gras et en accent. Utilisez
text-wrap: balance. Donnez au lecteur environ 3 mots par seconde, et fondez un sous-titre avant qu'un mouvement rapide de caméra ne découvre le sujet derrière lui. - Une bulle de parole qui suit un personnage : projetez le point de la tête et verrouillez la bulle à ≥24 px à l'intérieur du cadre.
- La carte de fin : nom, une ligne d'accroche, ce dans quoi ils sont bons, une citation. La scène se floute derrière.
Audio
- Les repères sont écrits sur l'horloge du film, à côté du moment auquel ils appartiennent (pas par étape de marche, un carillon quand une chose apparaît, un bruit sourd quand elle atterrit).
- Pas de sons de whoosh sur les transitions ; ils donnent une impression de bas de gamme. Utilisez des carillons, des tics et des impacts doux.
- Le lit sonore par défaut dans
template/audio.pyest généré à partir d'ondes sinusoïdales, donc il est libre d'utilisation partout. Si quelqu'un apporte une piste générée par IA, vérifiez sa licence : certains poids de modèles n'autorisent qu'un usage non commercial. - Volume : normalisation deux passes loudnorm à −14 LUFS (mesurer, puis appliquer linéairement), puis un limiteur. Une seule passe dépasse.
Faits de rendu qui nous ont coûté des jours (plus dans references/gotchas.md)
- Rendez en 2x et réduisez. EffectComposer supprime l'anti-aliasing du canvas, donc un rendu 1x a des bords irréguliers partout.
- Pas de marquage couleur sur le MP4. Un pipe JPEG le marque comme pleine plage, et certains lecteurs décalent alors les couleurs.
setparams=...=unknowndans le filtre. - Deux visages dans le même plan scintillent (z-fighting). Donnez à celui de devant un décalage de profondeur.
- Les polices doivent être chargées (
document.fonts.load) avant que tout texte du canvas ne soit dessiné, sinon il tombe silencieusement en secours. - GPU activé dans Chromium sans tête (
--use-angle=metalsur macOS). Sans cela, un rendu est environ 10x plus lent.
Travailler sans demande à chaque étape
La personne a dit oui aux règles d'autorisation du kit une fois (SETUP.md étape 4) ; restez à l'intérieur, sinon on lui redemande encore et encore :
- Écrivez et modifiez les fichiers avec les outils Write et Edit : pages, reel.json, brief, storyboard, AUDIT.md. Ne les générez pas avec un script Python ou shell, un heredoc ou
sed -i; chacun de ces éléments est une demande d'approbation. - Mesurez avec les outils du kit (stills, probe, sheet, collide, render, cover), pas avec des extraits
python3 -cponctuels. - Pas besoin de
mkdir, boucles ou chaînes : chaque outil crée son propre dossier de sortie et prend une liste de temps en un seul appel (stills.mjs … 1.5,6,12). - Les scripts propres à un reel (son script sonore, une aide) vivent dans
reels/<nom>/et s'exécutent avecpython3 reels/<nom>/audio.pyounode reels/<nom>/x.mjs, ce que les règles couvrent.
Commandes
Exécutez chaque outil comme une seule commande simple (drapeaux, pas de VAR=… en tête, pas de boucles, pas de chaînes &&). Ainsi, les règles d'autorisation de la personne (node tools/*) les couvrent et on ne lui demande pas d'approuver chaque étape. Les variables d'environnement fonctionnent toujours pour les scripts.
npm run doctor # vérifie cette machine d'abord
npm run demo # la preuve de 12 secondes : out/demo/
node tools/stills.mjs "2,8.5,15" # images fixes 2x → out/stills (le modèle voxel)
node tools/stills.mjs --page template/starter.html --format 16:9 1.6 # le starter à un autre ratio
node tools/stills.mjs --page template/starter.html --cfg reel.zh.json 1.6 # une autre config (ici : chinois)
node tools/stills.mjs --dsf 1 --out out/chk 2,8.5 # images rapides 1x pour une feuille
python3 tools/sheet.py out/chk/sheet.jpg 6 300 out/chk/t*.jpg
node tools/render.mjs --page reels/<nom>/page.html --out reels/<nom>/out/film-silent.mp4
python3 template/audio.py out/film.wav
zsh tools/mux.sh out/film-silent.mp4 out/film.wav out/film.mp4