---
name: dubup-video
description: Écrit le prompt multi-plans Seedance 2.5 d'une scène Dub'Up, génère avec voix anglaises pour tenir la synchro labiale, retire les voix du clip servi et rédige le brouillon de SceneManifest. Use when on génère la vidéo d'une scène, on découpe une scène en plans et en segments, ou on parle de Seedance sur ce projet.
---

# dubup-video

Transforme les visuels produits par `dubup-image` en un clip de 30 s jouable, et en le
`SceneManifest` qui va avec.

## La règle ultime : un comportement humain

Avant toute règle technique, celle-ci, posée par Benjamin le 2026-09-11 après la prise 1 de
« Purée de famille » : **une scène Dub'Up doit ressembler à une scène de film réaliste, jouée par
des gens, pas par des acteurs de théâtre.** Pas de surjeu, pas de grimace, pas de geste plus
grand que nature, pas de rouge qui tombe d'un coup. Une scène techniquement parfaite mais
surjouée est une scène ratée — on la regénère.

Ce que ça impose, en un mot : **écrire un cran en dessous** de ce qu'on veut voir, parce que le
modèle amplifie tout. Le détail est dans les trois sections de l'étape 2 — émotions en gestes
progressifs, jamais de direction sur une partie du visage, registre de jeu retenu — et elles
priment sur l'envie de faire « plus fort » quand une prise paraît molle.

## La règle qui prime sur la durée : la qualité

Posée par Benjamin le 2026-09-19, en majuscules, après une prise où les personnages parlaient
en accéléré : **on privilégiera toujours la qualité de la vidéo à sa durée.** Trente secondes
par génération, soixante au schéma : ce sont des plafonds, jamais des cibles. Quand une durée
entre en conflit avec une cadence naturelle, un silence nécessaire ou un geste crédible, c'est
la durée qui cède — moins de répliques, des répliques plus courtes, une génération de plus. Une
prise « presque bonne » qui parle trop vite ou tasse une réaction se regénère.

## La règle d'ouverture : à 0:02, il s'est déjà passé quelque chose

Posée par Benjamin le 2026-09-17, sur les courbes de rétention TikTok de « Affaire de famille »
et « Le génie qui peut pas » : **la plupart des spectateurs partent à 0:02**, la courbe passe
sous 50 % avant la troisième seconde, et les deux courbes sont identiques alors que les scènes
n'ont rien en commun. Ce qu'elles partagent, c'est ce qu'on voit à 0:02 : **rien**. Quatre
personnes qui mangent, le couteau qui touche le verre à 1,3 s. Un homme qui s'ennuie derrière un
comptoir, et le fait qu'il soit au milieu d'un désert n'est lisible qu'au plan 2. Trois
secondes d'un mari qui lit. L'ouverture « installation muette » — poser le lieu, laisser les
gens vivre, puis lancer — est exactement ce que le flux vertical ne pardonne pas : le spectateur
ne connaît ni le jeu ni la scène, il a deux secondes d'image pour décider, et il décide.

Le bumper de marque est en **fin** de vidéo (`docs/06`), pas en tête ; il n'est pour rien dans
cette chute. C'est le plan 1 de la scène qui la fait, et lui seul peut la corriger.

**Le plan 1 commence sur ce qui cloche, pas sur la situation.** Ce qui rend la scène digne
d'être regardée — l'objet incongru, le geste qui dérape, la phrase qui fait basculer — est à
l'image dès la première seconde, en cours, pas en préparation. La situation (qui est où, ce
qu'on mange, l'heure qu'il est) se lit ensuite, en arrière-plan des plans suivants ; la fiche de
lieu fixe la géométrie, on n'a pas besoin d'un plan pour la montrer.

Ce que ça impose, plan par plan :

- **Pas de plan d'installation en tête.** Si un plan large est nécessaire pour poser la
  géométrie, il vient en 2 ou 3 ; ou il reste en 1, mais déjà chargé : le geste d'accroche est
  en cours à l'image 1. On n'attend pas 1,3 s que le fils prenne son couteau — le plan s'ouvre
  sur le couteau qui touche le verre.
- **Avant 2 s, une réplique a attaqué ou un bruit franc a eu lieu.** Le bruit d'ouverture est
  une accroche à part entière — trois tings sur un verre, une porte, une main à plat sur un
  comptoir — et il tombe dans un silence, donc il respecte la contrainte critique ci-dessous.
  Une réplique qui attaque à 0,8 s est légale : rien n'impose d'amorce muette, seule la queue
  finale est contrainte (`SCENE_MIN_TAIL_MS`). Pas plus tôt : voir « aucune parole doublée avant
  0,8 s » à l'étape 1.
- **L'anomalie visuelle est cadrée, pas suggérée.** Un stand forain au milieu d'un désert est
  une accroche si on voit le désert autour du stand ; en plan moyen sur le comptoir, c'est un
  homme derrière un comptoir. Le premier plan de « Le génie » aurait dû être le large avec
  l'enseigne, le voyageur entrant déjà dans le champ.
- **L'accroche n'est pas un coup de force.** La règle ultime tient : pas de cri, pas de geste
  gonflé pour réveiller le spectateur. Ce qui accroche, c'est l'incongru et le basculement, dit
  à voix ordinaire — « I signed my sister up for 'Roommate From Hell' » à 0,8 s fait plus
  qu'un verre brisé.
- **Les secondes gagnées vont à la fin, pas au vide.** Retirer 2 à 4 s d'installation, c'est
  autant de place pour la réaction finale et la queue muette — là où le son et le regard se
  posent.

