Animation de sous-titres
Transformez un script ou une voix off en sous-titres animés mot par mot, captivants et synchronisés : chaque mot apparaît sur la syllabe, le mot actif est mis en évidence, et le texte reste collé à la narration. Créez le genre de sous-titres qui retiennent l'attention sur TikTok, Reels et Shorts — lisibles, en rythme et dans la zone de sécurité.
Quand l'utiliser
- Sous-titres mot par mot ou karaoké sur des vidéos verticales courtes (9:16).
- Incrustation de sous-titres ouverts sur une piste voix off/narration à partir d'un script.
- Transformation d'une transcription Whisper/SRT en texte animé dans Remotion ou sur le web.
- Restylage de sous-titres automatiques en un dévoilement de type « Hormozi » et personnalisé.
Le pipeline
| Étape | Tâche | Outil |
|---|---|---|
| Transcrire | Audio → horodatages par mot | Whisper (@remotion/install-whisper-cpp), AssemblyAI |
| Normaliser | → tableau { text, startMs, endMs }[] | type Caption de @remotion/captions |
| Paginer | Grouper les mots en « pages » de 1 à 4 mots | createTikTokStyleCaptions() |
| Animer | Pop-in par mot + mise en évidence du mot actif | spring() Remotion / CSS |
| Placer | Zone de sécurité, typographie lisible | règles de disposition ci-dessous |
| Exporter | Incrustation (MP4) et/ou fichier SRT/VTT | rendu Remotion / émission de fichier |
Deux règles non négociables
- Temporalité par mot, pas par ligne. Le karaoké n'est magique que lorsque chaque mot tombe sur la syllabe. Transcrivez toujours avec des horodatages par mot ; ne les simulez jamais en divisant une ligne uniformément sur sa durée — le décalage est immédiatement visible.
- La lisibilité prime sur le style. Les sous-titres sont lus sur un téléphone, en plein soleil, en mode silencieux. Gras sans-serif, contour épais ou ombre, contraste élevé d'abord ; la décoration vient après. Un sous-titre que personne ne peut lire est une décoration, pas un sous-titre.
Synchronisation mot par mot à partir d'un script
Whisper produit directement des horodatages par mot. Normalisez chaque source en une forme de jeton plate unique afin que le moteur de rendu n'ait jamais à se soucier de l'origine des mots :
// Jeton de sous-titre — la forme unique que tout en 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 niveau phrase, retranscrivez pour obtenir des horodatages par mot — n'interpolez pas. Whisper medium.en est environ 2× plus rapide que large et suffisamment précis pour un audio vocal propre.
Pop-in 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 l'impression de « vie ».
import { useCurrentFrame, useVideoConfig, spring, interpolate } from "remotion";
const Mot: React.FC<{ jeton: Token; actif: boolean }> = ({ jeton, actif }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const entree = (jeton.startMs / 1000) * fps; // image d'entrée du mot
const p = spring({ frame: frame - entree, fps, config: { damping: 12, mass: 0.6 } });
const echelle = interpolate(p, [0, 1], [0.6, 1]); // pop de 60 % → 100 % (dépasse)
return (
<span style={{
display: "inline-block",
transform: `scale(${echelle})`,
opacity: interpolate(p, [0, 1], [0, 1]),
color: actif ? "#FFE45E" : "#FFFFFF", // mise en évidence du mot actif
transition: "color 80ms linear",
}}>{jeton.text} </span>
);
};
Calculez actif en testant frame par rapport à [startMs, endMs] de chaque jeton. Utilisez une seule couleur d'accent ou une « boîte » remplie derrière le mot en cours — n'animez jamais la couleur de tous les mots à la fois.
Pages, pas des murs de texte
Affichez 1 à 4 mots à la fois. Utilisez createTikTokStyleCaptions() de Remotion (ou groupez manuellement) et réglez combineTokensWithinMilliseconds : ~200–500 ms pour du mot par mot, ~1000–1500 ms pour de courtes phrases lisibles.
import { createTikTokStyleCaptions } from "@remotion/captions";
const { pages } = createTikTokStyleCaptions({
captions, combineTokensWithinMilliseconds: 1200, // ~2–3 mots par page
});
Pop-in Web/CSS (sans Remotion)
Pour un lecteur DOM/en direct, donnez à chaque mot son propre animation-delay égal à startMs :
.mot { 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 } }
.mot.actif { color:#FFE45E; }
mots.forEach(m => { const el = creer(m.texte); el.style.animationDelay = `${m.startMs}ms`; piste.append(el); });
Typographie lisible et placement dans la zone de sécurité (9:16, 1080×1920)
| Propriété | Valeur | Pourquoi |
|---|---|---|
| Police | Gras/Extra-Gras sans-serif (Montserrat, Inter, Helvetica) | Lisible sur les petits écrans chargés |
| Taille | 56–80 px (≈8 % de la hauteur du cadre), min 45 px | Lisible en mode silencieux sur un téléphone |
| Contour | 2–6 px noir uni | Résiste à tout arrière-plan |
| Ombre | Ombre portée douce en complément du contour | Séparation sur les plans lumineux |
| Casse | Majuscules ou phrase ; remplissage à fort contraste | Impact + scannabilité |
| Position verticale | Bande centrale, ~62–70 % vers le bas | Au-dessus de l'interface, en dessous de l'action |
| Sécurité basse | Rester libre en bas ~280 px / 15 % | Évite l'interface des sous-titres/CTA/audio |
| Sécurité latérale | Rester dans les 80 % de largeur centraux | Évite les icônes du rail droit |
Placez les sous-titres dans la bande centrale — pas en bas, là où se trouve l'interface de la plateforme (nom d'utilisateur, audio, boutons). Limitez le texte à 2 lignes maximum.
Synchronisation avec une voix off / narration
Le script contient déjà le timing exact de l'audio. Placez ce même audio sous la composition et conservez les horodatages dans la base de temps 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 manuellement les décalages.
Incrustation vs fichier SRT/VTT séparé
| Incrustation (ouvert) | SRT/VTT séparé (fermé) | |
|---|---|---|
| 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 suffit pas seul | Requis (commutable) |
| Meilleure pratique | Incruster les sous-titres animés pour le social | Fournir également un fichier séparé pour le web/SEO |
Émettez les deux : incrustez les sous-titres animés pour le social, et écrivez un SRT/VTT simple à partir des mêmes jetons pour une lecture accessible/SEO.
Liste de vérification de la sortie
- Horodatages par mot (réels, issus de la transcription) — pas de fausse division uniforme.
- Pop-in par mot avec micro-dépassement ; une couleur d'accent pour le mot actif.
- 1 à 4 mots par page ; jamais un mur de texte.
- Gras sans-serif, 56–80 px, contour 2–6 px + ombre, contraste élevé.
- Bande centrale, libre en bas ~280 px et des rails latéraux.
- Audio placé sous la composition ; horodatages dans la base de temps audio.
- Incrustation pour le social et fichier SRT/VTT séparé pour l'accessibilité.
Livrer et vérifier (images rendues → MP4)
Assistant packagé (
scripts/): disposez vos images avecscripts/contact-sheet.sh sheet.png f-debut.png f-milieu.png f-fin.png, puis vérifiez l'encodage avecscripts/probe-mp4.sh out.mp4 [LxH] [fps]. Voirscripts/README.md.
Les sous-titres sont livrés sous forme de composition Remotion (<Composition> + schéma zod + defaultProps) — tout mouvement de mot est piloté par l'image via useCurrentFrame(), jamais Date.now() / Math.random() / minuteurs. Livrable = out/*.mp4 (incrusté) + le projet + le fichier SRT/VTT séparé. Le format vertical 9:16 (1080×1920) est le format par défaut.
Boucle de vérification — rendre des images → inspecter → encoder. Le timing des mots est ce qui casse ; vérifiez-le sur des images exactes avant d'encoder.
# Images au début / à une image de mot actif échantillonnée / à la fin — AVEC LES PROPRIÉTÉS FOURNIES (jetons réels + audio)
npx remotion still Captions out/f-debut.png --frame=0 --props='{"captionsSrc":"vo.json"}'
npx remotion still Captions out/f-milieu.png --frame=90 --props='{"captionsSrc":"vo.json"}'
npx remotion still Captions out/f-fin.png --frame=N --props='{"captionsSrc":"vo.json"}' # N = durationInFrames-1
# Inspectez chaque PNG :
# - le mot mis en évidence à l'image 90 est le mot dont [startMs,endMs] contient 90/fps (aucun décalage)
# - incrustation lisible : gras sans-serif, contour+ombre tient, pas de coupure
# - zone de sécurité 9:16 : le sous-titre se trouve dans la bande centrale, libre du haut ~12 % et du bas ~20-35 % (interface sous-titres/CTA/audio) et du rail d'action droit
npx remotion render Captions out/sous-titres.mp4 --props='{"captionsSrc":"vo.json"}' # encoder une fois les images correctes
npx remotion render Captions out/demo.gif --codec=gif # clip de démonstration 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 de mot actif intermédiaire et à la dernière — pas de police/audio manquant.
- Le mot correct est mis en évidence à l'image échantillonnée (image/fps se trouve dans son jeton) ; pas de fausse division uniforme.
- L'incrustation 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 l'image — pas de
Date.now()/Math.random()/ minuteurs. - Les propriétés fournies (jetons réels, pas seulement
defaultProps) s'affichent correctement ; MP4 + fichier SRT/VTT séparé é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 mise en évidence du mot actif, pagination manuelle, un émetteur SRT/VTT, des cartes de zones de sécurité par plateforme et une fiche technique de typographie lisible.