Animation de sous-titres
Transformez une transcription ou une voix off en sous-titres synchronisés mot par mot qui captent l'attention : chaque mot apparaît sur la syllabe, le mot actif est mis en surbrillance, et le texte reste collé à la narration. Créez le genre de sous-titres qui augmentent le temps de visionnage sur TikTok, Reels et Shorts — lisibles, en rythme, et à l'intérieur de la zone de sécurité.
Quand l'utiliser
- Sous-titres mot par mot ou karaoké pour vidéo verticale courte (9:16).
- Graver des sous-titres ouverts sur une piste de voix off/narration à partir d'une transcription.
- Transformer une transcription Whisper/SRT en texte animé dans Remotion ou sur le web.
- Restyler des sous-titres automatiques en un effet de révélation de marque de type "Hormozi".
Le pipeline
| Étape | Travail | Outil |
|---|---|---|
| Transcription | Audio → horodatages mot par mot | Whisper (@remotion/install-whisper-cpp), AssemblyAI |
| Normalisation | → jetons { text, startMs, endMs }[] | Type Caption de @remotion/captions |
| Page | Grouper les mots en "pages" de 1 à 4 mots | createTikTokStyleCaptions() |
| Animation | Apparition échelle par mot + surbrillance mot actif | spring() Remotion / CSS |
| Placement | Zone de sécurité, typographie lisible | règles de mise en page ci-dessous |
| Export | Burn-in (MP4) et/ou SRT/VTT annexe | rendu Remotion / émission de fichier |
Deux règles non-négociables
- Timing mot par mot, pas par ligne. Le karaoké n'est magique que lorsque chaque mot tombe sur la syllabe. Transcrivez toujours avec des horodatages mot par mot ; ne les simulez jamais en divisant uniformément une ligne sur sa durée — la dérive est immédiatement visible.
- La lisibilité prime sur le style. Les sous-titres se lisent sur un téléphone, en plein soleil, en mode muet. Police sans sérif en gras, contour ou ombre épais, contraste élevé d'abord ; la décoration ensuite. Un sous-titre que personne ne peut lire n'est pas un sous-titre, c'est de la décoration.
Timing mot par mot à partir d'une transcription
Whisper émet directement des horodatages mot par mot. Normalisez chaque source en une seule forme de jeton plate afin que le moteur de rendu n'ait jamais à se soucier de la provenance des mots :
// Jeton de sous-titre — la seule forme que tout l'aval consomme
type Token = { text: string; startMs: number; endMs: number };
// Analyser un bloc SRT "00:00:01,200 --> 00:00:01,640" en ms
const toMs = (t: string) => {
const [h, m, rest] = t.split(":");
const [s, ms] = rest.split(",");
return ((+h * 60 + +m) * 60 + +s) * 1000 + +ms;
};
S'il n'existe que des repères de phrase, retranscrivez pour obtenir un timing mot par mot — n'interpolez pas. Whisper medium.en est environ 2x plus rapide que large et assez précis pour un audio vocal propre.
Apparition échelle par mot (Remotion)
Animez l'échelle et l'opacité de chaque mot à l'aide d'un ressort (spring) ancré à son propre startMs. Le micro-dépassement est ce qui donne cette sensation de "vie".
import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
const Word: React.FC<{ token: Token; active: boolean }> = ({ token, active }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const enter = (token.startMs / 1000) * fps; // image d'entrée propre au mot
const p = spring({ frame: frame - enter, fps, config: { damping: 12, mass: 0.6 } });
const scale = interpolate(p, [0, 1], [0.6, 1]); // pop de 60% → 100% (dépasse)
return (
<span style={{
display: "inline-block",
transform: `scale(${scale})`,
opacity: interpolate(p, [0, 1], [0, 1]),
color: active ? "#FFE45E" : "#FFFFFF", // surbrillance mot actif
transition: "color 80ms linear",
}}>{token.text} </span>
);
};
Calculez active en testant frame par rapport à [startMs, endMs] de chaque jeton. Gardez la surbrillance avec une seule couleur d'accent ou une "boîte" remplie derrière le mot en cours — n'animez jamais la couleur de chaque mot à la fois.
Pages, pas de murs de texte
Affichez 1 à 4 mots à la fois. Utilisez createTikTokStyleCaptions() de Remotion (ou regroupez manuellement) et réglez combineTokensWithinMilliseconds : ~200–500 ms pour du mot par mot pur, ~1000–1500 ms pour de courtes phrases lisibles.
import { createTikTokStyleCaptions } from "@remotion/captions";
const { pages } = createTikTokStyleCaptions({
captions, combineTokensWithinMilliseconds: 1200, // ~2–3 mots par page
});
Apparition Web/CSS (sans Remotion)
Pour un lecteur DOM/en direct, donnez à chaque mot son propre animation-delay égal à startMs :
.word { display:inline-block; opacity:0; animation: pop .26s cubic-bezier(.34,1.56,.64,1) forwards; }
@keyframes pop { from { opacity:0; transform:translateY(.18em) scale(.7) } to { opacity:1; transform:none } }
.word.active { color:#FFE45E; }
words.forEach(w => { const el = mk(w.text); el.style.animationDelay = `${w.startMs}ms`; track.append(el); });
Typographie lisible et placement dans la zone de sécurité (9:16, 1080×1920)
| Propriété | Valeur | Pourquoi |
|---|---|---|
| Police | Sans sérif Gras/ExtraGras (Montserrat, Inter, Helvetica) | Se lit sur des écrans petits et chargés |
| Taille | 56–80px (≈8% de la hauteur du cadre), min 45px | Lisible en mode muet sur un téléphone |
| Contour | 2–6px de contour noir uni | Résiste à tout arrière-plan |
| Ombre | Ombre portée douce en complément du contour | Séparation sur les images lumineuses |
| Casse | Majuscules ou phrase ; remplissage à contraste élevé | Impact + scannabilité |
| Position verticale | Bande centrale, ~62–70% vers le bas | Au-dessus de l'interface, sous l'action |
| Sécurité basse | Rester hors des ~280px / 15% inférieurs | Évite l'interface des sous-titres/CTA/audio |
| Sécurité latérale | Rester dans les 80% centraux de la largeur | Évite les icônes latérales droites |
Placez les sous-titres dans la bande centrale — pas collés tout en bas, là où se trouve l'interface de la plateforme (nom d'utilisateur, audio, boutons). Limitez le texte à un maximum d'environ 2 lignes.
Synchronisation avec une voix off / narration
La transcription porte déjà le timing de l'audio exact. Disposez ce même audio sous la composition et conservez les horodatages dans la base de temps de l'audio — les sous-titres et la voix restent verrouillés sans aucun ajustement manuel. Si la VO est réenregistrée, retranscrivez ; ne décalez jamais les décalages à la main.
Burn-in vs fichier SRT/VTT annexe
| Burn-in (ouverts) | SRT/VTT annexe (fermés) | |
|---|---|---|
| Où | Pixels dans le MP4 | Fichier .srt/.vtt séparé |
| Social (TikTok/Reels) | Requis — garanti, stylisable | Souvent ignoré par la plateforme |
| Accessibilité (ADA/WCAG) | Ne satisfait pas seul | Requis (commutable) |
| Bonne pratique | Graver les sous-titres animés pour le social | Également fournir un fichier annexe pour le web/SEO |
Émettez les deux : gravez les sous-titres animés pour le social, et écrivez un fichier SRT/VTT brut à partir des mêmes jetons pour une lecture accessible et SEO.
Liste de vérification de la sortie
- Horodatages mot par mot (réels, issus de la transcription) — pas de simulation par division uniforme.
- Apparition échelle par mot avec micro-dépassement ; une seule couleur d'accent pour le mot actif.
- 1 à 4 mots par page ; jamais un mur de texte.
- Sans sérif gras, 56–80px, contour de 2–6px + ombre, contraste élevé.
- Bande centrale, hors des ~280px inférieurs et des rails latéraux.
- Audio placé sous la composition ; horodatages dans la base de temps audio.
- Fournir burn-in pour le social et un fichier SRT/VTT annexe pour l'accessibilité.
Livrer et vérifier (images rendues → MP4)
Assistant packagé (
scripts/) : assemblez vos images avecscripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png, puis vérifiez l'encodage avecscripts/probe-mp4.sh out.mp4 [LxH] [imgs/s]. Voirscripts/README.md.
Les sous-titres sont livrés sous forme de composition Remotion (<Composition> + schema zod + defaultProps) — tout le mouvement des mots est piloté par images via useCurrentFrame(), jamais Date.now() / Math.random() / minuteurs. Livrable = out/*.mp4 (incrusté) + le projet + le fichier SRT/VTT annexe. Le format vertical 9:16 (1080×1920) est le défaut.
Boucle de vérification — rendu d'images → inspection → encodage. Le timing des mots est ce qui casse ; vérifiez-le à des images précises avant de lancer un encodage.
# Images au début / une image échantillon de mot actif / fin — AVEC LES PROPS LIVRÉES (jetons réels + audio)
npx remotion still Captions out/f-start.png --frame=0 --props='{"captionsSrc":"vo.json"}'
npx remotion still Captions out/f-mid.png --frame=90 --props='{"captionsSrc":"vo.json"}'
npx remotion still Captions out/f-end.png --frame=N --props='{"captionsSrc":"vo.json"}' # N = durationInFrames-1
# Inspectez chaque PNG :
# - le mot en surbrillance à l'image 90 est le mot dont [startMs,endMs] contient 90/fps (pas de dérive)
# - burn-in lisible : sans sérif gras, contour+ombre tient, pas de coupure
# - zone de sécurité 9:16 : le sous-titre se trouve dans la bande centrale, hors des ~12% supérieurs et ~20-35% inférieurs (sous-titres/CTA/audio UI) et du rail d'action droit
npx remotion render Captions out/captions.mp4 --props='{"captionsSrc":"vo.json"}' # encoder une fois que les images sont bonnes
npx remotion render Captions out/demo.gif --codec=gif # clip de preuve README
Utilisez npx remotion compositions pour lire durationInFrames/fps et choisir les images du mot actif et de fin.
Avant de terminer :
- Les images se rendent proprement à l'image 0, une image intermédiaire de mot actif, et la dernière — pas de police/audio manquant.
- Le mot correct est en surbrillance à l'image échantillonnée (image/fps se trouve à l'intérieur de son jeton) ; pas de simulation par division uniforme.
- Le burn-in est lisible (contour+ombre) et le sous-titre est entièrement dans la zone de sécurité 9:16 à chaque image vérifiée.
- Piloté uniquement par images — pas de
Date.now()/Math.random()/ minuteurs. - Les props livrées (jetons réels, pas seulement
defaultProps) s'affichent correctement ; MP4 + fichier SRT/VTT annexe émis, GIF optionnel.
Fichiers de référence
references/word-timed-captions.md— construction de bout en bout : transcription Whisper et le typeCaption, un analyseur SRT complet, un composant Remotion avec timing mot par mot et surbrillance active, pagination manuelle, un émetteur SRT/VTT, des cartes de zones de sécurité par plateforme, et une fiche technique de typographie lisible.