vid-motion-graphics
Génère des motion graphics multi-scènes au format MP4. Affiche des animations HTML/CSS dans Chromium sans tête via Playwright (recherche d'image par API Web Animations), assemble les images PNG avec FFmpeg. Pas de React, pas d'API d'IA, pas de Python — zéro nouvelle dépendance en dehors de la famille graphic-gif.
Polices CDN uniquement. Aucune bibliothèque externe dans le HTML.
Règles critiques (à lire avant chaque génération)
- Utilisez
window.renderFrame(t)— pas de@keyframesCSS pour les transitions de scène. La recherche decurrentTimedans les animations CSS est silencieusement ignorée pour les recherches en arrière dans Chromium. L'approche renderFrame : une fonction JS pure calculeopacity/transformdirectement à partir des millisecondes. Playwright l'appelle une fois par image. Déterministe, sans concurrence. - Pas de
animation-delaysur AUCUN élément. Inutile avec renderFrame. Si vous vous surprenez à écrireanimation-delay, arrêtez — vous utilisez la mauvaise architecture. window.__videoReady = trueuniquement à l'intérieur dedocument.fonts.ready.then(...). Ne jamais définir de manière synchrone — les polices doivent être chargées avant que Playwright ne capture l'image 1, sinon le texte s'affiche avec des polices de secours.- Exposez
window.__stopPreview(). La boucle de prévisualisation rAF du navigateur entre en concurrence avec les appels evaluate/screenshot de Playwright.capture-frames.mjsappelle__stopPreview()avant la boucle d'images. Incluez-la toujours. - Utilisez
t < startMs(pas<=) dans les vérifications de limites de scène.t <= 0à l'image 0 rend la scène 1 noire. La protection correcte estif (t < startMs || t >= endMs) return hidden. - Body = dimensions exactes en pixels. La largeur et la hauteur sont des entiers (
1080px,1920px). Pas de%,vw/vhou d'unités responsives. - Aucune scène visible simultanément (sauf chevauchement d'entrée de 10 %). Toutes les scènes
opacity: 0en dehors de leur fenêtre renderFrame. - Les transitions utilisent
opacityuniquement. Pas de basculedisplay, pas devisibility— l'opacité composée par le GPU est parfaite image par image. - Ne déversez jamais le HTML dans le chat. Sauvegardez dans un fichier, montrez un résumé uniquement.
- Le titre énonce le message, pas le sujet. "3 raisons pour lesquelles le T4 a explosé les objectifs" pas "Vidéo de performance T4 2024".
- Lisez
references/scene-library.mdavant de générer TOUT HTML. Utilisez la structure HTML exacte et les noms de classes CSS de ce fichier.
Étape 1 : Prise en charge
Obligatoire : content_brief
Paramètres optionnels et valeurs par défaut :
| Paramètre | Défaut | Description |
|---|---|---|
| content_brief | — | Texte décrivant ce que la vidéo communique (obligatoire) |
| scenes | auto | Nombre de scènes (1–6). Auto = dérivé du brief. |
| duration_per_scene | 3s | Durée par scène en secondes (1–8s) |
| style | kinetic-dark | kinetic-dark / editorial-light / data-pulse / bold-type / minimal-clean |
| aspect_ratio | 1:1 | 1:1 (1080×1080) / 16:9 (1920×1080) / 9:16 (1080×1920) |
| fps | 30 | Images par seconde (24, 30 ou 60) |
| music | none | Chemin vers le fichier audio pour la piste de fond (mp3/m4a/wav) |
| source | — | Attribution de source affichée dans le pied de page de l'image finale |
Si content_brief est manquant, demandez exactement :
"Pour créer la vidéo, j'ai besoin d'un brief de contenu — que doit communiquer la vidéo ?
Exemple : 'Montrer 3 raisons pour lesquelles le chiffre d'affaires du T4 a augmenté de 85 % : nouvelles transactions entreprises, réduction de l'attrition, augmentation des prix. Utilisez des chiffres en gras. Style : data-pulse.'
Optionnel : style (défaut : kinetic-dark), format d'image (défaut : 1:1), secondes par scène (défaut : 3s)"
Si content_brief est présent → passez immédiatement à l'étape 2.
Étape 2 : Architecture interne (jamais montrée à l'utilisateur)
1. Analysez le brief en scènes (max 6) :
- Scène 1 = accroche ou titre (toujours)
- Scènes 2–N-1 = points d'appui, métriques ou rythmes narratifs
- Scène N = CTA ou résumé de clôture (toujours, si plus d'une scène)
- Une idée clé par scène — si le brief contient 7+ idées, consolidez les plus faibles
2. Lisez references/scene-library.md — choisissez le type de scène pour chaque scène :
- Accroche/ouverture →
title-card - Métrique unique →
stat-reveal - Liste de 2–4 points →
bullet-list - Avant vs après / deux valeurs →
split-screen - Témoignage / citation →
quote-card - Final / CTA →
cta-card
3. Lisez references/style-presets.md — chargez les jetons CSS + la personnalité d'animation pour le style choisi.
4. Calculez le timing :
totalDuration = sceneCount × duration_per_scene (secondes)
totalFrames = totalDuration × fps
Chaque scène occupe (100 / sceneCount)% de la plage totale des @keyframes.
| Scène | Début % | Fin % |
|---|---|---|
| 1 | 0% | (100/N)% |
| 2 | (100/N)% | (200/N)% |
| … | … | … |
| N | ((N-1)×100/N)% | 100% |
Dans la plage de chaque scène :
- Entrée : 10 % premiers de la plage de la scène
- Maintien : 10 % à 85 % de la plage de la scène
- Sortie : 85 % à 100 % de la plage de la scène
5. Déterminez les dimensions en pixels :
1:1→ L=1080, H=108016:9→ L=1920, H=10809:16→ L=1080, H=1920
Étape 3 : Génération HTML
Lisez references/scene-library.md ET references/style-presets.md avant d'écrire du code.
Structure HTML requise :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
[lien CDN de police depuis le préréglage de style]
<style>
:root {
[tous les jetons CSS du préréglage de style]
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: [L]px; height: [H]px;
overflow: hidden;
background: var(--bg);
font-family: var(--font-body);
position: relative;
}
.scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px;
opacity: 0;
will-change: opacity, transform;
}
.scene-inner {
width: 100%;
max-width: 960px;
}
/* CSS du type de scène depuis scene-library.md */
[collez le CSS du type de scène ici]
</style>
</head>
<body>
<div class="scene scene-1">
<div class="scene-inner">
[HTML de la scène 1 depuis le modèle scene-library.md]
</div>
</div>
[répétez pour chaque scène]
<script>
window.__videoReady = false;
window.TOTAL_DURATION_MS = [totalDuration * 1000];
// ── Aides d'animation ─────────────────────────────────────────────────────────
function lerp(a, b, p) { return a + (b - a) * p; }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function easeOutCubic(t) { return 1 - Math.pow(1 - clamp(t, 0, 1), 3); }
function sceneState(t, startMs, endMs) {
if (t < startMs || t >= endMs) return { opacity: 0, ty: 0 };
const prog = (t - startMs) / (endMs - startMs);
if (prog < 0.10) {
const p = easeOutCubic(prog / 0.10);
return { opacity: p, ty: lerp(24, 0, p) };
}
if (prog < 0.85) return { opacity: 1, ty: 0 };
const p = (prog - 0.85) / 0.15;
return { opacity: 1 - p, ty: lerp(0, -12, p) };
}
function applySceneState(el, state) {
el.style.opacity = state.opacity;
el.style.transform = state.ty !== 0 ? `translateY(${state.ty.toFixed(2)}px)` : '';
}
// ── Fonction de rendu principale — appelée par Playwright une fois par image ────────────────
window.renderFrame = function(t) {
// Scène 1 : 0ms – [D]ms
applySceneState(document.querySelector('.scene-1'), sceneState(t, 0, [D]));
// Scène 2 : [D]ms – [2D]ms
applySceneState(document.querySelector('.scene-2'), sceneState(t, [D], [2D]));
// ... répétez par scène ...
};
// ── Boucle de prévisualisation — arrêtée par Playwright avant la capture d'image ─────────────────
let __previewActive = false;
let __previewRafId = null;
window.__stopPreview = function() {
__previewActive = false;
if (__previewRafId !== null) { cancelAnimationFrame(__previewRafId); __previewRafId = null; }
};
document.fonts.ready.then(() => {
window.renderFrame(0);
window.__videoReady = true;
__previewActive = true;
const startTime = performance.now();
function previewTick() {
if (!__previewActive) return;
const elapsed = performance.now() - startTime;
if (elapsed < window.TOTAL_DURATION_MS) {
window.renderFrame(elapsed);
__previewRafId = requestAnimationFrame(previewTick);
} else {
window.renderFrame(window.TOTAL_DURATION_MS - 1);
__previewActive = false;
}
}
__previewRafId = requestAnimationFrame(previewTick);
});
</script>
</body>
</html>
Règles de qualité de conception :
- Taille de police du titre : jamais inférieure à 60 px — le texte doit être lisible à la taille d'une miniature mobile
- Chiffres et statistiques : toujours l'élément le plus grand à l'écran (120–200 px)
- Espacement serré des lettres pour les polices d'affichage :
-0.02emà-0.04em - Une couleur d'accent par scène — ne dispersez pas l'accent sur plusieurs éléments
- Préréglages sombres : séparateurs
rgba(255,255,255,0.10), jamais pleins - Préréglages clairs : séparateurs
rgba(0,0,0,0.10), jamais pleins transform-origin: center centersur chaque élément qui utilisetransform- Rembourrage à l'intérieur de
.scene: minimum 80 px — ne laissez jamais le texte toucher les bords de la fenêtre d'affichage
Étape 4 : Auto-QA (corrigez chaque échec avant l'étape 5)
Exactitude de renderFrame :
-
window.renderFrame(t)définie — fonction pure, aucun effet secondaire en dehors des écritures de style - La limite de scène utilise
t < startMs(past <= startMs) — évite l'image 0 noire - Zéro instance de
animation-delayou@keyframespour les transitions de scène - Aucune scène n'a des fenêtres
opacity: 1qui se chevauchent (sauf chevauchement d'entrée de 10 %) -
window.__stopPreview()exposé et la boucle de prévisualisation rAF vérifie__previewActive
Signal de préparation :
-
window.__videoReady = falsedéclaré avantdocument.fonts.ready -
window.__videoReady = truedéfini UNIQUEMENT à l'intérieur dedocument.fonts.ready.then(...) -
window.renderFrame(0)appelé à l'intérieur dedocument.fonts.ready.then(...)avant de définir__videoReady = true
Disposition :
-
html, bodyutilisent des dimensions exactes en pixels ([L]px,[H]px) - Pas d'unités
%,vw,vh,remsur la largeur/hauteur debody -
overflow: hiddensurhtml, body - Toutes les scènes
position: absolute; inset: 0
Conception :
- Toutes les couleurs proviennent des jetons du préréglage de style — pas de valeurs hexadécimales libres
- Toutes les polices proviennent du préréglage de style — pas de chaînes font-family libres
- Taille de police du titre ≥ 60 px
- Source dans la scène finale si le paramètre
sourceest fourni
Étape 5 : Exportation
Déterminez le slug à partir du contenu du brief (kebab-case, ≤30 caractères) :
mkdir -p graph/[slug]
Sauvegardez le HTML : graph/[slug]/video.html
Prévisualisation navigateur :
open graph/[slug]/video.html
Lancez l'exportation (remplacez [skill-root] par le chemin du répertoire de cette compétence) :
bash [skill-root]/scripts/export-video.sh \
graph/[slug]/video.html \
graph/[slug]/video.mp4 \
--duration [totalDuration] \
--fps [fps] \
--width [L] \
--height [H] \
[--music path/to/audio.mp3]
Le script installe Playwright lors de la première exécution (~200 Mo Chromium), capture toutes les images, puis assemble le MP4 avec FFmpeg.
Étape 6 : Résumé de sortie
## Vidéo : [titre du brief]
Date : [AAAA-MM-JJ] | Scènes : [N] | Style : [style] | Format : [ratio]
Durée : [N]s | FPS : [fps] | Images : [N]
Fichiers
Source : graph/[slug]/video.html
Sortie : graph/[slug]/video.mp4
Taille : [X] Mo
Liste de contrôle
- [ ] Toutes les scènes apparaissent en séquence sans image vide
- [ ] Texte lisible à la taille d'une miniature mobile
- [ ] Transitions de scène fluides (pas de coupures brutales)
- [ ] La scène finale inclut un CTA ou un message de clôture
- [ ] Attribution de source présente dans l'image finale (si fournie)
Conseils sur les prompts (afficher lorsque l'utilisateur demande des conseils)
"Fournissez un brief de contenu — des puces sur ce que chaque scène doit dire."
"Nommez directement l'insight. '3 raisons pour lesquelles le T4 a augmenté de 85 %' donne une colonne vertébrale à la vidéo."
"Mentionnez le style si vous avez une préférence : kinetic-dark (défaut), editorial-light, data-pulse, bold-type, minimal-clean."
"Spécifiez le format d'image pour la plateforme : 1:1 pour le fil LinkedIn/Instagram, 9:16 pour les Stories/Reels, 16:9 pour YouTube/présentations."
✅ Bon : "Créez une vidéo de 9 secondes. Le chiffre d'affaires du T4 a atteint 4,2 M$ (croissance de 85 %). Facteurs : transactions entreprises, attrition 1,2 %, augmentation des prix. CTA : acme.com/q4. Style : data-pulse. Format : 1:1."
❌ Mauvais : "faites une vidéo sur notre entreprise"