html-to-video · Créer des vidéos image par image avec du code
Qu'est-ce que c'est ?
Ce n'est pas une génération de vidéo par IA, c'est dessiner chaque image avec du code :
Écrire du HTML/Canvas, représenter chaque image comme une fonction pure frame(t)
→ Navigateur headless, chercher t, capture d'écran
→ ffmpeg encode en H.264 MP4
Sous-titres / mouvements de caméra / transitions, tout est dans le code, modification du code et re-rendu.
Avantages : déterministe (même entrée = même sortie), contrôle à l'image près, texte chinois parfait, zéro coût. Idéal pour : animations d'explication, graphiques dynamiques, démonstrations de produits, animations de visualisation de données, génériques. Pas adapté pour : prises de vue réelles / ambiances (utiliser VideoGen pour cela).
Ce que vous pouvez en faire (11 modèles dans assets/templates/ prêts à être modifiés) :
| Modèle | Utilisation | Sortie |
|---|---|---|
explainer.html | Vidéo explicative multi-scènes : générique / personnage + bulle / points clés / barres comparatives / fin, avec transitions, peut être alimentée par un tableau de scènes JSON ; en paysage, trois points clés s'alignent automatiquement en trois colonnes, en portrait (1080×1920), une mise en page verticale est disponible | --duration=17.6 |
title-card.html | Générique vidéo : titre principal animé lettre par lettre + sous-titre | --duration=6 |
bar-chart.html | Graphique à barres / classement : barres qui poussent une par une, valeurs qui défilent en synchronisation | --duration=6 |
line-chart.html | Tendance en ligne : ligne qui se dessine + points de données qui s'allument + étiquette du dernier point | --duration=8 |
number-counter.html | Grand nombre / KPI : compteur de 0 à la valeur cible + étiquette de variation | --duration=7 |
timeline.html | Chronologie / processus : axe central qui se dessine, jalons qui s'allument un par un | --duration=7 |
vertical-card.html | Carte verticale 9:16 : grand titre + grand nombre, pour Douyin/WeChat Reels/Reels | --duration=6 --width=1080 --height=1920 |
compare.html | Comparaison avant/après : deux colonnes côte à côte, conclusion en bas | --duration=7 |
flow-steps.html | Animation de processus : étapes qui s'allument une par une, étape courante en surbrillance | --duration=7 |
subtitle-card.html | Carte de sous-titres phrase par phrase : une phrase apparaît à la fois, barre de progression en bas | --duration=8 + --srt |
vertical-subtitle.html | Carte de sous-titres verticale : mots-clés en surbrillance, mise en page native 9:16 | --duration=8 --width=1080 --height=1920 + --srt |
Chaque modèle est autonome en un seul fichier, avec un bloc de configuration CFG bien visible en haut, modifiez le texte/les données —
pas besoin de connaître canvas pour produire une vidéo. Les 11 modèles supportent tous les thèmes (voir section « Changement de thème »).
Pour créer autre chose, regardez assets/animation-skeleton.html et partez de zéro.
Créez « une vidéo » pas « une animation » : utilisez assets/lib/scenes.js pour écrire plusieurs scènes + transitions,
le tableau de scènes peut même être externalisé avec --story=shots.json — un LLM écrit du JSON plus fiablement que des centaines de lignes de code canvas.
Voir la section « Multi-scènes et transitions » ci-dessous.
Dimensions : les modèles suivent la fenêtre, pas de modification manuelle
Les 11 modèles utilisent window.innerWidth/innerHeight pour le canevas, donc changer de dimensions ne coupe rien :
# Paysage 1080p (par défaut)
bash assets/render.sh title-card.html --duration=6 --fps=60
# Portrait 9:16
bash assets/render.sh title-card.html --duration=6 --fps=60 --width=1080 --height=1920
# 720p / 4K idem
bash assets/render.sh title-card.html --duration=6 --fps=60 --width=1280 --height=720
À l'intérieur, les modèles utilisent trois niveaux de mise à l'échelle pour adapter le design au canevas réel :
| Variable | Signification | 1920×1080 | 1080×1920 |
|---|---|---|---|
SX = W/1920 | Dimensions horizontales, marges gauche/droite | 1 | 0,5625 |
SY = H/1080 | Dimensions verticales, interlignes | 1 | 1,7778 |
SF = √(SX·SY) | Taille de police, largeur de ligne, rayons, flous | 1 | 1 |
La taille de police utilise une mise à l'échelle proportionnelle à la surface (SF) plutôt qu'à la largeur — en mode portrait, le texte ne devient pas plus petit, la mise en page s'étend simplement.
Sous 1920×1080, les trois valent 1, donc la sortie par défaut correspond pixel pour pixel à l'ancienne version (PSNR = inf).
Suivez la même règle pour vos animations, sinon la modification de --width/--height coupera silencieusement le coin inférieur droit :
const cv = document.getElementById('stage');
cv.width = window.innerWidth; // ← ne pas écrire 1920 en dur
cv.height = window.innerHeight; // ← ne pas écrire 1080 en dur
Le moteur de rendu mesure maintenant la taille du canevas une fois, et avertit explicitement s'il est plus petit que la fenêtre (plus de rendu partiel silencieux).
Installation des dépendances (une seule fois)
Le moteur de rendu nécessite un espace de travail Node (contenant Playwright + ffmpeg complet). Une commande suffit :
bash scripts/install-deps.sh # Espace de travail par défaut : ~/.workbuddy/binaries/node/workspace
bash scripts/install-deps.sh /path/to/workspace # Ou spécifiez le vôtre
Il détecte automatiquement le cache du navigateur local, choisit la version Playwright compatible, installe ffmpeg complet,
et écrit le chemin de l'espace de travail dans ~/.workbuddy/.html2video-env, rendant render.sh / smoke.sh / qa.sh prêts à l'emploi.
Installation manuelle possible (l'espace de travail doit contenir playwright + @ffmpeg-installer/ffmpeg), puis écrivez la configuration :
# ~/.workbuddy/.html2video-env
WB_NODE_WORKSPACE="<chemin absolu de votre espace de travail node>"
Vous pouvez aussi utiliser une variable d'environnement temporaire :
export WB_NODE_WORKSPACE="<chemin>". N'écrivez pas le chemin dans${VAR:-default}— si le nom d'utilisateur contient une apostrophe (ex.O'Brien), bash pourrait interpréter l'apostrophe comme un début de guillemet, provoquant une erreurbad substitution.
Quatre contraintes strictes (déjà rencontrées, à ne pas répéter)
-
La version de Playwright doit correspondre au cache du navigateur.
playwright-core@Xne reconnaît que le numéro de build chromium déclaré dans sonbrowsers.json, sinon erreur "Executable doesn't exist".→ Ne devinez pas la version, utilisez
scripts/install-deps.sh, il exécutescripts/detect-env.jspour détecter le cache et choisir automatiquement la version. Détection seule :node scripts/detect-env.js(afficheRECOMMEND_PW=...). Correspondances connues : build 1148→1.49.1, 1155→1.50.1, 1169→1.52.0, 1187→1.55.0, 1208→1.58.0.Note : dans certains environnements réseau, le téléchargement de chromium peut s'interrompre (répertoire partiel dans le cache). Dans ce cas, privilégiez la réutilisation du cache existant plutôt que la mise à niveau — c'est la raison d'être du script de détection.
-
Le ffmpeg fourni avec Playwright ne sert à rien.
ms-playwright/ffmpeg-*/ffmpeg-win64.exeest compilé avec--disable-everything, seulement le codec VP8 + conteneur webm, pas de H.264, pas de GIF, pas même de décodage PNG. Utilisez la version complète de@ffmpeg-installer/ffmpeg. -
ESM ne reconnaît pas
NODE_PATH.import 'playwright'dans.mjsdonneraERR_MODULE_NOT_FOUND. → Les scripts auxiliaires doivent être écrits en CJS (.cjs/require). -
PATH n'est pas fiable. Surtout sur Git Bash / Windows, les chemins de style Windows dans PATH peuvent être corrompus par la conversion. → Ne vous fiez pas à PATH pour transmettre ffmpeg, utilisez la variable d'environnement
FFMPEG(le moteur de rendu la supporte).
Processus de production
Étape 0 (la plus simple) : Choisir un modèle et modifier le texte
cp assets/templates/bar-chart.html my-video.html
# Ouvrez my-video.html, modifiez uniquement le titre / les données / la note dans CFG en haut
bash assets/render.sh my-video.html --duration=6 --fps=60
Les modèles intègrent une zone de sécurité pour les sous-titres : le texte ne tombe pas dans les 15% du bas de l'écran (couvert par les sous-titres incrustés). Suivez cette règle pour vos propres mises en page.
Étape 1 : Écrire l'animation HTML (si les modèles ne suffisent pas)
Utilisez assets/animation-skeleton.html comme squelette. Contrat principal (dont dépend le moteur de rendu) :
// Doit exposer ces deux éléments
window.__seek = (t) => { /* Redessiner complètement l'image au temps t */ };
window.__ready = true; // Signal de prêt
// Le moteur de rendu injectera window.__seekRender = true,
// il faut alors impérativement bloquer l'horloge interne et se fier uniquement à __seek pour pousser les images (pour garantir le déterminisme)
if (window.__seekRender) { renderAt(0); window.__ready = true; }
else { /* Aperçu navigateur : boucle auto-alimentée avec requestAnimationFrame */ }
Règle absolue : chaque image = fonction pure du temps t. Ne dépendez pas de l'état de « l'image précédente »,
n'utilisez pas Math.random() / Date.now() / performance.now() — cela produirait des sorties différentes pour la même entrée,
le contrôle de « déterminisme » dans QA échouerait, et le rendu parallèle avec plusieurs workers donnerait des résultats différents.
Le canevas est la seule sortie : <canvas width=1920 height=1080>, positionné en CSS à left:0; top:0,
body margin:0. Le moteur de rendu utilise clip:{x:0,y:0,width:W,height:H} pour capturer le coin supérieur gauche, donc le canevas doit remplir exactement la fenêtre, pas être centré.
(Le moteur de rendu supporte aussi les animations DOM/CSS/SVG, il suffit d'exposer __seek — mais attention : les animations DOM se recomposent plus lentement à chaque image,
le pur canvas est généralement plus stable et plus rapide.)
Noms de classe interdits (le CSS injecté par le moteur de rendu les masque inconditionnellement) :
.no-record .progress .progress-bar .counter .tCur .phases .phase-label .phase .replay .masthead .kicker .title .footer
De plus, n'ajoutez pas <button> à des éléments position:fixed/sticky, ils pourraient être masqués par des règles heuristiques comme des contrôles de lecture.
(Pour qu'un élément apparaisse dans la vidéo finale, ajoutez-lui data-record-keep="true" pour échapper au masquage heuristique.)
Polices chinoises : '"Microsoft YaHei", "PingFang SC", system-ui, sans-serif' ; à chasse fixe : 'ui-monospace, Consolas, monospace'.
Étape 2 : Test de fumée (ne sautez pas cette étape)
Rendez d'abord quelques images pour vérifier le contrat, l'absence d'erreurs dans la console, et la qualité visuelle, puis lancez le rendu complet :
bash assets/smoke.sh animation.html
# SMOKE_T="0,1,2" SMOKE_W=1280 SMOKE_H=720 bash assets/smoke.sh animation.html
Méthode plus rapide : rendre une courte séquence, pour voir l'effet dynamique en quelques secondes.
bash assets/render.sh animation.html --from=3 --to=5 --fps=30 # Rendre seulement 3s–5s
bash assets/render.sh animation.html --stills=1,3,5 # Exporter seulement trois images fixes
Étape 3 : Rendu complet
bash assets/render.sh animation.html --duration=12 --fps=60 --width=1920 --height=1080 --concurrency=4
Paramètres courants :
| Paramètre | Effet |
|---|---|
--duration / --from / --to | Durée / ne rendre qu'une partie (en secondes). Si la plage est spécifiée, le nom de sortie devient nom.début-fins.mp4, n'écrase pas la version complète |
--stills=0.5,2.0 | Exporter uniquement les PNG à ces instants, sans encodage |
--fps / --width / --height | Cadence images par seconde et résolution |
--preset=<nom> | Préréglage de dimensions : douyin / tiktok / vertical (1080×1920), xiaohongshu (1080×1440), instagram (1080×1350), square (1080×1080), bilibili / youtube / landscape (1920×1080) |
--presets=a,b,c | Produire toutes les proportions en une seule commande, un fichier par proportion (suffixe automatique -nomProportion). Pour publication multiplateforme |
--theme=<nom> | Changer de thème (midnight / paper / neon), remplace CFG.theme sans modifier le fichier. Un nom incorrect sera signalé et reviendra à midnight |
--out=<fichier> | Spécifier le chemin de sortie (par défaut, même répertoire et même nom). Utile pour produire plusieurs thèmes à partir d'un même HTML, pour éviter l'écrasement ; le répertoire parent est créé automatiquement s'il n'existe pas |
--json | La sortie standard contient uniquement une ligne de résultat JSON (output / duration / frames / warnings etc.), les logs lisibles sont redirigés vers stderr. Pour l'analyse par un agent |
--concurrency | Nombre de workers parallèles (chaque worker = une page Chromium, attention à la mémoire). auto choisit automatiquement en fonction du CPU et de la mémoire |
--audio=<fichier> | Mixage audio automatique après encodage (flux vidéo copié directement, sans ré-encodage). Si --duration n'est pas fourni, la durée vidéo est automatiquement celle du fichier audio (arrondi à l'image entière supérieure, le son de fin n'est pas coupé) |
| `--audio-fit=trim | pad` |
--gop=<secondes> | Intervalle entre images clés, ex. --gop=1 une image clé par seconde, pour un seek par seconde ultérieur |
--keep-frames | Conserver les PNG individuels même en cas de succès (pour un ré-encodage avec un CRF différent sans recapturer les images) |
--tmpdir=<répertoire> | Où stocker les PNG individuels (par défaut à côté du HTML). Une minute en 1080p fait environ 2.4 Go, spécifiez un autre disque si le disque système manque d'espace |
--resume | Reprendre un rendu interrompu, réutiliser les images déjà rendues (refusera si les paramètres sont différents de la précédente exécution, voir ci-dessous) |
--alpha | Sortie avec fond transparent (ProRes 4444 .mov), pour superposer à des images réelles / générées par IA, voir ci-dessous |
--srt=<fichier> | Fichier de sous-titres externe (SRT / VTT / JSON), injecte window.__cues, voir ci-dessous. Synonyme : --cues |
--cue-log=<fichier> | Exporter un CSV frame,t,cue_index,cue_start,cue_end pour vérifier l'alignement des sous-titres |
--story=<fichier> | Fichier JSON de storyboard, injecte window.__story, remplace entièrement le scénario (voir section « Multi-scènes et transitions ») |
--scene-log=<fichier> | Exporter un CSV de la timeline des scènes ; alerte si la durée totale du storyboard ne correspond pas à --duration |
--gif[=fps,largeur] | Produire un GIF après la vidéo (défaut 15fps / largeur 960), optimisation de palette en deux phases, sans re-rendu |
--poster=<secondes> | Capturer une image fixe de couverture .jpg à un instant donné, pour les réseaux sociaux |
--batch=<fichier.json> | Liste de tâches à rendre en séquence, rapport récapitulatif à la fin ; l'échec d'une tâche n'affecte pas les tâches déjà rendues, mais le code de sortie global est non nul |
--headed | Ouvrir la fenêtre du navigateur pendant le rendu (pour le débogage ; la concurrence passe automatiquement à 1 pour éviter plusieurs fenêtres) |
--allow-errors | Les erreurs de page ne sont pas considérées comme un échec (par défaut : erreur → sortie non nulle) |
--keep-chrome | Ne pas masquer les contrôles de lecture (débogage) |
--readytimeout=8 | Temps d'attente (en secondes) pour que __seek soit prêt (à augmenter pour les grandes polices/importants volumes de données) |
--no-serve | Charger la page via file:// (par défaut, utilise un serveur http local, voir ci-dessous) |
Projets ESM / fetch directement rendus. Le moteur de rendu démarre par défaut un serveur http local lié à 127.0.0.1 pour charger la page —
sous file://, <script type="module"> et fetch() sont bloqués par CORS ; les animations organisées avec Vite/esbuild,
les projets lisant des fichiers JSON locaux ne fonctionneraient pas. Avec le serveur http, ces problèmes disparaissent. L'adresse est affichée au début du rendu, collez-la dans le navigateur pour la console de débogage.
Si le serveur ne peut pas démarrer, il revient automatiquement à file:// avec un avertissement (pas d'erreur).
- La sortie est un
.mp4portant le même nom que le HTML. - Nombre d'images =
fps × durée. 12s@60fps = 720 images, environ 6~7 minutes (concurrency=4). - Encodage : environ 1 minute (crf 18 / preset medium).
En fin de rendu, si des erreurs de page sont détectées, le code de sortie est non nul (mais la vidéo est tout de même produite pour inspection).
Rendu interrompu, ne recommencez pas de zéro
Les longues vidéos (60fps × 30s = 1800 images) peuvent planter à 80%. Inutile de tout recommencer. Ajoutez --resume pour continuer :
bash assets/render.sh animation.html --fps=60 --duration=30 --resume
# ▸ resume : 1432 images réutilisées, reste 368 images à rendre
Trois règles, pour garantir que la vidéo reprise soit identique à une vidéo rendue en une seule fois :
- Les images sont d'abord écrites en
.partavant d'être renommées. Une image tuée ne laisse pas de PNG à moitié fait, donc les images réutilisées sont vraiment complètes. - Si les paramètres changent, refus catégorique. Si vous aviez 30fps et que vous passez à 60fps, les deux lots d'images seraient mélangés pendant l'encodage sans erreur, créant une vidéo incohérente. Donc si
fps / dimensions / plage / mode de rendudiffèrent, le script se termine et affiche « précédent vs actuel », vous laissant décider de supprimer ou non. - Les numéros d'images doivent être consécutifs. Si une image manque, ffmpeg termine silencieusement prématurément, la vidéo est plus courte que prévu. Avant l'encodage, le script vérifie l'intervalle
[début, fin)un par un, signale les trous et suggère d'ajouter--resumepour les combler.
Le répertoire temporaire est fixé à côté du HTML, nommé .seek-tmp-resume/ (sans --resume, c'est un répertoire unique, non interférent avec les rendus parallèles).
Supprimé automatiquement après le rendu ; pour le conserver, utilisez --keep-frames.
C'est intentionnel : les erreurs de page signifient souvent « écran noir total / contenu manquant sur la moitié de l'écran », l'ancienne version signalait un succès jusqu'à ce que vous vérifiiez les images — maintenant, les erreurs sont affichées avant l'encodage et signalées comme un échec à la fin.
Sortie transparente : superposer une couche de texte à des images réelles
Arrière-plan généré par IA + couche de texte générée par code est la combinaison la plus économique aujourd'hui (les modèles d'IA floutent le texte, le code le rend avec une précision au pixel près).
--alpha est conçu pour cela :
bash assets/render.sh animation.html --duration=6 --fps=30 --alpha
# → animation.mov (ProRes 4444 / yuva444p10le, fond transparent)
Côté modèle, une condition à respecter : quand window.__transparent est vrai, sauter le dessin de l'arrière-plan.
Actuellement, title-card, compare, flow-steps, subtitle-card, vertical-subtitle,
explainer (6 modèles) sont déjà écrits ainsi, ajoutez --alpha pour un effet transparent ;
les 5 autres sont des graphiques plein écran (barres / lignes / grands nombres / timeline / carte verticale) qui ont besoin d'un fond, ils ne produiront qu'une vidéo opaque.
Pour vos animations, suivez ce modèle :
const TRANSPARENT = !!window.__transparent;
function drawFrame(t) {
g.clearRect(0, 0, W, H);
if (!TRANSPARENT) {
// Fond / dégradé plein écran / taches lumineuses — tout cela doit être sauté
g.fillStyle = '#080A10';
g.fillRect(0, 0, W, H);
}
// Texte, graphismes, dessin normal
}
Les taches lumineuses semi-transparentes plein écran doivent aussi être sautées. Même à 5% d'opacité, superposées à une image réelle, elles éclaircissent toute la zone. Test pratique : en sautant uniquement le fond mais en gardant les taches, les coins ont alpha=12 (pas 0) ; en sautant tout, les coins ont alpha=0, seulement 3,7% des pixels sont opaques (uniquement le texte).
Coût en taille (960×540 / 2s / 20 images) :
| Sortie | Taille | Utilisation |
|---|---|---|
H.264 .mp4 | 118 Ko | Vidéo finale directement |
ProRes 4444 .mov | 760 Ko (6,5×) | Superposition dans un logiciel de montage |
Seulement ProRes, pas de VP9. Testé : --pix_fmt yuva420p + -auto-alt-ref 0 a bien augmenté la taille de fichier de 7,8 Ko
à 16,8 Ko (données alpha ajoutées), mais Chromium affichait toutes les images avec alpha=0, même le texte devenait transparent ;
lossless, débit fixe, -lag-in-frames 0 donnaient le même résultat. Puisqu'il est impossible de prouver l'exactitude, VP9 n'est pas proposé comme option.
Pour des fichiers transparents de petite taille, utilisez ProRes importé dans un logiciel de montage, puis exportez en webm — c'est le travail de ce logiciel.
Alignement audio : d'abord le son, puis la durée vidéo
Le flux de travail réel est « écrire le script → enregistrer l'audio → synchroniser les images », pas l'inverse.
Donc, si --audio est fourni sans --duration, la vidéo est calée sur la durée de l'audio :
bash assets/render.sh title-card.html --audio=voice.m4a --story=shots.json
# ▸ audio : audio 12.48s, --duration non spécifié → durée vidéo = durée audio (12.5s, arrondi à l'image entière supérieure)
- Arrondi à l'image entière supérieure, pas d'arrondi : perdre une demi-image couperait la fin du dernier mot.
- Si
--durationest explicitement fourni et que la différence avec la durée audio dépasse 0,35s, un avertissement clair indique la différence et ce qui est affecté. L'ancienne version traitait cela silencieusement : vidéo plus courte que prévu, la moitié de la fin de la vidéo avait disparu, le journal indiquait seulement✓ Done. --audio-fit=pad: si l'audio est plus court que la vidéo, ajoute du silence pour conserver la vidéo entière (par défauttrimtermine avec la piste audio).
Note : avec trim, la vidéo n'aura pas exactement la durée en secondes de l'audio. Le flux vidéo est copié avec -c:v copy,
ffmpeg ne peut terminer qu'aux limites des paquets d'images, il restera généralement 0.3–0.5s supplémentaires. Pour un alignement milliseconde, il faudrait ré-encoder, ce qui donnerait une qualité moindre pour une précision inutile — quelques images supplémentaires à la fin passent inaperçues.
Multi-publication : une seule commande pour toutes les proportions de plateformes
Pour publier une même vidéo sur Douyin, Xiaohongshu, Bilibili, dans un logiciel de montage, il faut créer une séquence par proportion et exporter une par une. Ici, la mise en page étant paramétrique, un rendu supplémentaire n'est qu'une boucle de plus :
bash assets/render.sh bar-chart.html --duration=8 \
--presets=douyin,xiaohongshu,square --out=dist/chart.mp4
# → dist/chart-douyin.mp4 1080×1920
# dist/chart-xiaohongshu.mp4 1080×1440
# dist/chart-square.mp4 1080×1080
| Préréglage | Taille | Pour qui |
|---|---|---|
douyin / tiktok / vertical | 1080×1920 | Douyin, Kuaishou, WeChat Video, Reels, Shorts |
xiaohongshu / xhs | 1080×1440 | Xiaohongshu 3:4 |
instagram / ig | 1080×1350 | Flux IG 4:5 |
square | 1080×1080 | Carré 1:1 |
bilibili / youtube / landscape | 1920×1080 | Paysage 16:9 |
- Une seule proportion : utilisez
--preset=xiaohongshu; plusieurs :--presets=a,b(au moins deux, sinon utilisez--preset). - Si le nom du préréglage est erroné, la commande échoue immédiatement en listant les noms disponibles — pas de rendu silencieux en 1920×1080 qui produit une vidéo aux proportions erronées.
- Que la mise en page tienne est une autre question : les modèles s'adaptent à la fenêtre, mais la densité d'information diffère entre 9:16 et 16:9,
sur 3:4, la police peut sembler trop grande / le contenu trop serré, il faut vérifier visuellement.
--presetsgarantit les dimensions, pas la composition. - L'espace blanc en bas du portrait est intentionnel, pas une erreur de mise en page : les barres de progression et les boutons des plateformes s'y trouvent, les modèles réservent 15% de zone de sécurité pour les sous-titres incrustés.
- Exclusif avec
--stills(l'un produit plusieurs vidéos, l'autre ne fait que des captures) ; non imbriqué avec--batch.
Pour les agents : --json
Ce sont principalement des agents AI qui utilisent cet outil. Un humain lit ✓ Done : xxx.mp4 (1,2 Mo · 30fps · 12s) sans problème,
un agent doit utiliser des expressions régulières — si le chemin contient des espaces, des caractères chinois ou des parenthèses, tout casse. --json structure le résultat :
bash assets/render.sh title-card.html --duration=6 --json
{"ok":true,"exitCode":0,"input":"…/title-card.html","output":"…/title-card.mp4",
"width":1920,"height":1080,"fps":60,"duration":6,"frames":360,"bytes":2411724,
"gif":null,"poster":null,"warnings":[],"mode":"video","theme":null,"preset":null}
- stdout contient seulement cette ligne ; les logs de progression et les messages lisibles sont redirigés vers stderr —
JSON.parse(stdout)fonctionne directement. warningsest un tableau : conflits de paramètres, non-correspondance audio/vidéo, thème inconnu, tout y figure. Un tableau vide = propre.durationest la durée réelle de la vidéo (lue par ffmpeg), pas la valeur prévue.- Avec
--presets,outputdevient un tableau, avecpresets: [{name,width,height,output,ok,bytes}].
Sous-titres avec timeline externe : quand la voix change, les sous-titres suivent
Écrire le texte des sous-titres dans CFG.lines du modèle est le plus simple, mais dès que vous avez une piste audio, les timings doivent suivre l'audio —
il est impossible de réajuster les secondes manuellement à chaque modification du texte. D'où le support direct d'un fichier de sous-titres :
bash assets/render.sh subtitle-card.html --duration=8 --srt=voice.srt --cue-log=cues.csv
- Support des formats SRT (
HH:MM:SS,mmm), WebVTT (MM:SS.mmmaussi accepté), JSON ([{start,end,text}]). - Analysé une fois, injecté via
window.__cues. Le modèle utilisetpour chercher quelle phrase afficher. Quand--srtest utilisé,__cuesest lu en priorité, sinon on revient àCFG.lines, permettant au même modèle de fonctionner avec les deux approches. --cue-logexporte un CSVframe,t,cue_index,cue_start,cue_end. Transforme « les sous-titres sont-ils bien synchronisés ? » en une valeur calculable : soustrayez le temps de la première image de chaque phrase au début du SRT, c'est l'erreur. Mesuré : 4 phrases / 80 images, erreur par phrase 0 ms, le nombre d'images couvertes correspond exactement à la théorie.
Côté modèle, deux lignes suffisent :
const cues = (window.__cues && window.__cues.length) ? window.__cues : CFG.lines;
const cur = cues.find(c => t >= c.start && t < c.end) || cues[cues.length - 1];
Les sous-titres ne doivent pas empiéter sur les 15% du bas. C'est la zone des barres de progression / boutons de like des plateformes, écrire des sous-titres là équivaut à ne rien écrire.
qa.js signalera « zone de sous-titres », les quatre nouveaux modèles en format vertical intègrent cette zone de sécurité.
Changement de thème : modifier un champ, pas de code canvas
assets/templates/themes.js contient trois palettes, les 11 modèles les utilisent toutes. Deux façons de changer :
# ① En ligne de commande (recommandé) : ne modifie aucun fichier, un rendu par thème
bash assets/render.sh bar-chart.html --duration=6 --theme=paper --out=bar-paper.mp4
# ② En modifiant le fichier : dans CFG en haut du modèle
const CFG = {
theme: 'paper', // midnight (défaut, sombre) | paper (clair) | neon (noir violet néon)
...
};
La ligne de commande est prioritaire sur CFG.theme (le moteur de rendu injecte window.__h2vTheme, le modèle lit
h2vTheme(window.__h2vTheme || CFG.theme)). Ainsi, le même HTML peut produire les trois thèmes en une seule commande :
[{ "h":
…
[le contenu a été tronqué pour la traduction]