Le test, avant de générer et encore après : **arrêt sur image à 1 s**. Quelqu'un qui ne connaît
ni le jeu ni la scène doit pouvoir dire ce qui est bizarre, ou ce qui vient de se passer. S'il
ne voit qu'un lieu et des gens qui y vivent, réécrire le plan 1.

## La règle qui commande tout : on génère avec les voix, en anglais, et on les retire ensuite

**`generate_audio: true`, dialogues écrits en anglais entre guillemets doubles.**

C'est contre-intuitif pour un jeu de doublage, et c'est pourtant la seule voie sûre. Seedance
co-génère l'audio et l'image dans **un seul espace latent** : la synchronisation labiale n'est
pas une fonction à part, c'est un sous-produit de la génération audio. Couper l'audio, c'est
risquer de couper le mouvement des lèvres — et une scène dont les bouches ne bougent pas est
injouable.

Une génération donne donc **deux fichiers**, et c'est exactement la convention que le repo
utilise déjà pour `sc_the_box` :

```
storage/prep/<slug>-vo.mp4     la génération brute, voix anglaises comprises.
                               Sert à MESURER. N'est jamais servie à un joueur.
storage/scenes/<slug>.mp4      le clip jouable, SANS les voix.
```

C'est mot pour mot ce que dit `packages/shared/src/the-box-envelopes.ts` : les enveloppes sont
« mesurées sur la voix isolée (`storage/prep/the-box-vo.mp4`), pas sur la scène servie aux
joueurs : celle-ci ne contient que la musique ».

### Ce que les voix anglaises font gagner

Elles ne servent pas au jeu — elles servent à **mesurer**, et elles rendent possibles trois
choses que je croyais perdues sur une scène générée :

| | Sans voix générées | Avec |
|---|---|---|
| `speechStartMs` / `speechEndMs` | écrits à la main, approximatifs | **mesurés** par détection de parole, comme sur une scène importée |
| `envelope` | impossible, laissé à `null` | **mesurable** sur la voix isolée |
| Découpage | purement autorial | écrit, puis **corrigé sur la mesure** |

L'anglais n'est pas un détail : c'est ce qui rend la voix d'origine inutilisable comme réponse
et garde le jeu ouvert. Le joueur français ne recopie pas, il réinvente.

### Ce que ça coûte : les voix doivent disparaître du clip jouable

Si les voix anglaises restent sous le doublage, le joueur s'entend doubler par-dessus quelqu'un
qui parle déjà. C'est précisément ce que `the-box` évite en ne servant que la musique.

Le nettoyage se fait en post-production, par séparation de sources (voir « Étape 5 »). Ce n'est
pas un problème en soi — mais il impose la contrainte ci-dessous, qui est la plus dure du skill.

### La contrainte critique : silence absolu sous les répliques

**Aucun évènement sonore ne doit tomber pendant un intervalle de parole.** C'est la règle la
plus dure de ce skill, et la raison n'est pas celle qu'on croit.

**Un séparateur de sources détruit ce qui partage les instants de la voix.** Retirer le stem de
voix, ce n'est pas découper au scalpel : là où un bruit et une réplique se superposent, l'algo
tranche mal — le bruit ressort mangé, étalé, ou emporté avec la voix. Et c'est **irréparable** :
on ne réinjecte pas ce qui a été mangé, et reséparer ne fera pas mieux.

Autrement dit : **les instants de doublage sont exactement les instants où le fond sonore se
dégrade.** Un fond propre après nettoyage n'est possible que si, sous chaque réplique, il n'y
avait rien d'autre qu'une nappe continue.

Une raison s'ajoute, moins grave : un bruit sous la réplique salit le doublage du joueur, qu'il
passe le nettoyage ou non.

**Et le corollaire visuel, qu'on oublie systématiquement :** aucun **geste bruyant à l'image**
sous une réplique — poser un objet, claquer une porte, faire tomber quelque chose. Un geste
sonore visible sans son est une incohérence que le joueur remarque ; et si on le sonorise, on
retombe dans le cas ci-dessus. Ces gestes vont **dans les silences entre segments**, comme les
sons eux-mêmes.

### Le renversement : hors des répliques, on veut du bruitage

Le rôle de bruiteur a disparu de Dub'Up. Personne ne fabriquera plus les sons de la scène — donc
**le bruitage généré par Seedance est désormais tout le design sonore du jeu**. Il ne faut plus
l'éviter, il faut le chercher : porte, pas, verre posé, chaise raclée, tout ce qui donne corps à
la scène.

La règle n'est donc pas « peu de bruitage », c'est **« beaucoup de bruitage, jamais sous une
réplique »**. Les silences entre segments passent du statut de place laissée à un joueur à celui
de seule fenêtre où le son de la scène peut exister — et survivre au nettoyage des voix.

## Étape 1 — Écrire le découpage avant le prompt

Le découpage écrit ici sert à **écrire le prompt**, pas à remplir le manifeste : les bornes
définitives seront mesurées à l'étape 4, sur les voix anglaises générées. Le prompt découle du
découpage, jamais l'inverse — mais le manifeste, lui, découle de la mesure.

Poser un tableau, en secondes, avant toute rédaction :

| Plan | de → à | Ce qu'on voit | Qui parle | Silence exploitable |
|---|---|---|---|---|
| 1 | 0 → 4 | plan large, le couteau tinte sur le verre à 0,2 s, le toast qui dérape | tom (dès 0,8 s) | 0,8 s en tête |
| 2 | 4 → 8 | gros plan, elle repose sa fourchette | anna | 1,5 s en fin |

