Processus complet de création d'animation de tableau blanc dessiné à la main (SRT → vidéo animée)
Basé sur l'expérience pratique du projet open-source srt-whiteboard-animation (github.com/geeklee/srt-whiteboard-animation). Résultat : MP4 animé de type « main qui dessine en parlant » avec voix off + captures d'écran clés + enregistrement d'écran.
Règles absolues (à respecter impérativement, basées sur des échecs réels)
- Interdiction d'utiliser des images d'exemple comme livrable. Les fichiers
scene-01-monkey-mountain-banana.pngdansexamples/servent uniquement à valider le pipeline. Vous devez générer des images en fonction du sujet demandé. Utiliser des exemples = échec. - L'image source doit être nettoyée. Les fonds beige papier générés par l'IA ont beaucoup de grain. Le rendu
adaptiveThresholdinterprète ces grains comme de l'encre, ce qui rend les traits pointillés et le fond bruité. Exécutez toujoursscripts/clean_source.pyavant le rendu. - Le dernier élément ne doit pas utiliser la région entière.
allowed mask = region - regions suivantes. Si la région du dernier élément est le canevas entier, elle vide lesallowedde tous les éléments précédents, ce qui rend les premières scènes vides (confirmé par des tests). La scène finale doit sélectionner une petite zone réelle (par exemple, une bande de nuages en haut). - Redimensionnez le sprite de la main en fonction du canevas.
drawing-hand.pngfait 1069×1472. Collé sur 1280×720, il occupe 40%+ de l'écran, masquant le contenu. Réduisez à 55% (587×809, largeur ~21% du canevas) pour un rendu naturel. - La durée du rendu est plus courte que l'annotation. Le moteur de rendu détermine le nombre d'images en fonction des pixels réellement dessinables dans chaque région.
sceneDurationMsn'est qu'une limite supérieure. Test : 8×8000ms=64s d'annotation n'ont produit que 45.8s de vidéo. Utilisez--total-mset calibrez avecffprobe. - Ne terminez pas l'enregistrement d'écran avec kill.
ffmpegtué de force perd l'atome moov, rendant le fichier illisible (moov atom not found). Utilisez-t <secondes>pour que l'enregistrement se termine naturellement. - L'enregistrement d'écran et les captures d'écran sont obligatoires. Ce n'est pas une option. Les captures doivent couvrir : l'image source, l'image de validation des annotations, la console de rendu, les images clés de la vidéo finale. Remplacer les captures par des notes documentaires = travail incomplet.
- Un canevas ne raconte qu'une seule histoire, avec une progression continue. Erreur réelle : répartir un drapeau rouge, un pont, une montagne enneigée et une pagode sur une même image, dessinés tous en même temps → l'utilisateur a jugé « le dessin est brouillon ». La bonne méthode est de disposer les éléments le long d'une ligne directrice unique (itinéraire géographique / chronologie) et de faire avancer le trait en continu dans cette direction. Le spectateur voit « le crayon qui marche », pas « on ajoute des détails partout sur la toile ».
- La disposition spatiale doit respecter la direction réelle. Pour la Longue Marche, dessinez selon la géographie réelle : Ruijin au sud-est → vers l'ouest → vers le nord → Yan'an. Une composition erronée ne peut pas être sauvée par un bon rythme. Avant de dessiner, confirmez la position relative de chaque nœud sur une carte.
- Le rythme du dessin = le rythme de la narration, un élément par phrase. La
durationMsde chaque élément doit être égale à la durée réelle de la phrase de narration correspondante (déduite de la détection de silence parsync_timing.py). La main doit s'arrêter exactement à la fin de la phrase correspondante. - Les scènes et le commentaire doivent correspondre sémantiquement une à une. Erreur réelle : la pagode (Wuqi) a été placée dans la scène de la phrase de conclusion. Résultat : le commentaire disait « arrivée à Wuqi » pendant que l'image dessinait autre chose. La phrase de conclusion finale (…vingt-cinq mille lis…) ne doit pas être associée à un nouvel élément. Laissez-la tomber sur « l'image entièrement dessinée et figée ». Le moteur de rendu ajoutera automatiquement des images vides pour la durée
--total-msrestante, créant un effet de contemplation. - Les sous-titres doivent être incrustés séparément, ne comptez pas sur
force_style. Le SRT pur n'a pas de PlayRes. libass utilise une résolution par défaut interne pour calculerFontSize/MarginV, ce qui rend le réglage impossible. Utilisezmake_srt.py→srt2ass.py(définissez explicitement PlayRes = taille de la vidéo, et ancrez le bas de ligne avec{\an2\pos(x,y)}) pour un contrôle total. - Une seule ligne par sous-titre. Deux lignes occupent deux zones de l'écran (haut et bas). En chinois, deux lignes coupent le souffle. Si une ligne ne suffit pas, découpez en plusieurs sous-titres consécutifs (en répartissant la durée proportionnellement au nombre de caractères). Ne réduisez pas la taille de police. Test : police 46px + 14 caractères max par ligne sur 720p = environ 60% de la largeur de l'écran, confortable à lire sur mobile.
- Le canevas doit pouvoir se déplacer, ne mettez pas l'image entière dans un plan fixe. Le retour d'utilisateur : « inutile de dessiner tout sur un seul canevas ». La bonne méthode : agrandissez le canevas (x2), et pour chaque scène, cadrez uniquement l'élément en cours pour qu'il remplisse l'écran. Une fois dessiné, faites un panoramique vers le suivant. Terminez par un zoom arrière pour montrer l'ensemble. Un plan fixe qui montre tout du début à la fin = chaque élément n'occupe que 10% de l'écran, invisible.
- La pointe du stylo doit suivre le contenu. La fonction
contour-wipedu projet amont fait un « effacement de bas en haut + balayage horizontal aléatoire de la pointe », les deux sont complètement déconnectés → l'utilisateur voit « la main bouge mais rien n'apparaît ». Remplacez par un pinceau serpentin : la couleur apparaît là où la pointe du stylo balaie. - Écrivez aux nœuds clés. Ne dessiner que des objets sans texte fait que le spectateur ne retient ni la date ni le lieu. Incorporez les noms de lieux/années dans l'image de base en police cursive (avec
add_handwriting.py). Ensuite, faites-les écrire par le moteur de rendu en modetextwrite, colonne par colonne – le trait avance lettre par lettre, comme une écriture manuscrite. - Après avoir agrandi le canevas, faites ces deux choses, sinon c'est soit trop lent, soit flou :
① Agrandissez l'image source avec
INTER_CUBICà la taille du canevas (leresizeinterne du moteur utiliseINTER_AREAqui, en agrandissant, est équivalent au plus proche voisin et rend les traits en blocs) ; ② Modifiez l'application de l'encre pour qu'elle ne traite que localement selon la boîte englobante (le code amont crée un nouveau masque de canevas entier pour chaque segment, ce qui est 4 fois plus lent sur un canevas de 2560 pixels). - Laissez 3 à 7 secondes pour le zoom arrière final dans la vidéo sans la voix. Ne programmez pas la dernière scène jusqu'à la dernière milliseconde de la narration. Sinon, il n'y a pas de temps pour l'action de « montrer l'image entière » en fin de vidéo.
- Utilisez la « boîte englobante réelle de l'encre » pour le cadrage, pas la région d'annotation. La région est un cadre dessiné à la main, généralement plus grand que le graphique. Dans une image dense, elle peut même chevaucher 2-3 voisins. L'utiliser pour calculer le zoom donne un résultat où « le centre de l'image est vide, le graphique est dans un coin, et le voisin est coupé en deux ». Le moteur de rendu intègre déjà : dans
set_target(element, allowed), utilisezink_pixels & allowedpour trouver la bbox, puis ajoutez une marge. Exception : ne pas utiliser la bbox pour les étiquettes de texte – les caractères de l'étiquette sont souvent plus larges que la région. Appliquer la bbox couperait la phrase entière hors du cadre (testé avec « conversationnel IA » qui sortait de l'écran). - Faites d'abord un panoramique de la caméra, puis commencez à dessiner au début de chaque scène. Laissez environ 5 images (0.22 s) de rotation à vide au début de chaque scène. Sinon, le crayon commence à dessiner avant que la caméra ne soit en place, et dessine hors champ → le spectateur voit « la main bouge mais rien n'apparaît ».
- Utilisez
--zoom-fillavec des valeurs différentes selon la densité du fond de l'image : 0.86 pour les images clairsemées, 0.70 pour les chronologies denses. Un seul paramètre pour tous les cas provoque des échecs. - Acceptez que les éléments longs et verticaux ne remplissent pas l'écran. Dans un écran 16:9, un élément vertical n'occupera que 60-70% de la hauteur et 25% de la largeur. C'est une contrainte géométrique. Pour un remplissage plus important, dessinez l'élément plus large ou passez en mode portrait.
- Générez la voix off en un seul bloc, ne la découpez pas phrase par phrase pour la concaténer. La concaténation de segments donne un débit/des pauses incohérents et un décalage inévitable de la timeline. L'utilisateur entendra un discours « incohérent ». Jimeng peut gérer plus de 1500 caractères en une seule fois (environ 2 minutes). Fournissez l'intégralité du texte en une fois.
- La timeline doit être basée sur les horodatages de la transcription de la voix off, pas sur la détection de silence. Pause ≠ fin de phrase. Si le nombre de phrases et le nombre de pauses ne correspondent pas, vous ne pouvez que les répartir uniformément, ce qui rend les sous-titres incorrects. Utilisez
asr_align.py(ffmpeg → faster-whisper word_timestamps → timeline au niveau des caractères). - Si vous changez la voix off, vous devez régénérer les sous-titres, le timing et la durée des scènes. Les horodatages des sous-titres sont une propriété du fichier audio, pas de l'image. Utiliser les anciens sous-titres avec une nouvelle voix = travail nul.
- Avant la livraison, comparez le « texte original vs la transcription » avec difflib. Si la similarité est <0.8, ou si des phrases entières sont manquantes/répétées, l'audio est défectueux, régénérez-le. Les erreurs de reconnaissance de mots homophones (ex: 弘/红, 尊/遵) sont normales pour un ASR et ne sont pas considérées comme des erreurs de lecture.
- Après la génération de la voix off, effectuez un contrôle médical du silence. Envoyer le texte entier à un TTS ne signifie pas que la plateforme ne le découpe pas en interne : Jimeng insère du silence absolu à -120dB entre les blocs (testé : 4.00s / 2.60s). Si la proportion de silence dépasse 20% ou s'il y a une pause >1.2s, utilisez d'abord
tighten_audio.pypour réduire, avant de parler d'alignement de timeline. - Le point de départ du sous-titre doit être accroché à l'image réelle de la première vocalisation. En présence de long silence, ASR marque le
startde la phrase suivante au début du silence. Le spectateur « regarde le sous-titre en attendant le son ».asr_align.pya une option par défautsnap_to_voicepour corriger cela. Ne la désactivez pas.
Norme de répertoire (obligatoire)
D:/04-Tests-Projets/<YYMMDD>-<Nom du projet>/
00-Documentation/ Documentation du projet et notes de code source
01-Resources/ Images originales générées par IA, sprite de la main
02-Subtitles-Voice/ SRT, mp3 de la voix off, tableau des durées
03-Annotations/ <nom de l'image>.png + <nom de l'image>.annotation.json (même nom et même dossier, nécessaire pour l'aperçu)
04-Rendu/ silent.mp4 par scène, mp4 de la vidéo finale
05-Captures-Enregistrements/ mp4 de l'enregistrement d'écran + png des captures clés
06-Documents/ Scripts, enregistrement des opérations, liste de vérification
Prérequis
git clone https://github.com/geeklee/srt-whiteboard-animation.git
cd srt-whiteboard-animation && python scripts/prepare_env.py # Crée .venv, installe opencv/numpy/av
Toutes les commandes de rendu suivantes utiliseront srt-whiteboard-animation/.venv/Scripts/python.exe.
Si vous obtenez _lay_ink() takes 6 positional arguments but 7 were given, c'est un bug connu du dépôt :
dans render_stream_whiteboard.py, self._lay_ink(writer, ink_frames, [], set(), None, allowed)
il suffit de supprimer le None en trop (le script scripts/check_bug_fix.py peut le détecter et le corriger automatiquement).
Workflow (7 étapes : storyboard → génération d'image → planche narrative spatiale → annotation → doublage et alignement → rendu → sous-titres)
Étape 1 · Script et storyboard (confirmer avant de continuer)
- Écrivez 6-10 phrases de commentaire, chacune durant 6-10 secondes, pour une durée totale de 40-90 secondes.
- Analysez le storyboard (optionnel, vous pouvez aussi le faire manuellement) :
<ENV_PY> srt-whiteboard-animation/scripts/parse_srt.py sous-titres.srt --target-sec 30 --min-sec 25 --max-sec 35 - Produisez un tableau de storyboard (Numéro de scène / Expression clé / Sujet visuel / Texte du sous-titre), arrêtez-vous et attendez la confirmation de l'utilisateur.
Étape 2 · Génération de l'image de thème par IA
Selon la « Spécification visuelle unifiée de génération d'images du projet », rédigez le prompt, chaque prompt doit contenir :
- Trait de crayon minimaliste / pur esquisse au crayon / gribouillage sobre style Notion, 16:9, beaucoup d'espace blanc, éléments non superposés
- Fond beige chaud
#F5EBD7, traits gris foncé#333333, seulement quelques accents en rouge/orange/bleu - Interdiction absolue de tout texte (caractères chinois, lettres, chiffres, étiquettes, noms de lieux, titres, filigranes), interdiction de réalisme/3D/dégradé/haute saturation
Utilisez ImageGen ou Jimeng image pour générer l'image. Chaque image correspond au sens principal d'une scène. Les éléments doivent pouvoir être séparés par des régions rectangulaires.
Après la génération, faites trois choses :
- Supprimez le filigrane : Les images générées par l'IA ont souvent
AI généré WORKBUDDYen bas à droite. Effacez-le aveccv2.inpaint. - Mettez au format 16:9 : Les images générées sont souvent en 3:2. Remplissez les côtés gauche et droit avec du beige papier, puis redimensionnez à 1280×720 (ne recadrez pas centré, cela couperait le sujet).
- Nettoyez : Supprimez le grain du papier, conservez les traits et les accents de couleur.
Principe :<ENV_PY> scripts/clean_source.py image_originale.png 03-Annotations/image_principale.pngpapier = filtre médian à grand noyau,traits = gris < papier-18,couleur = saturation>70 et gris<190, ne conservez quetraits|couleur, remplissez le reste avec la couleur du fond du papier ; forcez la couleur du fond du papier sur les 8 pixels de bordure pour éliminer les résidus de bord.
Étape 2.5 · Planche narrative spatiale (détermine si la vidéo finale est « brouillonne », doit être faite en premier)
Avant d'écrire le prompt de génération d'image, définissez d'abord la ligne directrice par écrit. Divisez en deux types :
| Sujet | Ligne directrice | Principe de disposition |
|---|---|---|
| Itinéraire historique / Bataille / Migration | Itinéraire géographique réel | Le point de départ est à l'emplacement réel. Allez « Ouest → Nord → Est » si c'est le cas. Les coordonnées x/y des nœuds doivent correspondre aux emplacements réels. |
| Histoire de la technologie / Histoire de l'entreprise | Une chronologie horizontale | Une piste traverse toute l'image, les nœuds sont équidistants, les icônes sont alternées (haut → bas → haut) pour éviter de les entasser toutes d'un côté. |
Contraintes strictes :
- Une seule ligne directrice. N'ajoutez pas d'éléments divergents du type « note complémentaire » sur cette ligne.
- Les éléments ne se chevauchent pas, et chaque élément doit pouvoir être contenu dans un rectangle qui ne traverse pas la ligne directrice (sinon les régions s'annulent mutuellement).
- La ligne directrice doit traverser toute l'image : l'espace blanc doit être réparti uniformément des deux côtés de la ligne directrice. Ne faites pas « tout entassé en bas à droite, une grande zone vide en haut à gauche ». Test : pour la Longue Marche, le point de départ était au sud-est, en bas à droite. Les 20 premières secondes montraient une accumulation en bas à droite et un vide en haut à gauche – c'était un défaut de composition. Dès la génération d'image, placez le point de départ plus au centre pour que la ligne d'itinéraire utilise tout le canevas.
Dans le prompt de génération d'image, indiquez la position de chaque élément dans l'image (« un petit drapeau rouge en bas à droite », « un pont de corde en haut à gauche »). Ne vous contentez pas d'écrire « dessinez un pont ». Si la position n'est pas spécifiée, la composition donnée par l'IA sera inutilisable.
Étape 2.6 · Deux paradigmes d'image de fond (paramètres testés, à copier directement)
Le fait que la vidéo finale « puisse remplir l'écran ou non, que la caméra puisse avancer ou non » est déterminé dès l'étape de génération d'image. Chaque paradigme a son propre ensemble de paramètres, ne les mélangez pas :
A. Type itinéraire géographique (histoire/bataille/migration) – Représentant : Itinéraire de la Longue Marche de l'Armée Rouge
| Élément | Valeur | Justification |
|---|---|---|
| Composition | Le point de départ suit la position réelle, l'itinéraire Ouest→Nord→Est traverse toute l'image | Si la composition est erronée, elle ne peut pas être sauvée, même avec un bon rythme |
| Nombre de nœuds | 6-8 étapes | Une étape = deux scènes (dessin de l'objet + écriture) → 12-16 éléments |
Objectif --zoom-fill | 0.86 | Beaucoup d'espace blanc entre les éléments, se rapprocher rend l'image non vide |
| Densité | Les éléments peuvent être grands ou petits (un pont peut être 3 fois plus large qu'un drapeau) | L'itinéraire lui-même est constitué d'éléments de différentes échelles |
B. Type chronologie horizontale (histoire de la technologie/histoire de l'entreprise/itération de produit) – Représentant : Histoire du développement des agents intelligents (Version que l'utilisateur a jugée « très bonne, l'effet est excellent », paramètres à copier directement)
| Élément | Valeur | Justification |
|---|---|---|
| Piste | Un axe principal en forme de S ou d'onde traversant toute l'image, 8 nœuds équidistants | Équidistance → vitesse de panoramique constante, rendu stable |
| Icônes | En alternance haut/bas (haut → bas → haut…), pas toutes sur une seule ligne | Sur une seule ligne, seule la moitié de l'image est disponible, le cadrage gaspille la moitié de la hauteur |
| Étiquettes | Accrochées au côté extérieur du même côté de l'icône : étiquette des nœuds du haut en haut, étiquette des nœuds du bas en bas | Situées au même écran que l'icône, la caméra n'a pas besoin de faire un panoramique pour écrire |
| Taille de police | Nom principal 36-40px en cursive, année 0.6 fois en dessous | Une fois le zoom effectué ≈ 60-80px à l'écran, lisible sur mobile |
Objectif --zoom-fill | 0.70 | Les nœuds sont proches les uns des autres. Un zoom trop important coupe les voisins en deux |
| Rapport des deux scènes d'une étape | Dessiner l'icône 0.60 / Écrire « nom + année » 0.40 | Il faut laisser suffisamment de temps pour écrire le sous-titre, sinon il est coupé avant d'être fini |
| Ligne de temps elle-même | Dessinée entre deux étapes adjacentes, traitée dans la même scène que l'icône de cette étape | Sinon, la ligne entière apparaît d'un coup, cassant le rythme |
Trois mises en garde importantes pour le type B :
- N'utilisez pas la boîte englobante d'encre pour les sous-titres de texte. (Voir exception de la règle 19). Les caractères de l'étiquette sont souvent plus larges que la région. Appliquer la bbox couperait la phrase entière.
- La région de l'étiquette doit être assez haute pour inclure « nom principal + année » sur deux lignes. Donnez une hauteur de 80-90 pixels. Si vous ne donnez que la hauteur du nom principal, l'année sera coupée.
- La fin doit faire un zoom arrière vers l'ensemble de la chronologie. La valeur narrative de la chronologie réside dans son « étendue globale ». Ne pas faire de zoom arrière pendant les 3 à 5 dernières secondes revient à n'avoir rien dessiné.
Base de test (1280×720 / 24fps) : 8 nœuds × 2 scènes = 16 éléments, vidéo finale 43.5s, rendu 33s,
--zoom-fill 0.70 --zoom-max 7 --hand-height 350.
Étape 3 · Annotation au niveau du pixel (le plus sujet aux erreurs, doit être validée)
Mesurez d'abord les coordonnées, ne les estimez pas à l'œil. Leçon apprise : confondre la taille d'affichage de l'aperçu (640×360) avec les coordonnées réelles (1280×720) a conduit à deux séries de coordonnées complètement erronées. La bonne méthode est d'utiliser la détection de couleur/trait pour obtenir la bbox réelle :
<ENV_PY> scripts/detect_marks.py 03-Annotations/image_principale.png étiquette # bbox des zones connexes d'accent rouge/orange/bleu
<ENV_PY> scripts/detect_lines.py 03-Annotations/image_principale.png étiquette # bbox des amas de traits gris foncé
Pour un calibrage manuel, superposez une règle : scripts/grid_ruler.py image_principale.png image_avec_grille.png (grille de 100px, marquée en pixels réels).
Auto-vérification du taux de couverture des régions (détermine si la vidéo finale est complète) :
- Les intervalles x de toutes les régions doivent former une bande ininterrompue (les chevauchements sont autorisés, pas les espaces vides).
- Les sujets traversants (itinéraire, chronologie) doivent être entièrement couverts par certaines régions, sinon la vidéo finale présentera des coupures.
- Le dernier élément doit sélectionner une petite zone spécifique (voir règle 3).
Écrivez 03-Annotations/<nom>.annotation.json :
{
"sceneId": "scene-01",
"canvas": { "width": 1280, "height": 720 },
"storyBasis": "Résumé de l'événement du sous-titre de cette scène",
"sceneDurationMs": 56000,
"elements": [
{
"id": "ruijin", "label": "Départ·Ruijin", "sequence": 1,
"narrativeRole": "Point de départ de la Longue Marche", "subtitle": "Texte du sous-titre correspondant",
"type": "point",
"region": { "x": 265, "y": 515, "width": 145, "height": 100 },
"reveal": { "direction": "left_to_right", "startMs": 0, "durationMs": 8100,
"maskPaddingPx": 16, "protectedRegions": [] },
"handPath": { "start": [275, 570], "end": [400, 575], "easing": "easeInOut" }
}
]
}
canvasdoit être égal aux pixels réels de l'image source ;regiondoit être en pixels entiers et à l'intérieur du canevas.sequencesuit l'ordre narratif, pas l'ordre visuel de gauche à droite.- Sous le mode de dessin
stream,direction/handPathsont uniquement pour la démonstration dans l'aperçu, ils n'affectent pas le trait de la vidéo finale.
Générez une image de validation et vérifiez visuellement image par image :
<ENV_PY> scripts/region_sheet.py image_principale.png annotation.json 05-Captures-Enregistrements/validation-region.png
Chaque case doit encadrer exactement l'élément cible. Si elle encadre du blanc ou un autre élément → coordonnées erronées, revenez à l'étape précédente.
Étape 4 · Voix off et alignement de la timeline
① Voix off : Générer l'intégralité du texte en une seule fois, ne jamais découper phrase par phrase (leçon apprise le 2026-11-05)
"<TTS_PYTHON> # N'importe quel Python 3.9+ avec requests installé" \
"jimeng-suite/scripts/jimeng_cli.py" audio \
--text "Texte complet du commentaire, en un seul paragraphe, utilisez des points pour marquer les phrases" \
--model "SeedAudio 1.0" --submode "Voix off polyvalente" --voice "狒狒_讲解" \
--out "xxx-voice.mp3" --outdir "<projet>/02-Subtitles-Voice" --timeout 900
Seed TTS + Lecture de textea été retiré le 2026-10-24, ne l'utilisez plus.--textdoit contenir l'intégralité du texte en une seule fois. Il peut gérer plus de 1500 caractères (environ 2 minutes d'audio). Ne le découpez plus en une multitude de petits segments de 3 secondes. Test : découper un script de 56 secondes en 12 segments générés séparément puis concaténés donne un débit/des pauses/un accent incohérents, et la timeline est inévitablement décalée par rapport aux sous-titres – l'utilisateur juge directement le travail comme « incohérent ».- Découpez uniquement par paragraphe narratif (un groupe de phrases formant une idée), chaque paragraphe 15-40 secondes, 1-3 paragraphes pour l'ensemble de la vidéo. Si un seul paragraphe suffit, utilisez-le.
- La modération du contenu peut bloquer : les termes militaires, violents ou liés aux blessures (bataille sanglante, anéantir, vaincre, ennemi) retournent
JimengContentRejectedError. Remplacez-les par des formulations neutres („payer un lourd tribut“, „sous le feu de l'ennemi“). Écrire les années en chiffres chinois (一九三四年) est plus sûr. - Test : 99 secondes de dialogue ont produit une vidéo de 62 secondes (1.19 Mo / 99.05 s).
② Contrôle médical du silence et compression du rythme (obligatoire, leçon apprise avec la v5)
« Envoyer le texte entier à un TTS » ne signifie pas que la plateforme le synthétise en un seul bloc : Jimeng découpe le texte long en blocs internes et les concatène en insérant du silence absolu à -120dB entre les blocs. Test sur Longue Marche v4 : sur 317 caractères / 99.05s, 31 pauses ≥0.4s totalisant 27.2s (27.5%), dont deux (4.00s @14.05 et 2.60s @70.60) étaient un silence numérique pur (le silence naturel a un bruit de fond d'environ -50 à -60dB, pas -120dB). La durée d'émission réelle n'était que de 71.85s (4.41 caractères/s, ce qui n'est pas lent) – le sentiment de « rythme lent, bizarre » était entièrement dû aux blancs.
# 1) Contrôle médical : toute pause >1.2s doit être traitée
ffmpeg -hide_banner -i xxx-voice.mp3 -af "silencedetect=noise=-38dB:d=0.35" -f null - 2>&1 | grep silence_start
# 2) Compression : préservez les pauses naturelles de manière graduée, ne modifiez aucun contenu de parole
<ENV_PY> scripts/tighten_audio.py 02-Subtitles-Voice/xxx-voice.mp3 \
02-Subtitles-Voice/xxx-voice-tight.mp3 --report 06-Documents/_tighten.json
tighten_audio.py : fenêtre RMS de 50ms, <-40dB considéré comme silence, pause ≥1.0s réduite à 0.55s,
pause 0.35–1.0s réduite à 0.32s, début du fichier réduit à 0.12s, reconcaténation par échantillon.
v4→v5 test : 99.05s → 83.15s, proportion de silence 27.8% → 14.0%, aucune pause >0.7s.
La compression modifie la durée de l'audio. Ensuite, la transcription ASR, le timing, le SRT, l'ASS, la durée des scènes et le total-ms du rendu doivent tous être recalculés à partir de l'audio compressé (règle 25).
--reuse-asrest à utiliser uniquement pour ajuster les sauts de ligne des sous-titres. Dès que l'audio change, ne réutilisez jamais l'ancien dump.
③ Timeline : Utilisez les horodatages de transcription de la voix off elle-même, ne devinez pas à partir de la détection de silence
<ENV_PY> scripts/asr_align.py 02-Subtitles-Voice/xxx-voice.mp3 06-Documents/texte_original.txt \
--model small \
--out-timing 03-Annotations/xxx.annotation.timing.json \
--out-srt 02-Subtitles-Voice/xxx.srt --max-chars 18 \
--dump-asr 06-Documents/_asr.json
Ce que fait asr_align.py : ffmpeg conversion en mono 16k → faster-whisper (word_timestamps=True)
→ construction d'une timeline caractère par caractère → localisation de chaque phrase du texte original dans la transcription complète (recherche exacte → ancrage sur les 8 premiers caractères → repli)
→ sortie de timing.json (compatible avec segments[i/start/end/text]) et SRT sur une seule ligne.
Pourquoi ne pas utiliser silencedetect : le silence indique seulement « il y a une pause ici ». La respiration et l'emphase créent aussi des pauses. Si le nombre de phrases et le nombre de pauses ne correspondent pas, vous ne pouvez que les répartir uniformément par nombre de caractères, et les sous-titres seront forcément décalés.
asr_align.py fait également par défaut une adsorption de la limite de phrase (snap_to_voice) : en présence d'un long silence, faster-whisper a tendance à marquer le start de la phrase suivante au début du silence, ce qui fait apparaître le sous-titre trop tôt (le spectateur attend le son). Le script trouve le vrai point de départ de la vocalisation sur une grille de 20 ms et corrige. --no-snap permet de désactiver.
④ Vérifiez obligatoirement le contenu audio (le seul critère objectif pour juger de la « cohérence ») :
<ENV_PY> -c "..." # difflib compare le texte original vs le full de _asr.json
La similarité doit être >0.8. Pour les noms propres chinois, le modèle small ne fait que des erreurs d'homophonie (弘/红, 尊/遵, 武/吴, 锁/索). Si la structure (ordre des phrases, longueur des phrases) est cohérente, c'est que la lecture est correcte. Si des phrases entières sont manquantes, répétées ou sautées → l'audio lui-même est défectueux, régénérez-le.
⑤ Trois règles pour les sauts de ligne des sous-titres (toutes dans asr_align.py::split_subs) :
- D'abord, découpez par signes de ponctuation en petites phrases. Ne fusionnez la petite phrase suivante que si le bloc actuel < limit×0.55. Sinon, vous aurez des collages de phrases comme « a entrepris le célèbre voyage de la Longue Marche cette année-là ».
- Après fusion, ne dépassez pas la limite (ne laissez pas de tolérance de +3. Avec 20 caractères répartis uniformément, vous pourriez obtenir des fins de ligne comme « … doit seulement parcourir quel »).
- Ce n'est qu'en cas de bloc unique encore trop long que vous faites une division uniforme. Si la queue fait ≤4 caractères, fusionnez-la avec la phrase précédente.
⑥ Espace blanc de fin (application de la règle 18) : Ajoutez "tail": 0.30 à la dernière station. Les 30% finaux de ce segment n'auront aucun élément, et le moteur de rendu utilisera ce temps pour le zoom arrière et l'image figée. Test : laisser 4 secondes sur une vidéo de 99 secondes suffit pour voir l'image entière.
⑦ Répartition de la durée par plan : Un élément correspond à une phrase (ou à une action dans une phrase). durationMs reprend directement la durée de cette phrase. Au sein du segment, utilisez le ratio pour diviser en « dessiner l'objet / écrire ». Voir scripts/make_annotation_v3.py.
Étape 5 · Rendu, montage et validation
<ENV_PY> srt-whiteboard-animation/scripts/render_stream_whiteboard.py \
03-Annotations/image_principale.png 03-Annotations/image_principale.annotation.json 04-Rendu/xxx-silent.mp4 \
01-Resources/hand-55pct.png \
--cap-long-edge 1280 --fps 24 --total-ms <durée de la voix en ms>
--cap-long-edgeégal au côté long de l'image source pour ne plus redimensionner. Ajustez à la baisse pour un aperçu plus rapide.--total-ms: durée de la voix off (en ms) pour garantir que l'image ne se termine pas avant la voix.- La dernière ligne de sortie donne
OUTPUT=<chemin>.
Montage de la piste audio :
ffmpeg -y -i xxx-silent.mp4 -i voix_off.mp3 -c:v copy -c:a aac -b:a 192k -shortest video_finale.mp4
Trois points à vérifier absolument (prenez une image de chacun) :
- Début : fond beige clair et propre, pas de lignes visibles en avance.
- Milieu : traits continus, couleurs correctes (rouge/orange/bleu présents).
- Fin : image entière complète, sans rupture de trait, sans grande zone blanche.
Si l'écart de durée >1s, ajustez --total-ms ou la durationMs de chaque scène et refaites le rendu.
Étape 5.5 · Caméra et écriture (cœur de la v3, détermine « si ça a l'air d'être dessiné »)
Le moteur de rendu amont dessine sur un canevas fixe du début à la fin. Pour que l'image ressemble à un langage cinématographique, ajoutez une sous-classe render_camera_whiteboard.py, sans modifier le code du dépôt (le script scripts/render_camera_whiteboard.py est déjà réutilisable) :
<ENV_PY> scripts/render_camera_whiteboard.py 03-Annotations/image_principale.png 03-Annotations/image_principale.annotation.json \
04-Rendu/xxx-silent.mp4 01-Resources/hand.png \
--canvas-long-edge 2560 --frame 1280x720 --fps 24 --grid-edge 20 \
--zoom-fill 0.86 --zoom-max 7 --hand-height 350 --total-ms 56064
Il fait six choses :
| Mécanisme | Implémentation | Pourquoi |
|---|---|---|
| Cadrage de la caméra | Canevas rendu en 2560×1440. Chaque image est rognée au viewport et redimensionnée en 1280×720 | Les éléments peuvent « remplir l'écran », puis être balayés |
| Cadrage par boîte englobante d'encre | Cadre cible = bbox d'encre réelle dans le masque allowed de la scène, pas la région | La région est souvent plus grande que le graphique, voire chevauche les voisins. L'utiliser pour le cadrage donne une image vide au centre et le graphique poussé dans un coin (c'est sur ce point que la v1 de la v3 a échoué) |
| Suivi de la caméra | Lissage exponentiel entre scènes adjacentes : tau 0.20/0.30 pour les scènes d'écriture, pad 0.34, pad 0.05 pour les objets | Produit naturellement un effet de panoramique, pas besoin d'images de transition |
| Tampon de changement de scène | Début de chaque scène : rotation à vide de 0.22s (environ 5 images) avant de déposer l'encre | La caméra met 0.3–0.6s pour effectuer le panoramique depuis la scène précédente. Déposer l'encre pendant ce temps signifie « dessiner hors champ », qui est la principale cause de « la main bouge mais rien n'apparaît » |
| Image entière à la fin | Après la dernière scène, la cible est définie sur l'ensemble du canevas. Des images supplémentaires sont ajoutées jusqu'à --total-ms | Pour montrer « l'image entière à la fin » |
| Taille fixe de la main à l'écran | La main n'est pas dessinée dans le canevas, mais superposée sur l'image de sortie (--hand-height 350) | Sinon, en zoomant, la main remplit tout l'écran. 430 était encore trop grand, masquant l'écriture en cours |
--zoom-fill doit être défini en fonction de la densité du fond de l'image (un seul paramètre ne peut pas tout gérer) :
| Type de fond | zoom-fill | Raison |
|---|---|---|
| Clair (carte/flowchart, grands espaces blancs entre les éléments) | 0.86 | Vous pouvez vous rapprocher en toute confiance, l'image n'est pas vide |
| Dense (chronologie horizontale, éléments adjacents) | 0.70 | Se rapprocher trop coupe les voisins en deux, donnant un aspect brouillon |
Contrainte stricte du 16:9 : Les éléments longs et verticaux (damier, téléphone) sont limités par la hauteur de l'écran. Ils ne peuvent remplir l'écran qu'à 60-70% en hauteur et environ 25% en largeur. C'est une contrainte géométrique, pas un bug. Pour un meilleur remplissage, dessinez l'élément plus large ou passez en mode portrait.
Écriture (pour écrire les noms de lieux/années) :
# 1) Incorporez d'abord l'étiquette dans le fond de l'image (les coordonnées sont celles de l'annotation en 1280×720, le script dispose automatiquement les deux lignes en fonction de la taille de la police)
<ENV_PY> scripts/add_handwriting.py fond2x.png 03-Annotations/image_principale-v3.png 02-Subtitles-Voice/etiquettes.json
# 2) Dans l'annotation de cette scène, écrivez "inkPath": "textwrite", la région est le cadre de texte
textwrite avance colonne par colonne de droite à gauche, la pointe du stylo est en tête de la zone révélée, garantissant que le texte est toujours écrit (le suivi du squelette laisse échapper les traits fins, ne l'utilisez pas). Les 30% des premières images maintiennent le stylo en attente pour que la caméra soit en place, puis commencent à écrire – sinon, l'écriture se ferait hors du cadre.
Mise en page des étiquettes : nom principal 34-40px en police cursive (C:/Windows/Fonts/simkai.ttf) + année en 0.6 fois la taille en dessous,
couleur (58,56,52), dans la même gamme de couleurs que le dessin au trait mais légèrement plus claire.
Division des scènes : Une étape = deux scènes. D'abord, dessinez l'objet (0.6 de la durée), puis immédiatement après, écrivez (0.4 de la durée). Le cadre de la scène d'écriture est son propre cadre de texte. Ainsi, la caméra effectuera un léger zoom avant, comme si le crayon « se déplaçait pour écrire à côté ».
Étape 6 · Incrustation des sous-titres (SRT → ASS → incrustés dans l'image)
La timeline des sous-titres ne doit pas être marquée séparément. Utilisez directement .timing.json produit à l'étape 4 – il est déjà aligné phrase par phrase avec le commentaire. Le moment où le sous-titre change est aussi le moment où le crayon s'arrête. C'est ça, « l'alignement du rythme ».
# 1) timing.json -> SRT (**sur une seule ligne**, les lignes trop longues sont automatiquement divisées en plusieurs sous-titres consécutifs)
<ENV_PY> scripts/make_srt.py 03-Annotations/image_principale.annotation.timing.json 02-Subtitles-Voice/x.srt \
--max-chars 14
# 2) SRT -> ASS (PlayRes explicite + ancre du bas avec \pos, la taille de police/la position sont en pixels réels)
<ENV_PY> scripts/srt2ass.py
[le reste du contenu a été tronqué pour la traduction]
Leçons apprises (bugs réels du code amont srt-whiteboard-animation, vérifiés et corrigés)
_lay_ink() takes 6 positional arguments but 7 were given: unNoneen trop. Corrigez le code source.- Le moteur de rendu amont n'a pas de mécanisme de caméra, il ne peut que dessiner sur un canevas fixe. Utilisez
render_camera_whiteboard.py. contour-wipene suit pas la pointe : la pointe se déplace horizontalement de manière aléatoire pendant que l'effaceur avance, créant un décalage. Utilisezsnake_wipe.set_targetsans le& allowedamène le cadre de la caméra à la taille de la région entière, ce qui donne une image vide. Le correctif a été intégré au script, il suffit de mettre à jour la référence.- Les sous-titres
force_stylene sont pas fiables : le PlayRes n'est pas défini, ce qui rend le positionnement imprévisible. Utilisezsrt2ass.py.
Résumé du workflow
- Entrée : Fichier SRT (sous-titres/script).
- Étape 1 : Rédigez le commentaire et découpez le storyboard. Confirmez avec l'utilisateur.
- Étape 2 : Générez les images par IA selon le paradigme visuel spécifié. Nettoyez et mettez au format 16:9.
- Étape 2.5 : Créez la planche narrative spatiale pour définir la ligne directrice et la disposition.
- Étape 2.6 : Choisissez le paradigme de fond (itinéraire ou chronologie) et réglez les paramètres (
--zoom-fill, etc.). - Étape 3 : Annotez au niveau du pixel. Générez une image de validation et vérifiez visuellement.
- Étape 4 : Générez la voix off en une seule fois, faites un contrôle médical du silence et compressez si nécessaire. Faites l'alignement ASR pour obtenir la timeline. Vérifiez la similarité du texte.
- Étape 5 : Faites le rendu et le montage audio. Validez avec trois captures.
- Étape 5.5 : (Optionnel mais recommandé) Utilisez la caméra pour un effet de suivi et d'écriture.
- Étape 6 : Incrustez les sous-titres (SRT → ASS).
- Sortie : Vidéo finale MP4 + captures d'écran clés + enregistrement d'écran.