Sous-titreur de Démo Vidéo
Problème
Les démos produits enregistrées échouent auprès de leur public pour trois raisons qui n'ont rien à voir avec le produit : le texte UI est dans la mauvaise langue, il est trop petit pour être lu, et la vidéo n'a souvent aucune narration (enregistrements silencieux). Le spectateur voit des clics se produire mais ne peut pas comprendre pourquoi. Les sous-titres résolvent cela — mais seulement s'ils sont stylisés pour être lisibles sur une UI chargée, placés là où ils ne couvrent pas l'élément démontré, et chronométrés pour ne jamais être coupés par une transition.
Cette compétence est le pipeline complet, y compris les modes de défaillance non évidents qui coûtent des heures lorsqu'ils sont découverts à dure.
Quand l'utiliser
- "Ajoute des sous-titres à cette vidéo de démo" / "sous-titre cet enregistrement pour notre équipe coréenne"
- Un enregistrement d'écran silencieux (
.MP4, sans flux audio, ou audio qui n'est que de la musique de fond) où l'histoire doit être racontée par le texte de la légende lui-même - Une démo produit en anglais uniquement montrée à un public non anglophone
- Des sous-titres existent déjà mais sont centrés en bas et couvrent le panneau de démo — nécessitant un placement qui suit le focus à la place
- Générer un script de lecture pour un présentateur qui parlera sur la vidéo (voir "Utilisation complémentaire")
Ne pas utiliser cette compétence pour : le doublage avec voix synthétisée (c'est un pipeline TTS), ou extraire des légendes qui existent déjà sur une plateforme.
Installation (une commande)
scripts/bootstrap.sh --dry-run # inspection uniquement : affiche exactement ce qui manque
scripts/bootstrap.sh # installe ce qui manque (demande une fois)
scripts/bootstrap.sh --yes --lang ja # sans surveillance ; prépare aussi les polices japonaises
bootstrap.sh est idempotent — il ne fait rien sur une machine déjà prête. Il détecte la
plateforme et choisit le bon installateur (Homebrew / apt / dnf / pacman / winget / scoop), et se
rabat sur le placement d'un ffmpeg statique dans ~/.local/bin lorsqu'aucun gestionnaire de paquets n'est disponible.
Exécutez-le comme première action de toute session qui va incruster des légendes ; les compétences n'ont pas de hook
au moment de l'installation, donc ce script est l'installateur.
Prérequis
Exécutez d'abord la vérification intégrée — elle inspecte la machine et vous dit exactement ce qui manque :
scripts/doctor.sh "Hiragino Sans GB" # passez la famille CJK que vous comptez utiliser
Ce qu'elle vérifie, et la commande d'installation quand quelque chose manque :
| Prérequis | Pourquoi | Installer |
|---|---|---|
ffmpeg compilé avec libass | incruste les légendes .ass (-vf ass=...) | macOS brew install ffmpeg ; Windows : build complet gyan.dev ; Debian apt install ffmpeg |
--enable-fontconfig dans la compilation ffmpeg | résout les polices par nom de famille ; sans cela vous obtenez un visage de secours silencieux | les mêmes builds ci-dessus l'incluent |
| une famille de police CJK que le système peut résoudre | le texte de la légende lui-même | macOS : Hiragino Sans GB (pré-installée) · Windows : Microsoft YaHei (pré-installée) · Linux : apt install fonts-noto-cjk |
python3 (optionnel) | seulement export_cues.py ; bibliothèque standard, aucun pip install | macOS : xcode-select --install ; Linux : apt install python3 |
bash, zip (optionnel) | scripts, et empaquetage pour distribution | pré-installé partout |
Pas de Node, pas d'ImageMagick, pas de paquets pip, pas de clés API. Un modèle capable de vision (l'agent lui-même) lit les images — ce n'est pas quelque chose que vous installez.
Deux notes d'environnement qui piquent :
- Mettez
ffmpegdans lePATH, ou exportez son emplacement pour les scripts :FFMPEG=/chemin/vers/ffmpeg scripts/burn.sh .... - Windows : libass résout les polices via fontconfig/DirectWrite selon la compilation. Si
les légendes s'affichent avec la mauvaise police, installez fontconfig (
scoop install fontconfigou MSYS2) et revérifiez avecfc-match "Microsoft YaHei:bold".
Pipeline
Travaillez de haut en bas. Ne sautez pas la Phase 2 — chaque phase ultérieure dépend de la connaissance de la géométrie UI.
Phase 1 — Inventaire (2 minutes)
scripts/probe.sh INPUT.mp4
Enregistrez : durée, résolution, fps, débit binaire, si un flux audio existe. Exécutez la vérification audio dans le script (volume moyen + carte de silence).
Décision à partir de la vérification audio :
| Résultat audio | Type de sous-titre |
|---|---|
| Aucun flux audio du tout | Sous-titres de narration — vous rédigez l'histoire ("ce qui se passe / pourquoi c'est important") |
| Audio présent mais pas de parole (musique uniquement) | Identique ci-dessus. Confirmez en transcrivant une tranche de 10s : un outil ASR qui renvoie vide ou presque vide sur de l'audio fort signifie musique, pas voix |
| Parole présente | Sous-titres de dialogue — transcrivez d'abord avec horodatages, puis nettoyez ; n'inventez pas la copie |
Phase 2 — Analyse visuelle et géométrie (la phase que les gens sautent)
Extrayez les images à 2 ips (un clip de 60s → ~120 images ; révisez d'abord toutes les 4 images pour l'arc, puis ez-vez sur le texte) :
scripts/split_frames.sh INPUT.mp4 frames/ 2
Construisez trois artefacts avant d'écrire une seule légende :
-
Feuille de rythme —
| heure | ce qui a changé à l'écran | texte à l'écran (textuel) |. Un "rythme" est tout changement : une notification apparaît, un panneau glisse, un bouton est cliqué, un défilement, une permutation de page, une transition. Notez les types de transition (flash blanc, coupure, noir) — les légendes ne doivent pas les chevaucher. -
Texte UI, textuel, lu à la résolution source. Les images de prévisualisation à échelle réduite brouillent les petits textes UI et vous le transcrirez mal. Recadrez à partir du fichier original :
ffmpeg -i IN.mp4 -ss 12.5 -frames:v 1 -vf "crop=W:H:X:Y,scale=2*iw:-1" out.jpg(-ssaprès-ipour une extraction image précise ; voir Pièges.) -
Carte de géométrie — les limites en pixels du panneau "actif" (la chose démontrée) en pixels source. Mesurez-le, ne le devinez pas :
scripts/find_edge.sh INPUT.mp4 30 2880 300 # t=30s, bande à partir de x=2880, 300px de largeLisez ensuite l'image de bande renvoyée et notez où le bord gauche du panneau se trouve à l'intérieur de la bande ; ajoutez ce décalage au x de la bande pour obtenir le bord absolu. Répétez pour d'autres scènes si la disposition change (par exemple, un panneau de chat s'ouvre plus tard).
Règle qui découle de la carte de géométrie : la légende vit dans la région libre adjacente au panneau de démo, jamais par-dessus. Voir Phase 5.
Phase 3 — Rédigez la copie de légende
Deux règles gouvernent tout :
- Une idée par réplique. Maximum 2–3 lignes courtes. Une réplique est une phrase, pas un paragraphe.
- Budget de vitesse de lecture. CJK : 3,5–5,5 caractères par seconde est confortable ; au-dessus de 6 c'est une
bousculade ; en dessous de 3 ça semble mort. Latin : 12–18 caractères par seconde. Calculez-le, ne devinez pas.
scripts/export_cues.pyaffiche la densité par réplique une fois que vous avez un fichier.ass— itérez par rapport à ce nombre.
Règles de synchronisation des répliques :
- Commencez la réplique au moment où le rythme visuel arrive, pas avant. Si une notification glisse à 1,0s, la réplique commence à 1,0s.
- Terminez les répliques avant une transition (flash blanc / coupure), en laissant au moins 0,8s de silence à travers elle. Une légende qui chevauche une transition donne l'impression d'un glitch.
- Laissez de vrais écarts. Trois à cinq secondes vides par minute garde la vidéo respirable ; des légendes de bout en bout épuisent le spectateur.
- Pour les sous-titres de narration, mettez la raison dans la légende, pas la mécanique : "l'agent a contacté d lui-même" bat "une notification est apparue dans le coin supérieur droit".
Arrêtez-vous ici et faites approuver la copie. Texte approuvé → stylisation. Texte rejeté après stylisation coûte un deuxième rendu pour rien.
Phase 3b — Choix de la langue de la légende
Le pipeline est indépendant de la langue, mais trois choses doivent être décidées par langue avant la stylisation.
1. Famille de police — choisissez-en une que la plateforme peut résoudre (vérifiez avec scripts/doctor.sh <lang>):
| Langue de la légende | macOS | Windows | Linux |
|---|---|---|---|
| Chinois (zh) | Hiragino Sans GB / PingFang SC | Microsoft YaHei | Noto Sans CJK SC |
| Japonais (ja) | Hiragino Sans | Yu Gothic / Meiryo | Noto Sans CJK JP |
| Coréen (ko) | Apple SD Gothic Neo | Malgun Gothic | Noto Sans CJK KR |
| Latin (en/fr/de/es…) | Helvetica Neue | Segoe UI | Inter / DejaVu Sans |
2. Vitesse de lecture — le budget dépend du script ; scripts/export_cues.py sélectionne le bon
automatiquement (--lang remplace) et signale les répliques qui le dépassent :
| Script | Confortable | Signaler comme trop rapide |
|---|---|---|
| Han / Kana (zh, ja) | 3,5–5,5 caractères/s | > 6,0 |
| Hangul (ko) | 4,0–6,0 caractères/s | > 6,5 |
| Latin | 12–18 caractères/s | > 19 |
3. Césure des lignes — Le chinois et le japonais n'ont pas d'espaces entre les mots : coupez à la ponctuation (U+FF0C, U+3001, U+3002, U+FF1A) et ne coupez jamais un nom de produit. Le coréen et le latin coupent aux espaces ; gardez une particule coréenne attachée à son nom. Si une ligne ne tient pas, refaites la mise en page avant de réduire la police — une police plus petite perd plus de lisibilité qu'une ligne supplémentaire.
4. Largeur de ligne par script — la largeur des glyphes n'est pas la même selon les scripts, donc la même
Fontsize donne des longueurs de ligne différentes. Budgetez les lignes comme floor(disponible_px / (fontsize * k)),
où k est approximativement 1,0 pour Han et 1,05–1,1 pour Hangul et Kana (ils sont plus larges), et 0,5 pour
Latin en moyenne (cela varie avec le mot).
Mesuré en 4K avec Fontsize 165 : 13 caractères Hangul ≈ 2 150 px (sûr dans un créneau de 2 450 px),
tandis que 18 caractères ≈ 2 970 px — cela dépasse et pousse la ligne hors du bord gauche du
cadre. Lorsqu'une ligne ne tient pas, refaites la mise en page ; ne réduisez pas la police pour la faire tenir.
Légendes bilingues (original + traduction) : seulement lorsque les deux lignes sont courtes. Deux lignes de langue doublent la charge de lecture et, dans une démo, rivalisent avec l'interface. Préférez une langue par version du fichier sauf si le client demande autrement ; quand vous devez, empilez l'original en haut et la traduction en dessous, et budgetez l'ensemble du bloc par rapport à la vitesse de lecture de la traduction.
Scripts de droite à gauche (arabe, hébreu) : libass prend en charge le RTL, mais la qualité du façonnage dépend de la compilation de harfbuzz. Rendez une image de prévisualisation avant de vous engager — vérifiez que les lettres se connectent correctement et que l'ancrage d'alignement a toujours du sens.
Le poids de la police diffère selon le script. Les familles japonaises et coréennes rendent généralement visuellement plus grandes
qu'une famille chinoise à la même Fontsize ; vérifiez avec une image de prévisualisation et ajustez à l'œil, pas par
règle.
Phase 4 — Stylisation (la lueur à deux couches)
Un seul style ASS ne peut pas produire "texte blanc avec un contour coloré épais et un halo coloré doux"
— Outline ne dessine qu'un seul anneau. Utilisez deux événements par réplique sur deux couches :
| Couche | Remplissage | Contour | Flou | Objectif |
|---|---|---|---|---|
| 0 | transparent (&HFF000000) | large, violet semi-transparent | \blur 20–30 | le halo |
| 1 | blanc | plus étroit, violet solide | aucun / \blur1 | le texte lisible |
Point de départ (voir assets/caption-template.ass). Les couleurs sont &HAABBGGRR — alpha 00 est
opaque, FF est totalement transparent, et l'ordre des octets est BGR :
purple RGB(124,46,240) → &H00F02E7C
halo, 35% opaque → &H55F02E7C
Mettez à l'échelle avec PlayRes, pas au goût : un rendu 4K utilisait fontsize 180 / outline 16 / halo 28 / blur 24 ;
le même rendu en 1080p est 80 / 7 / 12 / 12. Définissez PlayResX/Y sur la résolution de sortie et
conservez ScaledBorderAndShadow: yes.
Phase 5 — Placement qui suit le focus
Centré en bas est mauvais pour les démos : le bas est généralement l'UI, et le panneau de démo est la seule chose qui doit rester visible. Anchez chaque réplique à la région libre adjacente à ce qui est montré :
| Focus de cette réplique | Ancrage | Pourquoi |
|---|---|---|
| Un panneau latéral / panneau de chat est ouvert | \an6\pos(<gauche_panneau - 40>, <milieu_vertical_panneau>) | le bord droit épouse le panneau, le bloc croît vers la gauche, les changements de largeur ne nécessitent pas de recalcul |
| Un toast/notification en haut à droite | \an3\pos(<droite_toast>, <bas_toast + 80>) | l'ancrage en bas à droite place le bloc juste sous le toast |
| Page pleine largeur, sans panneau | \an5\pos(<centre_visuel_x>, <y entre deux blocs de contenu>) | centré, mais placé dans un écart mesuré — pas au centre géométrique |
| Carte titre avec grand type intégré | \an5\pos(center_x, EN DESSOUS du titre) | le centre géométrique entre en collision avec le propre titre de la vidéo |
Calculez la largeur maximale de ligne avant d'écrire : max_line_px ≈ fontsize × caractères. Si une ligne
dépasse la région libre, refaites la mise en page (ne réduisez jamais la police juste pour une réplique) — coupez à
la ponctuation et gardez les noms de produits non coupés.
Pour les blocs ancrés à droite, un dépassement de quelques dizaines de pixels sur un fond plat est acceptable et semble intentionnel ; le dépassement sur le panneau de démo ne l'est pas.
Gardez l'ancrage hors du bord du cadre. Le contour et le halo s'étendent au-delà de la boîte de glyphe d'environ
outline + blur pixels (en 4K avec outline 15 / blur 22, c'est ~40 px, plus l'anti-aliasing).
Un ancrage \an3 stationné à 50 px du bord droit voit donc son halo coupé et se lit comme un mot
cassé. Stationnez les ancrages à au moins 150 px à l'intérieur du cadre, et vérifiez le résultat dans une image de prévisualisation
— le découpage est invisible dans le fichier .ass et évident à l'écran.
Phase 6 — Incrustation
scripts/burn.sh INPUT.mp4 subs.ass OUT.mp4 [crf]
Ceci est -i IN -vf "ass=subs.ass" -c:v libx264 -crf 17~18 -pix_fmt yuv420p -movflags +faststart,
conservant la piste audio originale. Les sous-titres durs sont la bonne valeur par défaut pour les démos — ils survivent
à chaque lecteur, téléchargement et partage d'écran ; expédiez un fichier .ass/.srt en accompagnement uniquement si demandé.
Phase 7 — Vérification
Rendez d'abord des images fixes bon marché, puis vérifiez le fichier encodé :
scripts/preview_frame.sh INPUT.mp4 subs.ass 16.0 /tmp/check1.jpg
Vérifiez chaque image pour : texte sur le panneau de démo, texte hors cadre, retour à la ligne inattendu, une réplique qui devrait être terminée, halo devenu bouillie sur un fond lumineux. Extrayez ensuite 3 images du fichier terminé (les sous-titres sont cuits, la qualité est finale) et confirmez à nouveau.
Pièges (chacun a coûté du temps réel à trouver)
-ssavant-ineutralise le filtreass. La recherche d'entrée réinitialise les horodatages à zéro, donc le filtre rend l'état du sous-titre à t≈0 pour chaque image. Résultat : la vidéo brûle sans sous-titres du tout, et ffmpeg n'affiche aucune erreur. Mettez toujours-ssaprès-i(recherche de sortie), ou ajoutez-copyts. Les scripts fournis le font déjà.- Les coordonnées des images de prévisualisation mentent. Une image mise à l'échelle en 1280×720 mais affichée pour vous à ...
`}