Courts-métrages PS1 low-poly
Tout est dans le code : Claude écrit un script Blender par court-métrage, le rend sur la machine de l'utilisateur, regarde les images, les corrige, puis construit le son et les sous-titres. Aucun générateur vidéo.
Testé sur examples/lift-rocket/ (« Et si les ascenseurs étaient aussi rapides que des fusées ? », 34 s).
Lisez cet exemple avant d'écrire un nouveau court-métrage : c'est le modèle.
Prérequis (vérifier d'abord, installer ce qui manque)
- Blender 4.2+ (testé sur 5.3). Trouvez-le :
blenderdans le PATH, sinonC:\Program Files\Blender Foundation\<ver>\blender.exe//Applications/Blender.app/Contents/MacOS/Blender. - Python 3.10+ avec
numpy pillow soundfile faster-whisper;ffmpegdans le PATH. - Personnages : Quaternius « Ultimate Modular Men » (CC0, gratuit, quaternius.com). Décompressez dans
assets/quaternius-modular-men/ou définissezLOWPOLY_PACKvers son dossierIndividual Characters/Blends. - Une voix TTS (ElevenLabs ou tout outil que l'utilisateur possède). Optionnel : le MCP
earshotpour vérifier le son.
Le pipeline (un dossier par court-métrage, copiez examples/lift-rocket/ comme point de départ)
| # | Étape | Fichier / commande | Terminé quand |
|---|---|---|---|
| 1 | Script | script.md | Question d'accroche + chaîne de conséquences, chacune pire et plus drôle + chiffres réels + chute sèche. 60-110 mots pour 30-40 s. Chaque chiffre vérifié avec une source et écrit dans la liste Faits. |
| 2 | Voix | vo/takeA.mp3 | 2 prises, choisir celle avec des timings de mots propres. Voix de créateur énergique, pas calme (calme = retour « ennuyeux »). |
| 3 | Timings des mots | python ../../scripts/words.py vo/takeA.mp3 | takeA_words.json n'a aucun mot de longueur nulle. |
| 4 | Liste des plans | tableau dans script.md | Un plan par phrase VO, une nouvelle image toutes les 1 à 3 s, chaque plan lié à un temps de mot. |
| 5 | Scène | build.py (Blender) | blender -b -P build.py -- frames --still 30,120,... rend des images ; regardez chaque image avant le rendu complet. |
| 6 | Aperçu + notes | python ../../scripts/preview.py . render puis serve | L'utilisateur n'a pas de notes ouvertes (voir boucle d'aperçu). |
| 7 | Rendu | blender -b -P build.py -- frames | PNG 360×640 EEVEE, f_0001.png... |
| 8 | Son | python build_mix.py | Effets sonores placés sur les images, VO par-dessus, nappe lo-fi en dessous, ducking. Écrit mix_raw.wav + cues.json. |
| 9 | Post | python post.py | Tramage PS1, effets, sous-titres avec surlignage du mot, callouts statistiques -> out/ en 1080×1920. |
| 10 | Multiplexage | voir ci-dessous | short.mp4, puis mesurer et vérifier la synchronisation. |
Règle d'image partout : 24 ips, image 1 = t 0, F = lambda t: int(round(t * 24)) + 1.
Multiplexage :
ffmpeg -y -framerate 24 -i out/f_%04d.png -i mix_raw.wav -c:v libx264 -crf 16 -pix_fmt yuv420p \
-af "loudnorm=I=-15:TP=-1.5:LRA=11,aresample=48000" -c:a aac -b:a 192k -shortest short.mp4
Échanger la voix plus tard sans recouper l'image :
python ../../scripts/words.py vo/takeB.mp3 puis python ../../scripts/align.py vo/takeA_words.json vo/takeB.mp3 --end 34 (--end = là où la nappe VO s'arrête ; la sortie dure end + 2 s. Sans elle : dernier mot + 1,5 s, ce qui donne un fichier plus court que l'image)
-> takeB_aligned.wav + takeB_aligned_words.json. Pointez build_mix.py (variable d'environnement VO) et post.py vers eux.
Boucle d'aperçu (avant le rendu complet)
Aperçu rapide basse résolution dans le navigateur, l'utilisateur laisse des notes sur la timeline, Claude corrige et re-rend uniquement les plans dont le contenu a changé. Plans = les marqueurs de caméra dans build.py.
| Faire | Commande (depuis le dossier du court-métrage) |
|---|---|
| Rendre l'aperçu | python ../../scripts/preview.py . render (première exécution : tous les plans ; ensuite : uniquement les plans modifiés) |
| Ouvrir la page de notes | python ../../scripts/preview.py . serve -> http://127.0.0.1:8765 |
| Lire les notes | python ../../scripts/preview.py . notes (groupées par plan, avec les numéros d'images) |
| Après correction | éditer build.py, relancer render, puis python ../../scripts/preview.py . done 3 5 --reply "ce que vous avez changé" |
| Forcer des plans | render --shots c_press,c_slam ou render --all |
build.pydoit accepter--blend-only(construire la scène, sauvegarder, ne pas rendre). Voir l'exemple.- Détection de changement : chaque image d'aperçu reçoit une empreinte (caméra active, transformation de chaque objet visible, pose d'armature, échanges de visages, matériaux, lumières, données de maillage). Un plan se re-rend uniquement si l'une de ses images a changé. Les plans inchangés conservent leurs anciennes images.
- Paramètres dans
preview.json(optionnel) :scale50 (%),step2 (toutes les 2 images),samples1,audio(par défautmix_raw.wav, sinon la VO),final= les commandes du pipeline complet. Le bouton « Approuver & rendre final » de la page les exécute ;notesaffiche APPROUVÉ lorsque l'utilisateur l'a cliqué. - Corrigez les notes une par une, répondez à chacune, re-rendez, et dites à l'utilisateur de ne rien rafraîchir : la page recharge la vidéo elle-même. Les notes répétées du même type deviennent une règle dans ce fichier.
- Mesuré sur lift-rocket (RTX 3070 Ti portable, 820 images) : premier aperçu 76 s, un plan modifié 13 s, rien changé 12 s. La majeure partie du temps de rendu d'une image est une surcharge fixe, donc une résolution plus basse aide moins que de rendre moins d'images.
API du kit (scripts/lowpoly_kit.py, importer à l'intérieur de Blender)
load_character(scn, "Suit")->(armature, meshes, actions). Personnages : Adventurer, Beach, Casual_2, Casual_Hoodie, Farmer, King, Punk, Spacesuit, Suit, Swat, Worker.- Actions (sur place, 24) : Death, HitRecieve, HitRecieve_2, Idle, Idle_Neutral, Interact, Kick_Left/Right, Punch_Left/Right, Roll, Run, Run_Back/Left/Right, Walk, Wave, Gun_Shoot, Idle_Gun*, Idle_Sword, Sword_Slash, Run_Shoot.
add_faces(scn, arm, head, tag, kinds, spec, drop)avant de déplacer l'armature. Types : neutral, blink, side, shock, grimace, squint, dazed, xeyes.dropsupprime les yeux/sourcils 3D du pack.show_faces(faces, [(image, type), ...]): coupures franches, chaque visage tient jusqu'au suivant.nla(arm, [(action, début, fin, blend[, influence]), ...]): les entrées suivantes sur des pistes plus hautes.drive_root(arm, [(image, x, y, lacet, ease), ...], last): lacet 0 fait face à -Y (la caméra par défaut).new_cam,aim,yaw_to,ps1_render_settings(scn, outdir).- Décors :
scripts/set_lift_lobby.py(exécutez avecBUILD_ONLY=True, il retournebox,uv_world,mat). Les nouveaux décors suivent le même modèle : boîtes avec textures pixel (interpolation = "Closest").
Règles de rendu
- Rendu 360×640,
filter_size 0, AgX Punchy, ombrage plat ; post : couleurs 15 bits + tramage Bayer, x3 nearest. - Les visages sont des décalcomanies plates 32×28 pixels échangées par coupures franches ; n'animez jamais un visage en douceur.
- Écrasement sous force en animant l'échelle de l'armature (z vers le bas, xy vers le haut). Temps comiques : rapide à l'entrée (2-3 images), maintien.
- Sous-titres : 1-3 mots, sans-serif gras, blanc avec contour noir, mot actuel en jaune, tiers inférieur (y 1300 de 1920).
- Callouts statistiques : boîte arrondie rouge, tiers supérieur, apparition en 4 images, un par nombre réel.
Règles sonores
- Chaque effet sonore est placé de sorte que son pic tombe sur l'image (
at(name, frame, db, peak=...)). - La voix doit gagner : VO normalisée à 0 dBFS, bus SFX -11 dB, musique -7 dB, plus ducking sidechain pendant que la voix parle (SFX -5 dB, musique -9 dB). La v1 du court-métrage ascenseur avait SFX à -3 et musique à 0 : « Je n'entends pas la voix off ».
- Coupez la musique net sur le temps d'arrêt important, ramenez-la après. Le silence vend la blague.
- Master : loudnorm -15 LUFS, crête vraie -1,5 dBTP.
- Vérifiez : earshot
timelinesur le mp4 vscues.json(objectif : chaque cue à moins de 100 ms),measurepour LUFS.
Bugs déjà rencontrés (ne pas répéter)
- Les maillages du pack sont livrés cachés au rendu :
hide_render = False(le kit le fait). - Les maillages de porte sont déjà à leur emplacement fermé : n'animez que le décalage, sinon ils se ferment avec un écart de 0,7 m.
- Un plafond plus bas que ~2,75 m cache les affichages muraux de la caméra à l'intérieur d'une cabine d'ascenseur.
- Un personnage qui ne doit pas encore être vu : mettez en clé
location.z = -50jusqu'à son plan. - NLA un plan unique passant à une tenue : réglez
blend_out = 0du clip ou la pose saute pendant une image. - Les plans inversés deviennent noirs (plafond non éclairé) : ajoutez une lumière avant et une petite émission au plafond. Rendez l'angle inversé tôt.
- faster-whisper sur mp3 peut planter avec une incompatibilité PyAV :
words.pydécode d'abord avec ffmpeg. - Une phrase commençant à -0,04 s donne une tranche vide dans l'alignement : serrez avec
max(0.0, ...)(fait).
Coût (court-métrage ascenseur, RTX 3070 Ti portable)
Rendu 820 images en 92 s, post 17 s, 0 €. Personnages, SFX, musique : 0 € (CC0 + synthétisé dans le code). Voix : ElevenLabs environ 0,10 € par prise, 2 prises par court-métrage.
Avant de remettre le travail
- Regardez un plan contact des images finales (toutes les ~2 s), pas seulement les images fixes.
- Mesurez le volume, vérifiez la synchro, vérifiez les sous-titres par rapport au texte VO (les corrections orthographiques vont dans
post.pyFIX). - Dites à l'utilisateur ce qui a été mesuré et ce qui n'a pas été entendu par un humain.