Communitygithub.com

znlgis/video-studio

Créez des vidéos en chinois à partir de zéro avec du code (vulgarisation, documentaire, promotion, animation). Scénario → voix off edge-tts → chronométrage réel → rendu image par image (PIL ou Canvas) → encodage ffmpeg → mixage numpy → QA automatique.

Qu'est-ce que video-studio ?

Video Studio est un outil puissant de production vidéo par code, conçu pour créer des vidéos avec narration chinoise, un rendu déterministe image par image, et une chaîne de validation automatique. Il transforme un simple fichier JSON (`film.json`) en une vidéo complète en orchestrant plusieurs étapes : le script est découpé en lignes avec texte parlé et sous-titres, la voix off est générée avec `edge-tts` et sa durée réelle est mesurée pour construire la timeline, puis chaque image est rendue via `PIL` ou un Canvas headless. Le mixage audio est effectué au niveau de l'échantillon avec `numpy` pour éviter les silences non déterministes, et `ffmpeg` encode le tout. Une règle fondamentale impose de *toujours* proposer plusieurs designs visuels avant de lancer le rendu, afin que l'utilisateur choisisse l'apparence (couleurs, typographie, musique, rythme). Le système supporte le rendu parallélisé, la reprise sur erreur, et une QA automatisée avec validation des métriques (VMAF, loudness, alignement audio/vidéo). Utilisez cette skill pour générer des vidéos de vulgarisation, des reportages transformés en vidéo, des animations, ou pour corriger des problèmes de synchronisation ou de qualité dans des vidéos générées.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/znlgis/dsh-video-studio-skill/tree/HEAD/skills/video-studio

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Video Studio

En une phrase : vidéo = scénario + durée réelle de l'audio + fonction de rendu image par image déterministe + encodage ffmpeg + QA automatique. Les images et le son sont entièrement générés par du code : zéro素材, reproductible, reprise possible, validation scriptable.