Vérifier sur ce tableau, **avant** de générer quoi que ce soit :

- **à 0:02, il s'est passé quelque chose** : une réplique a attaqué, ou un bruit franc a eu
  lieu, ou l'anomalie de la scène est cadrée plein champ (règle d'ouverture). Un plan 1
  « installation », « ils mangent », « il lit », « elle attend » est à réécrire avant d'aller
  plus loin ;
- des silences assez longs pour porter le son de la scène — c'est là, et nulle part ailleurs,
  que le bruitage peut exister ;
- une **queue muette finale** : dernier segment refermé avant `durationMs`. Le minimum validé
  est `SCENE_MIN_TAIL_MS` = 400 ms, mais viser 1 à 3 s — c'est là que se posent le regard, la
  réaction et le dernier bruitage. (`sc_the_box` : 2,7 s.)
- aucun geste sonore sous une réplique ;
- **aucune parole doublée avant 0,8 s de scène** (Benjamin, 2026-09-30). Une réplique qui ouvre
  la scène n'a pas d'image avant elle pour décompter : l'app décompte sur l'image gelée, puis
  l'image part. L'iPhone n'ouvre son micro que 0,3 à 0,5 s après (la vidéo d'abord, toujours :
  un micro ouvert avant elle est coupé par la vidéo), Android dès l'image. Une parole à 0,8 s
  passe entière ; plus tôt, son début est perdu sur iPhone. Avant 0,8 s, l'accroche est un bruit
  franc ou l'anomalie cadrée ;
- chaque segment entre 700 ms et 20 s, et au plus 6 personnages.

## Étape 2 — Écrire le prompt multi-plans

Seedance enchaîne plusieurs plans dans **une seule** génération, en tenant les personnages. Ne
pas générer plan par plan : on perdrait la continuité qui est justement l'apport du modèle.

Structure :

```
[Profil visuel, repris mot pour mot de refs.json — c'est le lien avec les images-clés.]

Personnages : @Image 1 est <nom>, <marqueurs visuels>. @Image 2 est <nom>, …
Décor : @Image 3 est <lieu>, vue d'ensemble. @Image 4 est le même lieu en contrechamp.
  Toute la scène se joue dans ce décor, sans en changer la géométrie.

Plan 1 (0-4 s) — <valeur de plan, mouvement de caméra>. Dès l'image 1, <le geste ou
  l'objet qui accroche, déjà en cours>. <bruit franc> à 0,2 s. <nom> parle dès 0,8 s.
  Aucun geste bruyant pendant la réplique.
Plan 2 (4-8 s) — <valeur de plan>. <nom> parle. Aucun geste bruyant.
…
Plan N (27-30 s) — <valeur de plan>. Personne ne parle, personne ne bouge d'un geste
  sonore. Réaction tenue jusqu'au noir.
```

- Reprendre les `ref` de `refs.json` **littéralement** : `@Image 1`, `@Image 2`… — c'est la
  syntaxe de Higgsfield, avec l'arobase et l'espace. Leur numéro est leur rang de téléversement,
  donc l'ordre du fichier : personnages, puis lieux, puis plans.
- **Toujours passer les fiches de lieu en référence**, même quand les images-clés montrent déjà
  le décor. Une image-clé cadre un moment ; la fiche de lieu donne la géométrie de la pièce, et
  c'est elle qui empêche le décor de bouger au changement de plan. Si un contrechamp est prévu,
  passer aussi l'angle correspondant — sinon Seedance invente ce qu'il y a derrière la caméra.
- **Écrire les dialogues en anglais, entre guillemets doubles.** C'est ce qui produit le
  mouvement des lèvres, donc ce que les joueurs vont doubler. Les joueurs ne l'entendront jamais
  — la voix est retirée du clip servi — mais elle est mesurée, et elle décide de tout le
  découpage. Écrire de vraies répliques, jouées, pas des marmonnements.
- Nommer la valeur de plan et le mouvement de caméra à chaque plan. Sans ça, le modèle recadre
  au hasard et le découpage en segments ne correspond plus à l'image.
- **Écrire le plan 1 comme un plan déjà commencé.** Pas de *they are eating*, *he is reading*,
  *he waits* suivi d'un *at about 1.5 seconds…* : le geste d'accroche est le premier verbe du
  plan, et « at about » n'y apparaît pas. Si l'accroche est une réplique, elle est la première
  chose que le personnage fait.

### Une émotion s'écrit comme une suite de gestes, jamais comme un état

C'est la faute qui a abîmé `mauvaise-nouvelle` : le plan 3 disait *« his jaw tightens, his
eyes fill, his face reddens »*, et le patient est devenu rouge **d'un coup**, en une image. Un
état décrit au présent (`reddens`, `flushes`, `turns pale`, `tears streaming`, `furious`,
`devastated`) est appliqué par Seedance comme un interrupteur : la couleur ou l'expression
bascule, elle ne monte pas. Un visage réel ne fait jamais ça.

Ce qui tient : décrire ce que le corps **fait**, dans l'ordre où il le fait, avec le temps que
chaque chose prend.

| À proscrire | À faire |
|---|---|
| *his face reddens* | *his jaw sets; his breathing goes deep and slow; the veins in his neck stand out by the end of the line* |
| *his eyes fill, tears streaming* | *he blinks more often; his eyes go glassy; one tear finally spills over at the end of the line* |
| *she is furious* | *she stops chewing, sets her fork down, and her breathing gets deeper and slower* |
| *he breaks down* | *his mouth works before any sound comes; his breath catches twice; then the first sob* |

