# Checklist du SceneManifest — scène générée

Les invariants sont validés par zod dans `packages/shared/src/scene.ts` (`checkSceneInvariants`)
et décrits dans `docs/03-format-scene.md`. Ce fichier ne liste que ce sur quoi **une scène
générée** se casse, en pratique.

## Les pièges de champs

| Champ | Piège |
|---|---|
| `audioBed.ambienceUrl` | `z.string().url()` — **URL absolue obligatoire**. Une clé de stockage (`scenes/x/amb.m4a`) est rejetée. C'est le seul champ média du schéma dans ce cas : `video.url`, `video.posterUrl` et `character.avatarUrl` acceptent une clé. |
| `audioBed.originalAudioGainDb` | Requis, `-60 ≤ x ≤ 0`. Pas nullable, même quand `keepOriginalAudio` est `false`. Mettre `-18`. |
| `rights.sourceKind` | Énuméré : `public_domain`, `cc0`, `cc_by`, `licensed`, `owned`. Pour une scène générée → `owned`. |
| `rights.allowsMusicReuse` | Défaut `false`. Ne le passer à `true` que si une musique est réellement fournie et autorisée. |
| `synopsis` | 280 caractères. Court volontairement : l'image raconte, la surprise fait le jeu. |
| `foley` | **Le rôle de bruiteur a été retiré de Dub'Up.** Sur une scène générée : `{ "enabled": false, "cues": [] }`. Le champ existe encore dans le schéma et dans `FOLEY_SEGMENT_ID`, mais plus aucun joueur ne tient ce rôle — le son de la scène vient entièrement de la génération. |

## Les invariants qui sautent le plus souvent

1. `0 ≤ startMs < endMs ≤ durationMs`.
2. Durée d'un segment : **700 ms minimum, 20 s maximum**.
3. Deux segments d'un **même** personnage ne se chevauchent jamais. Deux personnages différents,
   si (dispute, chevauchement voulu).
4. **Chaque personnage porte au moins un segment.** Un personnage visible mais muet n'est pas un
   rôle : soit on lui écrit un segment, soit on le sort de `characters`. Piège fréquent sur une
   scène générée, où un figurant traverse un plan.
5. `id` de segment uniques et **stables** : ils indexent les prises et les scripts réécrits.
6. `durationMs` entre 5 s et 60 s. Une génération Seedance de 30 s passe large.
7. Respiration finale : dernier `endMs` < `durationMs`, d'au moins 400 ms
   (`SCENE_MIN_TAIL_MS`), viser 1 à 3 s.
8. `characters` : de 1 à 6.

## `speechStartMs` et `speechEndMs`

**Mesurés** dans les deux cas. Sur une scène importée, par détection de parole sur la VO ; sur
une scène générée, sur les voix anglaises de `storage/prep/<slug>-vo.mp4`. Le découpage de plans
sert à écrire le prompt — les valeurs du manifeste viennent de la mesure, pas du plan.

- `startMs ≤ speechStartMs < endMs`, sinon rejet.
- `speechStartMs < speechEndMs ≤ endMs`, sinon rejet.
- Absents, l'attaque vaut `startMs` et la parole court jusqu'à `endMs`.

Un segment est un **tour de parole**, pas une réplique : il commence quand le personnage
précédent se tait. `speechStartMs` est donc souvent 1 à 3 s après `startMs`, et c'est ce champ —
lui seul — qui pilote le décompte affiché au joueur. Le renseigner : sans lui, le joueur fixe
l'image sans savoir quand attaquer.

Fermer chaque segment **une seconde après `speechEndMs`**, sauf si le même personnage reparle
avant (les segments d'un même personnage ne peuvent pas se chevaucher) ou si la scène se termine.

## `envelope`

`SegmentEnvelopeSchema` décrit l'intensité de la **voix d'origine**, à échelle commune à toute
la scène.

Une scène générée en a une : les voix anglaises de `storage/prep/<slug>-vo.mp4`. Les mesurer là
et **jamais** sur le clip servi aux joueurs, qui n'a plus de voix — même méthode et même échelle
que `THE_BOX_ENVELOPES` (−55 dB → 0, −15 dB → 100, commune à la scène entière, jamais
recalculée segment par segment : normaliser chaque segment ferait crier un murmure).

Toute retouche des bornes de segment invalide les enveloppes : les remesurer.

Une pause de 400 ms et plus dans la réplique doit **se voir** : Demucs laisse dans la voix une
rumeur, un souffle, une foule, qui tiennent la forme d'onde au-dessus de 20 et la font lire d'une
traite. Les points de la pause passent à 0 (Benjamin, 2026-10-04 : « la réplique est en deux temps,
la zone qui oscille est d'une traite »).

Si on la renseigne un jour, `levels.length * binMs` doit couvrir `endMs - startMs` à un `binMs`
près, sinon rejet.

## Avant publication

Ajouter la scène dans `packages/shared/src/catalogue.ts` via `parseSceneManifest({ … })`, puis :

```
npm run build -w @dubup/shared
```

Le parse a lieu au chargement du module : toute violation jette à la compilation.

Puis le **contrôle des fenêtres**, obligatoire (Benjamin, 2026-10-04 : « je veux un contrôle
total, il en va de la qualité de l'application ») :

```
python scripts/controle-fenetres.py <slug>
```

Il confronte chaque réplique à la voix isolée et aux mots de Whisper : parole coupée au début ou à
la fin de la fenêtre, pause cachée par la forme d'onde, forme d'onde infidèle ou décalée. La scène
n'entre au catalogue qu'à **0 réplique à regarder** : chaque signalement est soit corrigé, soit
écouté et jugé sans défaut, puis inscrit dans `REVIEWED` avec sa raison. Une foule dans la piste
voix se déclare dans `CROWD`.

Puis le **teaser de la vignette** (Benjamin, 2026-10-04 : « des extraits de la vidéo pour donner
envie ») : quatre extraits de deux secondes hors des répliques, muets, en boucle, que la vignette
joue au survol (ordinateur) ou en haut de l'écran (téléphone), et la grande affiche de la scène de
la semaine tout le temps :

```
python scripts/scene-teaser.py <slug>
```

Il écrit `storage/scenes/<slug>/teaser.mp4` (≈ 130-300 Ko) et une planche de contrôle dans
`output/teasers/<slug>.png` — regarder qu'aucun extrait ne montre une bouche qui parle ; `MANUAL`
dans le script force les moments d'une scène. Poser `teaserUrl: 'scenes/<slug>/teaser.mp4'` sous
`posterUrl` dans catalogue.ts, et copier le fichier sur le Pi avec les autres médias de la scène.

Puis le **logo en haut à gauche** du clip servi (Benjamin, 2026-10-05) : il est posé dans
l'encodage de l'étape 5 pour une scène nouvelle ; sinon, `node scripts/filigrane-scenes.mjs
<slug>` (720p et 1080p). Vérifier sur une image extraite (`ffmpeg -ss 2 -i … -frames:v 1`) que
le logo est là avant de copier la scène sur le Pi.

`docs/03` cite un `npm run validate:scene` — **il n'existe pas** dans `package.json`.