0. Règles d'airain (enfreindre une seule entraîne des problèmes garantis)

  1. L'audio d'abord, l'image ensuite. La timeline doit être dérivée de la durée réelle mesurée du TTS, jamais estimée par nombre de caractères. Une timeline basée sur le nombre de caractères dérivera visiblement à partir de la 20e phrase.
  2. Chaque image est une fonction pure : draw(t, frame) -> Image. Interdiction d'utiliser random/time(), utilisez filmkit.hash2/hash3/vnoise pour l'aléatoire. Le même numéro d'image doit toujours produire la même image.
  3. Les sous-titres et les gros textes doivent rester dans la zone de sécurité : corps de texte y ∈ [58, 974], sous-titres y ∈ [872, 1030], jamais au-delà de 1030.
  4. Les images originales sont encodées directement : images PIL → pipe rawvideo → libx264. N'utilisez pas cv2.VideoWriter comme fichier intermédiaire (2x plus lent et perte de qualité).
  5. Le mixage utilise numpy pour la superposition échantillon par échantillon, n'utilisez pas une seule commande ffmpeg avec des centaines d'entrées (cela provoque des silences non déterministes sur Windows).
  6. Avant la livraison, exécutez le script de QA + regardez 3 images fixes (avec read_image). "Ça produit une image" ne veut pas dire "il n'y a pas de problème".
  7. Le son suit l'image : les points de repère sonores sont enregistrés dans le code de la scène (K.CueBus / hook cues() de scenes.py), pas ajustés manuellement après coup. Si l'image change (durée d'affichage modifiée, rythme ajusté), les points de repère suivent automatiquement.
  8. Ajoutez une pause si ce n'est pas clair, ne ralentissez pas tout le film : utilisez hold dans film.json (global) ou lines[].hold (par phrase) pour ajouter une pause après la voix off ; alignez la musique sur les mesures avec bpm.

0.5 【Règle d'airain】 Proposer d'abord, produire ensuite (que l'utilisateur le demande ou non)

Si l'utilisateur ne le demande pas, vous devez quand même proposer. C'est la règle produit la plus importante de cette compétence, pas une suggestion. Raison : si l'apparence est choisie par le programme, l'utilisateur ne participe jamais à la décision, le résultat sera "toutes les vidéos se ressemblent". Donc le flux est fixe : générer plusieurs propositions sur place → l'utilisateur en choisit une → geler le design → produire la vidéo.

Action fixe (la première chose à faire pour toute nouvelle tâche de production de vidéo) :

python propose.py --brief "<sujet/public/durée/ton>" --script film.json --n 3

Cela produit N propositions candidates (chacune = 1 design + 4 images fixes + coût + distance de design avec les autres candidates), sauvegardées dans work/proposal.json, et imprime "Confirmez-en une / si aucune ne convient, utilisez --revise". Entièrement hors ligne, en quelques secondes.

Réaction de l'utilisateurCe que vous faites
"Celle-ci" / choisit unepython propose.py --confirm --candidate B, puis autoriser le rendu
"Trop froid/trop voyant/change de voix/rythme plus rapide"python propose.py --revise "<citation exacte>" — nouvelle rév, l'ancienne est conservée pour diff
"Aucune ne va, recommencez"Idem, précisez la demande (le texte de --revise est stocké dans proposal.feedback)
Dit explicitement "Décidez vous-même"Proposez quand même, choisissez-en une que vous recommandez et expliquez pourquoi, faites approuver par l'utilisateur

Contraintes strictes :

  1. Avant propose.py --confirm, render.py avec frames/all sera bloqué par la barrière de proposition (avertissement par défaut, --require-proposal échoue).
  2. Les candidates doivent différer sur ≥2 dimensions de design (palette/motif/transition/easing/numéro/musique/voix). Changer seulement la couleur ne compte pas comme plusieurs propositions, diversity.vs[].n < 2 sera signalé.
  3. Chaque candidate doit avoir une intention en chinois (pitch), expliquant "ce que ce design veut accomplir" — pas "Proposition A/Proposition B".
  4. Il n'y a pas de modèles intégrés : tous les champs de design.json sont ouverts (35 champs de thème + 5 signatures de langage visuel + son/musique/rythme), la palette est un point quelconque dans un espace de teinte continue, pas un choix parmi quelques designs prêts à l'emploi. Chaque proposition est générée sur place — deux prises consécutives du même brief donneront des designs différents (pour reproduire, passez --seed).
  5. Registre anti-répétition entre vidéos (activé par défaut) : --confirm enregistre le design gelé dans ~/.dsh/video-studio/design-ledger.json (max 500 entrées). La prochaine proposition évitera activement les designs récemment livrés — si une candidate a une distance de design < 2 dimensions avec une vidéo du registre, elle est automatiquement remplacée par un nouveau nonce avec avertissement. C'est le mécanisme qui fait passer "différentes vidéos, designs différents" de "aléatoire" à "avec mémoire". --show-ledger pour consulter le registre, --no-ledger pour ignorer cette fois, variable d'environnement VIDEO_STUDIO_LEDGER pour changer le chemin ou mettre off pour désactiver. Les échecs de lecture/écriture du registre sont traités comme une table vide, ne bloquent jamais la production.

Un seul cas permet de sauter la proposition : pure réparation / pure re-rendu (pas de changement de contenu, pas de changement de design, seulement correction de désynchronisation AV ou nouveau rendu). Dans ce cas, exécutez sans --require-proposal et expliquez dans la réponse "Proposition non effectuée cette fois parce que…".

0.5.1 D'où vient l'apparence : seulement deux chemins

Il n'y a pas de "style intégré" possible. Toute valeur style écrite dans film.json (auto / legacy / un id) sera clairement rejetée avec deux instructions : le scénario ne détermine pas l'apparence, l'apparence est déterminée par work/design.json. De même, il n'y a pas de table THEMES/STYLES/VISUAL_LANGUAGES dans la compétence, ni de dossier presets/ ; la musique est générée sur place par musicgen.py selon les paramètres, et scenes.draw_frame n'a pas de branche legacy.

Pour une apparence spécifique, seulement deux chemins :

# ① Exiger en langage naturel (reconnaît les mots-clés dans must_have/avoid)
python propose.py --brief "Conférence tech, bleu foncé, format cinémascope" --script film.json

# ② Écrire directement design.json (tous les champs ouverts, palette RGB arbitraire)
python render.py --script film.json --stage all     # Prérequis : work/design.json existe déjà

Contrat détaillé dans references/proposal.md et schemas/design.schema.json.


1. Démarrage en cinq étapes (zéro copie : pas de copie de script, pas de copie de modèle)

Ce chemin n'implique aucune "copie". Les scripts de la compétence restent dans le répertoire de la compétence et sont exécutés directement ; votre répertoire de projet n'a besoin que d'un film.json (plus les dossiers work/ et out/ générés).

<SKILL_DIR> désigne le resourceBase.path retourné par l'outil skill lors du chargement de cette compétence. Une fois installé comme plugin, c'est le dossier skills/video-studio/ dans le package (le node_modules/@dsh-gis/dsh-video-studio-skill du profil est un lien). Si introuvable, localisez par nom : find ~ -path '*/skills/video-studio/scripts/filmkit.py'.

<REPO> désigne la racine du package (racine du dépôt, ou node_modules/@dsh-gis/dsh-video-studio-skill/ dans le profil après installation), soit <SKILL_DIR> remonté de deux niveaux. tools/ et schemas/ sont sous <REPO>, pas sous <SKILL_DIR>.

# 1) Écrire seulement un film.json (le seul fichier que vous devez écrire à la main dans le répertoire du projet)
mkdir -p myfilm && cd myfilm
cat > film.json <<'JSON'
{
  "title": "Mon film",
  "lines": [
    {"id": "l01", "kind": "hook",  "chapter": "01 · Ouverture",
     "text": "Voici une image et un son générés entièrement par code.", "sub": "Zéro素材, tout par code", "big": "Zéro素材, tout par code"},
    {"id": "l02", "kind": "body",  "chapter": "02 · Corps", "kicker": "PIPELINE",
     "text": "Toute la pipeline ne comporte que cinq étapes : écrire le scénario, générer la voix off, construire la timeline, rendre image par image, mixer et encoder.",
     "sub": "Cinq étapes, chaque étape sauvegarde", "big": "Cinq étapes",
     "items": [{"text": "Scénario film.json"}, {"text": "Voix off"}, {"text": "Timeline"}, {"text": "Rendu image par image"}, {"text": "Mixage"}]},
    {"id": "l03", "kind": "outro", "chapter": "99 · Conclusion",
     "text": "D'abord le script, ensuite le son, enfin l'image.", "sub": "L'ordre est plus important que l'effort", "big": "L'ordre est plus important que l'effort"}
  ]
}
JSON
#    lines[].text = texte parlé (langage naturel) ; sub = sous-titre (court) ; kind = hook/body/quote/outro/emphasis/calm
#    Le style visuel/la musique/la voix/le rythme sont tous déterminés par le design, il n'y a **aucun** champ `style` à écrire dans film.json.
#    Validation du contrat (optionnel, quelques secondes) :
python <SKILL_DIR>/scripts/filmcheck.py film.json

# 2) Auto-vérification de l'environnement (optionnel ; 69 garde-fous de régression, environ 10-20 secondes, hors ligne ; les modules optionnels manquants sont marqués SKIP)
python <SKILL_DIR>/scripts/selfcheck.py

# 3) 【Règle d'airain】 Proposer d'abord, produire ensuite — le même brief donne des résultats différents à chaque fois, donc l'utilisateur doit voir
python <SKILL_DIR>/scripts/propose.py \
       --brief "Trois ans de grands modèles AI, 30 secondes, vulgarisation, bleu foncé" \
       --script film.json --work work
#    Produit 3 candidates : chacune avec un design + un planche contact d'images fixes (work/proposals/rev1/_compare.png) + distances de design,
#    résultat en 2 secondes, entièrement hors ligne. Regardez les images côte à côte, choisissez-en une.

# 4) Confirmer la candidate, geler le design (work/design.json) — le rendu n'utilisera que ce design
python <SKILL_DIR>/scripts/propose.py --confirm --candidate A --work work
#    Si insatisfait, changez-en une : --revise "Encore plus froid, moins de décorations, voix féminine" (nouvelle rév, l'ancienne reste pour diff)
#    Pour reproduire une fois particulière : ajoutez --seed 42 à la première proposition (pas de seed = nouveau nonce à chaque fois, c'est la source de "chaque vidéo est différente")

# 5) Voir les images fixes d'abord, puis la vidéo complète ; sans --scenes, le rendu utilise le rendu intégré de la compétence
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage stills \
       --stills "l01:3,l02:5,l03:2.5"
#    → work/stills/_sheet.png : regardez avec read_image, ne sautez pas

python <SKILL_DIR>/scripts/render.py --script film.json --work work \
       --out out/film.mp4 --stage all
#    TTS→Typographie→Musique→Effets sonores→Mixage→Rendu image→Encodage→QA en une seule commande ; tous les intermédiaires dans work/, reprise possible.

À propos de --scenes (la clé du zéro copie) : Sans --scenes, render.py cherche dans l'ordre ① scenes.py dans le même répertoire que le script → ② assets/scenes.py intégré à la compétence. Donc par défaut, vous n'avez rien à écrire, rien à copier, le rendu intégré produit directement la vidéo ; seulement si vous voulez modifier l'image, placez votre propre scenes.py (dans le même répertoire que le script) implémentant draw_frame et éventuellement cues(), il sera automatiquement trouvé — vous pouvez aussi utiliser explicitement --scenes my_scenes.py. Le code source du rendu intégré est le meilleur exemple : <SKILL_DIR>/assets/scenes.py.

# Signature réelle (six paramètres, il en manque un et ça plante) : `scenes.draw_frame`
def draw_frame(scene, t_local, dur, t_global, script, frame):
    # scene    : dictionnaire de ce segment dans la timeline (id / kind / scene_type / sub / big / items / stats ...)
    # t_local  : temps local dans le segment (secondes, 0..dur) — pour les animations d'entrée/de maintien
    # dur      : durée totale de ce segment (secondes)
    # t_global : temps global dans la vidéo (secondes) — pour les animations continues (lent zoom, respiration)
    # script   : dictionnaire complet du scénario/plan (plan, couleur du thème, words, etc. ; mis en cache, ne modifiez pas)
    # frame    : numéro d'image absolu (entier) — pour le bruit déterministe/phase, n'utilisez pas time()
    return img   # PIL.Image, doit être 1920x1080 RGB (utilisez K.new_frame() pour commencer, c'est le plus sûr)

Pour écrire un scenes.py personnalisé, commencez par copier un renderer de <SKILL_DIR>/assets/scenes.py (comme _draw_hero), qui a déjà contourné les pièges de la fonction pure, la mise en page en cache, le pré-rendu des fonds, etc.

Pourquoi proposer d'abord : Il n'y a pas de "style intégré", l'apparence est déterminée par design.json. Deux prises consécutives du même brief donneront des designs différents (palette/motif/transition/musique/voix peuvent toutes changer), donc le flux doit être "générer des propositions → validation humaine → geler → produire" ; sans validation, le rendu via render.py émet un avertissement (avec --require-proposal, il échoue). Contrat de design dans references/proposal.md.

Exécution par étapes (chaque étape fait seulement son travail, sauvegarde, peut être relancée individuellement) :

python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage style   # Analyser le design confirmé, imprimer le thème/langage visuel/alternance de mise en page/style de sous-titre
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage tts      # Synthétiser seulement la voix off + durée réelle (cache avec validation d'entrée)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage layout   # Construire seulement la timeline + sélectionner les compositions, génère timeline.json / sub.srt / storyboard.json
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage cues     # Générer seulement les points de repère -> work/cues.json
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage score    # Générer seulement la musique -> work/music.wav (musicgen génère sur place)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage sfx      # Générer seulement la piste d'effets sonores (sfxkit, selon cues.json)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage audio    # Mixer seulement (voix+musique+effets, priorité à la voix)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames   # Rendre seulement les images (--engine pil|web, --jobs N parallélisation par blocs)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage mux      # Assembler seulement (flux vidéo copié, double passe loudnorm)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage qa       # QA seulement (lecture seule, écrit dans work/qa/)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage stills   # Rendre seulement les images fixes de la maquette + planche contact
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate stills   # Validation manuelle d'une barrière (écrit work/signoff.json)
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage explain  # Rapporter seulement "quelles étapes sont obsolètes, pourquoi, quoi exécuter", n'exécute aucune étape

Ordre complet de --stage all : style → tts → layout → cues → score → sfx → audio → frames → mux → qa. Si une étape échoue, elle s'arrête à cette étape, ne continue pas avec une mauvaise entrée.

Calculer le coût avant de lancer le rendu (optionnel ; estimate.py est un outil du dépôt, appelé selon <REPO>) :

python <REPO>/tools/estimate.py --script film.json --jobs 4

Il donne le nombre d'images/le temps par étape/le disque/la suggestion de parallélisation (--json lisible par machine), c'est une borne supérieure pas une promesse ; si work/timeline.json existe, il utilise la durée réelle, sinon estimation grossière par nombre de caractères. Pour économiser le VMAF complet, écrivez dans film.json "qa": {"vmaf": {"mode": "off"}} (estimate et render lisent ce champ, G11 noté skipped).

Voir les preuves de livraison avant validation :

cat out/QC-REPORT.md     # gate matrix divisé en pass / fail / skipped ; skipped = pas exécuté cette fois, ne veut pas dire réussi
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate release

La validation est liée à l'empreinte du contenu : si le scénario / scenes.py / le design change, l'ancienne validation est automatiquement invalidée, il faut revoir et resigner. Pour forcer avant livraison (skipped compte aussi comme échec) : ajoutez --strict-gates.

Rendu de longs métrages (parallélisation par blocs + reprise) :

python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames --jobs 4
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames --jobs 4 --resume

Les blocs sont sauvegardés dans work/segments/, nommés seg_<image début>_<image fin>.mp4 ; avant l'assemblage, les trous sont explicitement détectés, un bloc manquant provoque une erreur, il n'est pas permis de produire une vidéo avec un segment manquant. Par défaut, la taille des blocs est total_frames / jobs / 4 arrondi à la seconde entière (jobs=4, 900 images donne environ 8 secondes par bloc), peut être écrasée avec chunk_frames dans film.json.

Après avoir modifié les répliques / modifié scenes.py / modifié le design / modifié les paramètres d'encodage, --resume refusera de réutiliser les anciens blocs (le plan fingerprint est stocké dans work/segments/_plan.json). Une erreur vous invitera à rm -rf work/segments — c'est intentionnel : réutiliser d'anciens blocs avec une nouvelle timeline produirait une vidéo "où l'image et la voix ne correspondent pas", et la barrière de durée ne le détecterait pas.

2. Huit étapes standard (P0–P7)

ÉtapeProductionAction cléErreur courante
P0.5 Définir le designwork/design.json + work/style.jsonpropose.py génère des propositions → --confirm gèle le design ; --stage style analyse seulement le design confirmé et imprime le thème/langage visuel/alternance de mise en page/sous-titresSauter → render.py erreur (pas de design = pas d'apparence) ; rendu sans confirmation = avertissement de proposition
P0 Établir les basesCONVENTIONS.mdCalculer une fois pour toutes la résolution/fps/durée/BPM→images, tout le projet se réfère à la même sourceChaque fichier écrit des nombres magiques comme 150 → les limites des scènes ne s'alignent pas
P1 Écrire le scénariofilm.jsonChaque phrase sépare text (parlé) et sub (sous-titre) ; chaque segment un kind ; optionnel lines[].type pour verrouiller la compositionUn champ utilisé pour deux choses → sous-titre trop long ou lecture bizarre ; n'écrivez pas style (n'influence plus l'image, l'apparence est décidée par work/design.json)
P2 Vérification des faitsFACTS.mdChaque chiffre/année à l'écran a une référence ; créer une liste noire "ne pas afficher"Sauter cette étape → les chiffres à l'écran et la voix se contredisent, le premier commentaire vous donnera tort
P3 TTSwork/tts/*.mp3 + tts.jsonSynthèse phrase par phrase edge-tts, mesure réelle de la durée avec ffmpeg -f f32le, cache disqueEstimer la durée par nombre de caractères → les sous-titres dérivent
P4 Construire la timelinework/timeline.json + sub.srtdur = lead + durée vocale réelle + tail, la ponctuation doit être dans ce segmentSegment trop court → la voix off est coupée par le segment suivant
P5 Mixagework/audio.wavBus numpy : voix + musique + effets sonores superposés échantillon par échantillonMixage ffmpeg multi-entrées → silences non déterministes
P6 Maquette3 images fixesRendre d'abord 3 images pour vérifier la composition/palette/poids de la police, une fois décidé, rendre tout le filmRendre directement tout le film → 36000 images plus tard, la police est moche
P7 Film finalout/film.mp4rawvideo→libx264 CRF18 + double passe loudnorm à -16 LUFSDébit trop bas / son à -25 LUFS → "comme une ébauche"

Remarque : filmkit / render.py utilise par défaut crf=18 ; assets/film.json n'est qu'un outil de test (pour la régression de la chaîne complète et des images de référence/barrières, pas un modèle à copier), il utilise 17. Quand vous écrivez votre propre scénario, si vous n'écrivez pas crf, c'est 18 ; pour des textes plus nets, vous pouvez écrire 17 (taille environ +30%).

3. Comment choisir le moteur de rendu

ScénarioChoixVitesse réelleAdapté à
Texte et images / données / vulgarisation / style UIPIL (numpy+cv2 en aide) — par défaut filmkit40–80 fps @1080p30 (exemple 75)La plupart des vidéos de vulgarisation chinoises, rapports transformés en vidéo
Nécessite des vecteurs/particules/3D/texture de papier fluidesCanvas2D/WebGL sans tête + capture CDP → image2pipe (webengine.py)80–130 ms/image (iGPU, capture PNG)Longs métrages de niveau documentaire, nécessitant un langage visuel complexe
Animation pure de donnéesmanim (si installé)Selon la scèneMathématiques / dérivation de formules

Les vitesses ci-dessus sont mesurées sur une seule machine à vide (changement de machine, images plus lourdes modifient les résultats ; voir §12). Ne prenez pas les intervalles comme des garanties.

Le design ne couvre que le moteur PIL (engine: pil, par défaut) : les couleurs/polices/mise en page/motifs de work/design.json sont lus par assets/scenes.py. La page du --engine web est écrite par vous (assets/web/ n'est qu'un exemple fonctionnel, pas un modèle de production), l'image ne suit pas le design ; mais la musique et la voix suivent design.json (elles ne dépendent pas du moteur de rendu).

Par défaut, PIL : peu de dépendances (numpy+Pillow suffit), démarrage rapide, forte déterminisme, faible coût de débogage. Utilisez la solution navigateur uniquement pour "transformation fluide de particules / texture en temps réel avec shader" :

cp -r <SKILL_DIR>/assets/web ./web                     # Modèle de page (Canvas2D natif, zéro bibliothèque tierce)
pip install playwright && python -m playwright install chromium   # Nécessaire seulement avec le moteur web
python render.py ... --engine web --stage cues        # Exporter les points de repère depuis la page
python render.py ... --engine web --stage frames --jobs 3 --resume
python webengine.py textqa ./web/index.html            # Texte qui se chevauche / sort du cadre / disparaît / n'apparaît jamais

Compromis entre les deux voies (réfléchissez d'abord, changer de moteur nécessite de refaire l'image) :

PIL (engine: pil)Navigateur (engine: web)
Dépendancesnumpy + Pillow (+cv2 pour comparaison de netteté)+ playwright + Chromium/Edge
Vitesse40–80 images/s8–12 images/s (capture d'écran image par image + encodage, un ordre de grandeur plus lent)
Langage visuelComposition bitmap, dégradés, géométrie, texteCanvas2D/WebGL : particules, transformations, shaders, animations vectorielles
DéterminismeNaturel (fonction pure pour dessiner)Garanti en cherchant précisément la timeline à f/FPS (window.renderFrame)
DébogageModifier une image et l'exécuterDans le navigateur, index.html?dev=1&t=secondes pour voir directement, les bugs sont faciles à trouver
Adapté àTexte et images / données / vulgarisation / vidéos promotionnellesLongs métrages de niveau documentaire, nécessitant un langage visuel fluide

Trois contraintes strictes pour le moteur navigateur (déjà implémentées dans webengine.py, à respecter aussi si vous écrivez votre propre page) :

  1. Toutes les interpolations sont placées sur une timeline principale en pause ; rendre l'image f = la chercher à f / FPS secondes ; interdiction de dépendre de la lecture.
  2. Les contenus programmatiques comme les boucles/particules/vagues utilisent un "calcul direct selon le temps" (update(fn)), interdiction d'utiliser setInterval pour accumuler.
  3. La page doit exposer window.ready / DURATION / renderFrame(f) / getCues() ; pas de CDN, pas de requête réseau.

Pour une reproductibilité au niveau octet entre sessions navigateur (crash et redémarrage, rendu multiple pour diff, QA compare deux images), activez le mode déterministe : --deterministic ou variable d'environnement WEBENGINE_DETERMINISTIC=1 (désactive le GPU raster, testé 8/8 sessions cohérentes, coût environ +22%/image). Désactivé par défaut : au sein d'une même session, c'est déjà cohérent ; entre sessions, seulement ≤4 pixels, ±1 niveau de différence d'anticrénelage.

4. Liste de contrôle qualité (ces éléments déterminent si "ça ressemble à une vidéo professionnelle")

  • Luminosité et profondeur : fond sombre (#0a1420) + accent lumineux en haut ; évitez le noir/blanc pur, l'écran de l'utilisateur mangera les dégradés.
  • Poids de la police et taille : gros texte 84–96px, sous-titres 34–38px, sous-titres 46px ; chinois utilisez msyhbd.ttc (Microsoft YaHei Bold).
  • Sous-titres : ≤ 2 lignes, environ 24 caractères chinois par ligne, avec fond semi-transparent, y ≥ 872. Le retour à la ligne réel est basé sur la largeur en pixels (fit_sub_lines utilise max_w=1560 px pour le retour automatique), 24 caractères est une "limite approximative" pas une validation stricte ——la constante SUB_MAX_CHARS=24 n'est actuellement référencée par aucun code, ne comptez pas sur elle pour vous arrêter.
  • Rythme : un changement visuel toutes les 3-6 secondes (nouvelle carte / déplacement / défilement de chiffres) ; statique plus de 6 secondes, le spectateur part.
  • Caméra : seulement un lent zoom de 3-15%, pas de grands mouvements avant/arrière.
  • Étalonnage : une couleur principale + une couleur d'accent pour tout le film, ne dépassez pas 4 teintes.
  • Audio : master -16 LUFS / crête vraie ≤ -1 dBTP ; voix off 12 dB au-dessus du silence ; marge de 1,4 dB avant AAC.

5. Son : musique + effets sonores + points de repère

L'idée de cette section vient d'un retour d'expérience sur une vraie vidéo (ai-history-film) : un mauvais son n'est pas "pas assez fort", mais "le son ne connaît pas l'image". L'ordre de résolution est toujours : d'abord faire savoir au son ce qui se passe à l'image (points de repère), ensuite parler du timbre, enfin du master.

5.1 Trois couches sonores

CoucheQui faitSauvegardeDescription
Voix offfilmkit.tts (edge-tts)work/tts/*.mp3Seule autorité de durée : la timeline est dérivée de celle-ci
Musiquemusicgen.py (orchestration paramétrique)work/music.wavGénérée sur place selon les paramètres bpm/key/scale/mood/seed du design + paramètres d'orchestration ; pas de style musical prêt à l'emploi
Effets sonoressfxkit.py (modélisation physique)work/sfx.wavSynthétisés un par un selon les points de repère enregistrés dans cues.json

Les trois bus sont mixés dans --stage audio : quand un effet sonore se déclenche, la musique s'efface automatiquement (duck_db par défaut 4 dB, cinema/epic 5.5 dB — avec une atténuation de 3 dB, la musique est presque au même niveau que l'effet sonore pendant la transition, l'audition devient confuse), puis confié à ffmpeg double passe loudnorm à -16 LUFS / crête vraie ≤ -1 dBTP. Ne normalisez pas vous-même le niveau final dans numpy — loudnorm est bien plus précis que la normalisation manuelle, et la crête vraie est la barrière.

Séparation intra-film : la musique a déjà des transitions aux limites des segments (swell / riser / sub_boom écrits par l'orchestrateur), la piste sfx ne répète pas whoosh/riser à la même limite — --stage sfx applique automatiquement -3 dB (sfx.duck_transitions=True) aux cues de transition qui tombent dans [limite - 2.2 s, limite].

Skills associés