Sous-titres vidéo conversationnels
Aperçu
Les sous-titres forment leur propre couche : ils portent chaque mot prononcé et ne dépendent jamais de ce qui est dessiné dans l'image. Les étiquettes des éléments dans l'image (noms de pièces, avantages/inconvénients, flèches) sont une couche différente avec un langage visuel différent (annotations à côté de l'objet) ; ne coupez jamais un mot des sous-titres parce qu'« il est déjà à l'écran ».
Pipeline : words.json + forbid.json (boîtes que les sous-titres ne doivent pas couvrir) + config.json + profile
→ plan → manifest.json (données canoniques) → burn (aperçu gravé = vérité visuelle) / export srt|ass|vtt → validate.
Rôles des formats (ne pas les mélanger) :
| format | contient | ne contient PAS |
|---|---|---|
manifest.json | mots, timings, mise en page, placement, chevauchements acceptés, levées avec raisons, vérifications de rendu : source de vérité pour les données | pixels |
.srt | texte + timing | graisse, couleur, contour, mot actif, position. Ne promettez jamais de style dans SRT |
.ass | famille + Gras/\b700, taille convertie en métriques libass, contour, boîte de fond, mot actif par tranche de temps, position | fondus ; nécessite la police : subtitles=x.ass:fontsdir=fonts |
.vtt | CSS ::cue (famille, graisse, contour via text-shadow, fond) + classes .active_<spk> / .em_<spk> / .dim / .u par tranche de temps | position exacte ; la page doit charger la police avec @font-face |
preview.mp4 gravé | tout : la seule source de vérité visuelle | — |
Quand l'utiliser
- Sketches dialogués, interviews, vidéos explicatives avec narrateur : format vertical ou carré, une ou deux lignes maximum.
- Vous avez des timings de mots (ou audio : STT cloud optionnelle via des clés d'environnement) et vous savez où se trouvent les visages / mains / accessoires clés (boîtes ; pour les scènes 3D, générez un masque de couleur unie et encadrez-le).
- Pas pour les vidéos lyriques (mots rebondissants, à l'échelle, délibérément non pris en charge).
Algorithme
Segmentation (chunk dans le profil) : rupture dure sur pause > pause_s (0,35 s), fin de phrase, changement de locuteur ; rupture douce sur une virgule une fois qu'un bloc a 2+ mots ; limité par max_words, largeur en pixels (max_width_frac de la zone de sécurité) et max_dur_s (~1,2 s de parole se lit bien). Un bloc ne se termine jamais sur un mot fonctionnel (не, в, а, and, the…) : il passe au bloc suivant. Un mot isolé rejoint son voisin de la même phrase s'il tient. Les durées des mots sont plafonnées à 2,5× la médiane pour qu'un mot ne traîne pas dans une pause. Minutage : une unité apparaît ≤ 50 ms avant son premier mot, ne coupe jamais le mot précédent, reste hold_s après son dernier mot, au moins min_show_s.
Placement — placement: {mode: fixed_bottom_center, collision_lift: false} (par défaut pour le dialogue).
- Une seule ligne de base pour toute la vidéo :
baseline = safe.bottom - baseline_margin_px(ou définie explicitement). Chaque unité est alignée en bas sur celle-ci (y + h = baseline) et centrée sur l'image (x + w/2 = W/2). Rien ne bouge : pas de levée, pas de décalage latéral, pas de x par locuteur. La largeur de ligne est limitée pour que la ligne centrée reste à l'intérieur de la zone de sécurité. - Une ligne qui croise une boîte interdite à la ligne de base reste en place : l'unité reçoit
overlap_accepted: [classes]et la feuille de QA dessine un cadre jaune « chevauchement accepté (ligne de base fixe) ». Information, pas un échec. Croiser l'interface utilisateur de la plateforme reste une erreur (réduisez la police ou fractionnez la phrase). - Option
collision_lift: true(modebaseline_lift) : x sous le locuteur (align: speaker) ou centré avec solutions de repli gauche/droite ; si chaque x entre en collision, soulevez par pas entiers delift_step_pxjusqu'à être libre. L'unité reçoitlift = {px, reason: [classes heurté à la ligne de base], t0, t1}, le manifeste liste toutes les levées danscollision_lifts. Pas de raison → pas de levée. Utilisez-le uniquement lorsque le brief demande de garder les visages dégagés à tout prix. - Ne supprimez jamais un mot.
Apparence. Contour (outline_px) + ombre douce + un fond léger dont l'alpha est augmenté par unité juste assez pour que la couleur la plus sombre éclairée soit ≥ 4,5:1 (WCAG) par rapport au pixel au 90e centile derrière elle. Les locuteurs diffèrent par la couleur du mot actif et, en mode hybride/cartes, une barre de 4 px dans la couleur du locuteur ; pas de bulles de parole, pas de plaques nominatives lourdes. Mouvement calme : fondus alpha uniquement, pas de pop d'échelle, pas de glissements verticaux.
QA (scripts/validate.py, également exécuté par demo) :
- schéma : le manifeste par rapport à
templates/manifest.schema.json; - lignes ≤ 2 ;
- placement : en mode
fixed_bottom_centerl'étalement de y-baseline et de x-centre sur toutes les unités est 0 (tolérance 0,5 px) et il n'y a pas de levées ; en modebaseline_liftchaque levée est sur la grille, a une raison, et l'unité entre effectivement en collision à la ligne de base ; - zéro chevauchement avec l'interface utilisateur de la plateforme sur chaque image ; boîtes interdites : 0 en
baseline_lift, signalé commeoverlap_accepted(info) enfixed_bottom_center; - chaque mot exactement une fois, dans l'ordre, visible pendant qu'il est prononcé ;
- contraste ≥ 4,5 et pas de rebond, tous deux mesurés par image par
burn; - polices résolues : fichier présent, famille et style Pillow, poids OS/2,
fc-scan, et lefontselectde libass sélectionne le fichier et le poids déclarés ; - pixels : sur l'image gravée ET sur le rendu libass de l'ASS, la couleur active est présente, le bas du glyphe est à moins de 3 px de la mise en page, la largeur de hampe est à moins de 1 px d'un raster de référence du poids déclaré, et le contour est à moins de 1 px de
outline_px.
Profils (profiles/*.json)
| profil | quoi | meilleur pour |
|---|---|---|
karaoke | 2–4 mots à la fois, mot parlé dans la couleur du locuteur, reste atténué | le plus stable ; couloirs fixes, vidéos explicatives |
reveal | les mots apparaissent en fondu à la parole ; mise en page fixée à l'avance ; ≤ 2 lignes | narration calme |
cards | une pensée par carte (≤ 8 mots, 2 lignes), panneau dense, mot-clé coloré | lignes lentes et citables ; lecture avant la parole |
kinetic | un jeton à la fois, police plus grande | punchlines uniquement ; peu de contexte |
hybrid (par défaut) | ≤ 3 mots, les mots apparaissent en fondu, soulignement sous le mot parlé, couleur d'emphase, barre de locuteur | dialogue |
Options des systèmes de R&D en tant que paramètres : tempo = chunk.max_dur_s: 1.2 + timing.min_show_s: 0.83 (activé dans hybride) ; stress = emphasis: "energy" (score z de sonie du mot par locuteur à partir de --audio), ou "keyword" avec une liste keywords.
Commandes
python -m subkit demo examples/fixture # 5 profils + hybrid_lift -> aperçus, srt/ass/vtt, validation_report.{md,json}
python -m subkit plan --words W --forbid F --config C [--profile hybrid] [--audio A] -o manifest.json
python -m subkit burn manifest.json --forbid F --video in.mp4 [--audio A] -o preview.mp4 --qa qa.jpg
python -m subkit export manifest.json --format ass -o captions.ass # srt | ass | vtt
python scripts/check_clean.py # avant un commit : pas de media/keys/chemins absolus
ELEVENLABS_API_KEY=... python -m subkit stt audio.wav -o words.json # optionnel ; ou --provider groq avec GROQ_API_KEY
Pièges
- libass ≠ taille de police Pillow. libass dimensionne par winAscent+winDescent OS/2 et place la ligne de base winAscent sous le sommet de la ligne ; l'export convertit les deux, sinon le texte ASS sort ~35 % plus petit, plus fin et plus bas.
- Les polices variables dans ASS reviennent à l'instance par défaut (généralement Regular). Livrez une instance statique du poids que vous utilisez (ici
Onest-Bold.ttf, 700) et passezfontsdir. ffmpeg -ssavant-iréinitialise les timestamps : le filtre de sous-titres rend alors t=0. Cherchez après-ilors de la gravure ASS.- Le karaoké
\k/\kfest en centisecondes et colore chaque mot parlé de façon permanente ; pour « seulement le mot courant », émettez des tranches de temps (comme le fait l'export). - SRT n'a pas de style : si une livraison nécessite du poids ou une surbrillance, livrez ASS/VTT ou la vidéo gravée.
- Une ligne flottante (x par locuteur, hystérèse, levées « plus jolies ») est perçue comme une gigue dans le dialogue. Gardez le mode fixe par défaut ; en mode
baseline_lift, le validateur revérifie que chaque levée entre effectivement en collision. - Le contraste est jugé sur la couleur éclairée par rapport aux 10 % les plus lumineux des pixels derrière elle, pas la moyenne : le feu, la neige et les hautes lumières échouent aux moyennes.
- Ne supprimez pas les mots déjà dessinés à l'écran. Déplacez l'étiquette dessinée vers une couche d'annotation à la place.
- Les zones de sécurité dans
templates/safe_zones.jsonsont approximatives ; les interfaces utilisateur des plateformes changent, donc revérifiez-les avant publication.
Liste de vérification
- words.json couvre toute la parole (STT cloud ou aligneur forcé) ; locuteurs étiquetés
- forbid.json : visages, mains, feu/accessoires par plan (+ boîtes
speakerspouralign: speaker) - config : taille, fps,
safe_zone, couleurs des locuteurs (teintes différentes, toutes ≥ 4,5:1 sur le fond) -
plan→ manifeste ; lisezoverlap_accepted(fixe) oucollision_lifts(mode levée, chacune avec une vraie raison) -
burn→ regardez la feuille de QA (cyan = ligne de base, jaune = chevauchement accepté, orange = levée avec raison) et les images réelles -
validatetout PASS ; livrez la vidéo gravée (+ ASS/VTT si un lecteur les rend ; SRT uniquement en texte brut)