Règles :

- **Un seul changement à la fois**, chacun avec sa durée (`over two seconds`, `by the end of
  the line`, `slowly`). Trois états dans une même phrase donnent trois interrupteurs.
- **Toujours dire ce qui précède** — la retenue avant la crise. Le naturel vient du temps où le
  personnage *essaie de ne pas* : *he tries to keep his face still and cannot quite manage it*.
- **Aucune couleur de peau ni de teint** dans une direction de jeu. Le rouge, le pâle, le
  brillant sont des sous-produits que le modèle doit produire lui-même, en douceur, s'il y
  arrive — les commander, c'est obtenir l'aplat.
- **Ne jamais diriger les sourcils, ni le front, ni les yeux écarquillés.** Vu sur « Purée de
  famille » : *her eyebrows rise slowly through the line* a donné un front plissé de dessin animé
  et un visage en caoutchouc. Seedance surjoue toute instruction sur une partie du visage. Un
  visage qui encaisse s'écrit par l'**immobilité** (*she goes completely still*, *her face
  otherwise still, her voice doing the work*), et le bloc personnages porte un garde-fou global :
  *no exaggerated, theatrical or cartoonish expressions, no wide-eyed stares, no raised or
  knitted eyebrows*.
- **Pas d'adjectif d'émotion nu.** `angry`, `sad`, `terrified` ne disent rien au corps.
  Remplacer chaque adjectif par le geste qui le montre.
- Une expression qui doit **rester** (le père figé, boudin en l'air) se décrit comme un
  mouvement arrêté, pas comme un état : *he stops mid-throw and stays exactly like that*.
- Terminer les blocs de personnages par une consigne globale : *All expressions change gradually
  and continuously, never between one frame and the next.*

Le test avant d'envoyer : relire chaque plan et remplacer mentalement chaque verbe d'état par un
verbe d'action. S'il en reste un qu'on ne sait pas remplacer, le supprimer.

### Le registre : Seedance joue ce qu'on écrit, en plus grand

Deuxième leçon de « Purée de famille » : la prise 1 était **surjouée de bout en bout**, et ce
n'était pas une dérive du modèle — c'était le prompt. *hurls it hard*, *shouts, leaning over
the table*, *laughing, head tipped back*, *taunting*, *smashes… explodes* : chaque verbe fort
a été rendu comme au théâtre. Le modèle amplifie ; il faut donc écrire **un cran en dessous**
de ce qu'on veut voir.

- Un bloc **ACTING** en tête, après les personnages, qui fixe le registre pour toute la
  scène : *naturalistic, understated performance, as in a French realist family drama; the
  actors underplay; gestures small and economical; faces move little; the voice does most of
  the work at ordinary conversational volume; even the anger is contained.*
- **Pas de verbe de scène.** *shouts* → *speaks, loud, his voice tight and slightly unsteady* ;
  *smashes* → *sweeps off the table with the back of his hand* ; *laughing, head tipped back*
  → *a short laugh through his nose* ; *grabs with both hands* → *reaches with one hand*.
- **Une hésitation avant chaque geste fort** (*looks at it for a moment as if he cannot
  believe what he is about to do*). C'est l'hésitation qui fait l'humain, pas le geste.
- Les majuscules du script (« ON N'HUMILIE PAS… ») sont une intention d'auteur, pas une
  direction de jeu : on la traduit par la tension de la voix, jamais par *roar* ou *scream*.

**Le son obéit à la même règle.** Sur la même prise, *explodes*, *crashes*, *a clear ringing*,
*wet slap* ont donné du bruitage de cinéma — impacts gonflés, réverbération. Un bloc **SOUND**
après le bloc caméra fixe le registre (*real domestic sound, recorded in the room, at its
natural loudness: no cinematic sound design, no added impact, no boom, no reverb*), et chaque
bruit se décrit par ce qu'il est vraiment, avec sa durée : *one short dry crack and a few
pieces skittering, over in under a second* plutôt que *crash* ; *a dull, quiet thud* plutôt
que *slap* ; *three small tings* plutôt que *a clear ringing*.

### La cadence : deux mots par seconde, et la fenêtre écrite dans le plan

Vu sur « Une mission simple » v5 (2026-09-19) : 17 mots dans un plan de 4 s, 23 mots dans un plan
de 4 s — **Seedance tient la durée du plan et accélère la parole pour y faire entrer le texte.**
Les personnages parlaient en accéléré, et une réplique accélérée ne se double pas : les lèvres
vont plus vite que n'importe quelle voix.

Une voix naturelle, jouée, fait **2 à 2,5 mots par seconde** en anglais. Donc, avant d'écrire une
réplique, compter les mots que sa fenêtre peut porter :

| Fenêtre de parole | Mots au plus |
|---|---|
| 2 s | 5 |
| 3 s | 7 |
| 4 s | 9 |
| 5 s | 12 |

Et trois choses dans le prompt, jamais laissées implicites :

- **la fenêtre de parole de chaque réplique, en secondes, dans le plan** : *he speaks slowly,
  from about 8.5 to 11 seconds* — pas seulement la durée du plan ;
- **la cadence dans le bloc SOUND**, une fois pour toute la scène : *every line is spoken at a
  slow, natural, unhurried pace, about two words per second, with real breaths between
  sentences; the lines are short on purpose and must never be rushed or sped up to fit the
  shot — if a line runs long, the shot holds on the speaker until it is finished* ;
- **la dernière ligne du prompt** le redit : *spoken in English, slowly and naturally, never
  sped up*.

Allonger le temps de jeu d'un rôle, ce n'est donc pas allonger ses phrases : c'est lui donner
**plus de répliques**, ou des plans plus longs. Le bilan du catalogue du 2026-09-19 fixe ce
qu'un rôle doit avoir : au moins deux répliques et 6 s de parole, des répliques de 2 à 4,5 s,
au moins 40 % de la scène en parole, et aucun rôle à une seule réplique.

### La syntaxe des références dans Higgsfield : `@image_1`, pas « @Image 1 »

Constaté le 2026-09-19 : l'interface a sa propre syntaxe de mention, **`@image_1`, `@image_2`…**
(minuscules, tiret bas, sans espace). « @Image 2 » collé tel quel est transformé en « @image_1 2 »
— et toutes les références du prompt pointent sur la première image. Écrire `@image_N` dans les
prompts ; les `ref` de `refs.json` restent un rang de téléversement, c'est le numéro qui compte.

## Étape 3 — Générer

Le projet passe par **Higgsfield**, et c'est une étape **manuelle** : Seedance 2.5 n'y existe que
dans l'interface web.

> **Ne pas chercher à automatiser cette étape.** L'API Higgsfield (`docs.higgsfield.ai`) n'expose
> que Seedance **v1** — `/bytedance/seedance/v1/lite/…` et `/v1/pro/fast/…` — plafonné à 12 s,
> avec un seul `image_url`, sans `generate_audio` (ce champ n'existe que sur leurs endpoints
> Veo 3.1). Ni les 30 s, ni le multi-plans, ni les références multiples, ni l'audio natif. Un
> script qui viserait cette API produirait autre chose que ce que décrit ce skill.

Dans l'interface Higgsfield :

1. Téléverser les images de `output/scenes/<slug>/refs/`, **dans l'ordre de `refs.json`** —
   personnages, puis lieux, puis plans. C'est cet ordre qui donne leur numéro aux `@Image N`.
2. Coller le prompt de l'étape 2.
3. Régler : durée **30 s**, ratio **16:9**, résolution **480p** (Benjamin, 2026-10-04 : beaucoup
   moins cher que le 720p). Une prise 480p est une vraie prise, jugée comme définitive. Quand
   toute la scène est finie (générations collées), **un seul upscale** de la vidéo entière vers
   le profil du catalogue, **1280×720** ; mesures et manifeste se font sur ce fichier-là. Le
   support est le téléphone : **ne jamais monter en 1080p ni lancer l'upscale 4K**, c'est du
   travail et du poids perdus, le worker redescendra de toute façon.
4. Générer, puis télécharger le mp4.

L'audio est produit dans la même passe, automatiquement — il n'y a pas de bouton pour le couper,
et c'est ce qu'on veut : c'est lui qui tient la synchro labiale.

Ranger le fichier téléchargé dans `storage/prep/<slug>-vo.mp4`. **Ne jamais le servir à un
joueur** : il porte les voix anglaises.

Capacités du modèle dans l'interface : jusqu'à 30 s, ratios de 9:16 à 21:9, 480p / 720p / 1080p,
et jusqu'à 50 entrées multimodales (images et clips) pour verrouiller personnages, décors et
style sur toute la vidéo. On n'en utilise qu'une partie, volontairement.

> **Le format est 16:9, 1280×720, pour toutes les scènes générées.** C'est celui de `sc_the_box`,
> donc le catalogue reste homogène — condition technique du `-c copy` de `DUB_STORY` (`docs/06`),
> qui ne peut concaténer que des scènes au même profil d'encodage. Une scène verticale isolée
> casserait cet enchaînement.
>
> À traiter plus tard, côté app : `apps/mobile/app.json` verrouille l'orientation en `portrait`.
> Une vidéo 16:9 y joue dans une bande, comme `the-box` aujourd'hui.

> **Pas de `seed` exposée.** Conséquence directe : une regénération ne redonnera pas le même
> film, seulement une autre prise du même prompt. Regénérer coûte donc plus cher qu'avec une
> API — raison de plus pour valider le découpage de l'étape 1 **avant** de lancer.

## Étape 4 — Mesurer ce qui a été livré, avant d'écrire quoi que ce soit

Le prompt **demande** une coupe à 7 s. Il ne la garantit pas. Si le modèle coupe à 6,4 s et
qu'on écrit le manifeste depuis le tableau de l'étape 1, chaque segment est décalé de 600 ms —
et le joueur double une image qui n'est pas la sienne. Le manifeste suit **l'image livrée**,
jamais le plan d'intention.

Relever les coupes réelles :

```
ffmpeg -i seedance-out.mp4 -filter:v "select='gt(scene,0.4)',showinfo" -f null - 2>&1 \
  | grep -o 'pts_time:[0-9.]*'
```

Relever les attaques de parole — c'est ce que les voix anglaises rendent possible, et c'est la
même méthode que sur une scène importée :

```
ffmpeg -i storage/prep/<slug>-vo.mp4 -af silencedetect=n=-30dB:d=0.3 -f null - 2>&1 \
  | grep silence_
```

`speechStartMs` est la fin de chaque silence, `speechEndMs` le début du suivant. Ces valeurs-là
priment sur le tableau : c'est l'image et la voix livrées qui font foi.

Puis vérifier, en regardant le clip :

| À vérifier | Ce qu'on fait si c'est faux |
|---|---|
| **Arrêt sur image à 1 s : l'anomalie ou le basculement se voit** ; à 2 s une réplique a attaqué ou un bruit franc a eu lieu | réécrire le plan 1 et **regénérer** — c'est la seconde qui décide si la scène est regardée, on ne la rattrape pas au montage |
| Coupes conformes au tableau (± 200 ms) | réécrire les bornes du manifeste sur les valeurs mesurées |
| Première parole doublée à 0,8 s ou plus tard | plus tôt, le début de la réplique est perdu sur iPhone (étape 1) : le dire à Benjamin, qui décide de regénérer le plan 1 |
| Les lèvres bougent et articulent | **regénérer** — sans mouvement labial la scène est injouable |
| **Silence total sous chaque réplique**, hors nappe continue | **regénérer** — c'est le contrôle décisif : un bruit ici sera détruit par le nettoyage des voix, et le fond avec |
| Aucun geste sonore **visible** sous une réplique | regénérer ce plan, ne pas déplacer le segment pour compenser |
| Queue finale réellement immobile et silencieuse | rogner `durationMs` ou regénérer |
| Le décor n'a pas bougé entre deux plans | ajouter l'angle de lieu manquant en référence, regénérer |
| Les voix sont bien en anglais | regénérer : une VO française donnerait la réponse au joueur |

> **Pour le contrôle décisif, séparer d'abord.** Sur le mix brut, la voix masque exactement ce
> qu'on cherche à entendre — un bruitage sous une réplique est inaudible tant que la réplique est
> là. Faire tourner la séparation de l'étape 5 **avant** de valider, et écouter le stem sans voix
> aux instants de parole. C'est le seul endroit où le défaut s'entend, et c'est aussi celui où il
> se paie.

Regénérer avec **la même `seed`** et un prompt ajusté : on change ce qui cloche, pas la scène.

Ne jamais rattraper un défaut d'image en tordant le manifeste. Un segment mal placé se paie sur
chaque partie jouée, indéfiniment ; une regénération se paie une fois.

## Étape 5 — Normaliser au profil du catalogue

Le worker rend en `-c:v copy`, non négociable (`docs/06`). La sortie Seedance doit donc être
transcodée **une fois**, à l'import, au profil unique du catalogue — c'est aussi ce qui permet à
`DUB_STORY` de concaténer en `-c copy`.

**Retirer les voix d'abord.**

> **Seedance ne sort pas de stems.** `generate_audio` est un booléen, rien de plus : pas de
> pistes séparées dialogue / SFX / musique. C'est architectural — audio et image sont
> co-générés dans un seul espace latent, il n'existe donc à aucun moment du calcul de pistes
> distinctes à récupérer. La séparation est **forcément post-hoc**.
>
> Corollaire : générer deux fois avec la même `seed`, une fois `true` une fois `false`, ne
> donnera pas le même film. Le mode audio fait partie du latent, donc l'image change aussi. Ne
> pas tenter cette voie sans l'avoir vérifiée sur 5 s.
>
> Le seul levier natif est en **entrée** : jusqu'à 10 audios en référence, 30 s cumulées. Ça
> conditionne l'ambiance générée, ça ne la sépare pas.

Demucs est installé sur la machine, mais **hors du `PATH`** : `python` et `python3` n'y répondent
pas (l'alias `python3.exe` du Store est un leurre qui ouvre le Microsoft Store). L'appel est :

```
"$LOCALAPPDATA/Programs/Python/Python312/python.exe" -m demucs --two-stems=vocals -n htdemucs \
  --shifts 2 -o <dossier-sortie> <audio.wav>
```

Sortie : `<dossier-sortie>/htdemucs/<nom>/vocals.wav` et `no_vocals.wav`. Une scène de 30 s prend
une dizaine de secondes sur CPU. Extraire d'abord un WAV 48 kHz stéréo du `-vo.mp4`.

Passer l'audio de `<slug>-vo.mp4` dans un séparateur (Demucs ou équivalent), jeter le stem de
voix, garder le fond. On récupère l'ambiance que Seedance a posée sur les bonnes frames.

**Écouter le résultat aux instants des répliques, pas ailleurs.** C'est là que le séparateur
abîme le fond, et c'est le seul contrôle qui compte : si un bruitage y était, il ressortira
mangé ou étalé. Le défaut ne se répare pas — il se regénère (voir la contrainte critique
ci-dessus).

Repli, si la séparation laisse des résidus de voix audibles : `-an` et une nappe d'ambiance
fabriquée à part, déclarée en `ambienceUrl`. Plus propre, mais rien dans le projet ne produit de
nappe aujourd'hui.

Puis normaliser au profil unique du catalogue — le worker rend en `-c:v copy`, non négociable
(`docs/06`), et c'est aussi ce qui permet à `DUB_STORY` de concaténer en `-c copy` :

```
ffmpeg -y -i storage/prep/<slug>-vo.mp4 -i bed-sans-voix.wav \
  -map 0:v -map 1:a \
  -vf scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:-1:-1 \
  -r 30 -c:v libx264 -preset slow -crf 21 -pix_fmt yuv420p -g 30 -keyint_min 30 \
  -c:a aac -b:a 192k -ar 48000 \
  -movflags +faststart \
  storage/scenes/<slug>.mp4
```

En voie (b), remplacer `-i bed-sans-voix.wav -map 1:a` et l'encodage audio par `-an`.

**`-g 30 -keyint_min 30` : une image-clé par seconde, pas une de moins.** Sans eux, x264 en
laisse une toutes les 2 s (mesuré sur `the-box` : 0, 2, 4… 32 s), parfois beaucoup plus. Or
l'app place la lecture sur une borne de segment (`fromSec`) et attend qu'une vidéo se cale : le
décodeur remonte à l'image-clé précédente et décode jusqu'à la cible, donc plus elles sont
espacées, plus le placement est lent, sur un téléphone d'entrée de gamme et sur le Pi. À 1 s,
le pire cas est un rejeu d'une seconde d'images (docs/13, section 7, mesure 11). Coût : environ
+30 % de taille pour une scène de 30 s (4,2 → 5,6 Mo mesuré, dont une part vient de la seconde
génération quand on réencode un fichier déjà servi).

Compatible avec le `-c:v copy` du worker : la structure de GOP n'entre ni dans le concat avec
le bumper ni dans le mixage, seul le profil compte (H.264 High, yuv420p, 1280×720, 30 i/s), et
il ne change pas. Vérifié le 2026-09-30 sur `the-box` : scène `-g 30` + `bumper.mp4` en
`-f concat -c copy`, puis mixage en `-c:v copy`, sans erreur ; les images-clés sont restées à
1 s dans le rendu (0,04 s, 1,04 s, 2,04 s…, décalées du seul délai de départ). Le bumper est sans
B-frames et se colle derrière n'importe quelle structure de GOP (`scripts/make-hd.mjs`).

**Une scène déjà au catalogue** se réencode avec `node scripts/regop-scenes.mjs <slug>…` : il
part du fichier servi, garde la piste son telle quelle (`-c:a copy`) et l'image image pour image,
écrit dans `storage/prep/gop30/` (jamais en place), vérifie durée, nombre d'images et
images-clés, puis affiche la commande de copie vers le Pi — sans la lancer. La 1080p ne
suit pas : elle ne sert qu'à la copie qu'un joueur demande après la révélation, jamais au jeu.

**Le logo Dub'Up en haut à gauche, dans le clip servi** (Benjamin, 2026-10-05 : « pour montrer
qu'elles nous appartiennent »). Jamais au montage — le worker rend en `-c:v copy` — mais gravé
une fois dans la source, d'où il se retrouve partout : table, prise, montage, copie 1080p,
vertical, réseaux. Pour une **scène nouvelle**, le poser dans l'encodage de l'étape 5 lui-même,
sans seconde génération : remplacer `-vf …` par

```
  -i apps/worker/assets/logo-dark.png   -filter_complex "[0:v]scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:-1:-1[base];[2:v]scale=141:-1,format=rgba,split[logo][silhouette];[silhouette]colorchannelmixer=rr=0:gg=0:bb=0:aa=0.55,gblur=sigma=4[shadow];[logo]colorchannelmixer=aa=0.85[mark];[base][shadow]overlay=28:28:format=auto[shadowed];[shadowed][mark]overlay=26:26:format=auto,format=yuv420p[v]"   -map "[v]" -map 1:a ```

(le logo en troisième entrée, 11 % de la largeur à 2 % du bord, 85 % d'opacité, une ombre douce
pour les images claires — le graphe exact est `watermarkFilter` dans
`scripts/filigrane-scenes.mjs`). Pour une **scène déjà servie**, ou une 1080p sortie de
`make-hd.mjs` : `node scripts/filigrane-scenes.mjs <slug>…` — même profil et mêmes
vérifications que regop-scenes.mjs, écrit `storage/prep/filigrane/` (jamais en place) et
affiche la commande de copie vers le Pi sans la lancer. Fait pour tout le catalogue le
2026-10-05. Le vertical reprend le petit logo au prochain `make-vertical.mjs`, qui lit le clip
servi ; en attendant il garde son grand logo sous la scène.

**Vérifier à l'oreille que plus aucune voix n'est audible** avant de publier. Une syllabe
anglaise résiduelle sous le doublage se remarque immédiatement.

## Étape 6 — Rédiger le SceneManifest

Traduire le tableau de l'étape 1. Points où l'on se trompe, à vérifier un par un :
`references/manifest-checklist.md`.

En voie **(a)**, le fond sans voix est déjà muxé dans le clip — donc c'est `keepOriginalAudio`
qui le porte, exactement comme `sc_the_box`, et on n'a pas à toucher `ambienceUrl`.

> **Le gain du lit se règle à l'encodage, pas dans le manifeste.** `originalAudioGainDb` est en
> `z.number().min(-60).max(0)` : **on ne peut pas amplifier depuis le manifeste**. Or un fond qui
> sort d'un séparateur est très bas — mesuré sur « Mauvaise nouvelle » : −44,7 LUFS, soit 31 dB
> sous le lit de `sc_the_box` (−13,6 LUFS). Lui appliquer les −18 dB usuels le rendrait
> parfaitement inaudible.
>
> La cible est le niveau où `sc_the_box` arrive **après** son gain : **−31,6 LUFS**. Mesurer le
> fond séparé, poser la différence en `volume=<N>dB` dans le `ffmpeg` ci-dessus, et laisser
> `originalAudioGainDb: 0` en disant pourquoi en commentaire. Vérifier au passage que le pic
> reste sous −3 dBFS : un transitoire de bruitage écrête bien avant que le niveau intégré soit
> atteint.

Le manifeste porte alors :

```jsonc
"audioBed": {
  "keepOriginalAudio": true,
  "originalAudioGainDb": 0,
  "ambienceUrl": null,
  "musicUrl": null
},
"rights": { "sourceKind": "owned", "allowsPublicSharing": true, "allowsMusicReuse": false }
```

En voie **(b)** seulement, `keepOriginalAudio: false` et une nappe déclarée. Attention alors :
`ambienceUrl` est en `z.string().url()` — **URL absolue obligatoire**, contrairement à
`video.url` qui accepte une clé de stockage. Et `originalAudioGainDb` reste requis même à
`false` : le champ n'est pas nullable.

**Les enveloppes.** Les voix anglaises les rendent mesurables, sur
`storage/prep/<slug>-vo.mp4` et non sur le clip servi — c'est la méthode déjà employée pour
`THE_BOX_ENVELOPES`, avec la même échelle commune à toute la scène (−55 dB → 0, −15 dB → 100),
jamais recalculée segment par segment. Ne pas les laisser à `null` : c'est ce qui dit au joueur
sur quel mot le personnage pousse.

> **Mettre à zéro hors de `speechStartMs`..`speechEndMs`.** La séparation isole les voix mais ne
> distingue pas les locuteurs : sur une scène où deux personnages se chevauchent, la queue d'un
> segment porterait la voix du voisin, et le joueur verrait une courbe monter alors que son
> personnage s'est tu. Hors de sa parole, l'enveloppe d'un personnage vaut zéro — ce n'est pas
> un arrangement, c'est ce qu'elle mesure.
>
> Mesurer aussi sur le stem `vocals.wav`, pas sur le mix : c'est là que les bornes de parole sont
> nettes. Et quand la piste de voix ne coupe pas entre deux personnages, **trancher à l'image** —
> repérer l'image où la bouche du premier se referme.

> **Le seuil de −30 dB de `silencedetect` rate ce qui est murmuré.** Sur « Mauvaise nouvelle »,
> le « non » que Larry souffle en secouant la tête (10,91 → 11,94 s, −33 dB sur la voix isolée)
> est passé sous le seuil, et sa réplique s'ouvrait 1,2 s trop tard. Après la mesure, **relire
> le stem de voix en RMS par 100 ms** sur les silences qui précèdent chaque attaque (astats), et
> ouvrir la parole sur tout ce qui dépasse −40 dB et que l'image confirme. Un souffle sans
> bouche qui bouge n'est pas une attaque ; un non de la tête, si.
>
> **Et vérifier la fin de chaque réplique à l'image, pas seulement sur un creux de la voix.**
> Le même film : la piste de voix marque un creux de 320 ms à 27,9 s, qui avait été lu comme la
> frontière entre le médecin et des sanglots de Larry. À l'image, le médecin articule sans
> s'arrêter jusqu'à se détourner, 1,3 s plus tard. Extraire les visages en planche
> (`ffmpeg -ss … -vf fps=8,crop=…,tile=8x2`) sur la zone douteuse, avant d'écrire la borne.

**La bouche.** Une fois les bornes posées et la scène dans le catalogue, une passe de détection
labiale par scène — c'est ce que lit la bande rythmo pour poser les syllabes sur les
ouvertures et les labiales (b, p, m) sur les fermetures :

```
npm run build -w @dubup/shared
node scripts/detect-lips.mjs <slug>
```

Ça écrit `packages/shared/src/<slug>-lips.ts` (`<SLUG>_LIPS`, un point tous les 50 ms comme
l'enveloppe, `null` là où le visage n'est pas vu) et une planche de contrôle dans
`output/scenes/<slug>/lips.png` — **la regarder** : une courbe qui bouge avec la parole, des
crêtes orange sur les voyelles, des traits rouges sur les fermetures. Puis brancher chaque
segment : `mouth: <SLUG>_LIPS.s1`, à côté de son `envelope`. Le script dit quels segments n'ont
pas de bouche (visage vu moins de la moitié du temps de parole : un profil franc dans un plan
large, typiquement — MediaPipe ne le tient pas) ; ceux-là restent sans `mouth`, la bande y lit
l'enveloppe. Python et MediaPipe : le même `python.exe` que Demucs, `pip install mediapipe
opencv-python` ; le modèle est dans `tools/lips/face_landmarker.task`. Une scène prend une
minute. Toute retouche des bornes invalide la mesure : relancer.

Valider avant publication : ajouter la scène dans `packages/shared/src/catalogue.ts`, enveloppée
dans `parseSceneManifest({ … })`. Le parse a lieu au **chargement du module** et jette sur toute
violation — donc compiler ne suffit pas, `tsc` ne fait que typer, il n'exécute rien. Il faut
compiler **puis importer** :

```
npm run build -w @dubup/shared
node --input-type=module -e "import('./packages/shared/dist/catalogue.js').then(m => console.log(m.CATALOGUE_SCENES.length + ' scenes'))"
```

> `docs/03` mentionne un `npm run validate:scene` : **ce script n'existe pas** dans
> `package.json`. Ne pas le lancer, il échouera. La validation passe par le parse ci-dessus.

## Étape 7 — Rendre compte

Dire, dans cet ordre : **ce qu'on voit et ce qu'on entend à 0:02** (l'accroche, mesurée : le
bruit à x s, la première attaque de parole à y s), la durée obtenue, le nombre de plans, les
bornes de segment **mesurées** (et de combien elles s'écartent du tableau de l'étape 1), **où
sont les silences et ce qu'on y entend**, la longueur de la queue muette.

Puis confirmer les deux points qui ne se rattrapent pas après publication :

- **aucune voix anglaise audible** dans `storage/scenes/<slug>.mp4` ;
- `storage/prep/<slug>-vo.mp4` conservé — sans lui, plus moyen de remesurer les enveloppes ni
  la bouche le jour où une borne de segment bouge.
