Incruster des sous-titres dans une vidéo 1080×1920 pour TikTok ou Reels part en général d'un plan raisonnable : le fil recouvre à peu près le cinquième inférieur de l'image, donc il faut garder le sous-titre au-dessus. Avec le filtre subtitles de ffmpeg, on passe par force_style et on règle MarginV. Puis on lance le rendu, et le sous-titre se retrouve ailleurs, et bien plus gros que prévu.
La raison, c'est qu'un fichier .srt ne sait pas quelle taille fait votre vidéo. ffmpeg le convertit en script ASS avec un canevas fixe et minuscule, et chaque nombre de force_style est lu dans les unités de ce canevas. Vous trouverez ci-dessous des mesures sur un vrai rendu 1080×1920, et un force_style qui place le sous-titre exactement là où le calcul l'attend.
Le canevas 384×288 derrière chaque .srt
Convertissez n'importe quel .srt avec ffmpeg et regardez l'en-tête :
ffmpeg -i one.srt one.ass && head -12 one.ass
[Script Info]
; Script generated by FFmpeg/Lavc61.19.100
ScriptType: v4.00+
PlayResX: 384
PlayResY: 288
ScaledBorderAndShadow: yes
...
Style: Default,Arial,16,&Hffffff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,1
PlayResX: 384 et PlayResY: 288 définissent le système de coordonnées du script, un petit canevas 4:3. libass le met à l'échelle de l'image sur laquelle il dessine : les valeurs verticales par 1920 / 288 = 6.67, les horizontales par 1080 / 384 = 2.81. MarginV, Fontsize, Outline, MarginL et MarginR dans force_style sont tous exprimés dans ces pixels de script.
Donc MarginV=28, ce n'est ni « 28 pixels » ni « 28 % de la hauteur ». Sur une image de 1920 pixels, cela fait environ 187 pixels. Fontsize=22 fait environ 147 pixels. J'ai vérifié le second directement : un H majuscule avec Fontsize=22 et avec PlayResY=1920,Fontsize=146.67 couvre exactement les mêmes pixels (lignes 1258 à 1351).
Mesurer où tombe le texte
Pour avoir des chiffres plutôt que des impressions, on rend un sous-titre sur une image grise unie de 1080×1920, on relit l'image en octets de gris bruts et on cherche les pixels blancs. La bibliothèque standard de Python suffit :
import subprocess, sys
FF = sys.argv[1] # path to an ffmpeg built with libass
W, H = 1080, 1920
def text_box(style, srt="one.srt"):
"""Burn one caption onto a grey 1080x1920 frame and return where the white text landed."""
vf = f"subtitles={srt}:force_style='{style}',format=gray"
raw = subprocess.run(
[FF, "-v", "error", "-f", "lavfi", "-i", f"color=c=0x303030:s={W}x{H}:d=2",
"-vf", vf, "-ss", "1", "-frames:v", "1", "-f", "rawvideo", "-"],
capture_output=True, check=True).stdout
pts = [(i // W, i % W) for i, v in enumerate(raw) if v > 200]
rows = sorted({y for y, _ in pts})
lines = 1 + sum(1 for a, b in zip(rows, rows[1:]) if b - a > 20)
xs = [x for _, x in pts]
return rows[0], rows[-1], min(xs), max(xs), lines
Le sous-titre de one.srt est une vraie ligne de 38 caractères tirée d'une transcription, "the Saharan dust cloud that we've seen", affichée de 0 à 2 secondes. Le seuil de 200 capte le remplissage blanc et ignore le contour noir ; un écart de plus de 20 lignes vides compte comme une nouvelle ligne de texte.
Les résultats
La cible est celle qu'utilise l'éditeur vidéo d'osFoundry pour le 9:16 : les 20 % du bas de l'image sont couverts par le fil, et les sous-titres se placent à 72 % de la hauteur, ce qui, sur 1920 pixels, veut dire que le texte doit se terminer vers la ligne 1382.
| force_style | Haut du texte | Bas du texte | Lignes |
|---|---|---|---|
MarginV=10,Fontsize=22 |
1437 (74.8%) | 1826 (95.1%) | 3 |
MarginV=28,Fontsize=22 |
1317 (68.6%) | 1706 (88.9%) | 3 |
MarginV=81,Fontsize=22 |
964 (50.2%) | 1352 (70.4%) | 3 |
PlayResX=1080,PlayResY=1920,MarginV=538,Fontsize=80 |
1235 (64.3%) | 1366 (71.1%) | 2 |
Trois choses ressortent.
MarginV=28, le nombre qu'on obtient en traitant la marge comme un pourcentage (100 − 72), laisse la dernière ligne à 89 % de la hauteur, en plein dans la bande recouverte. MarginV=81 correspond aux mêmes 28 % exprimés en pixels de script (0.28 × 288), et le bord inférieur tombe bien à 70 %.
Corriger la marge seule ne suffit pas pour autant. Avec Fontsize=22, la ligne de 38 caractères devient trois lignes de texte de 147 pixels, et comme le sous-titre est ancré par le bas, les lignes supplémentaires poussent vers le haut. Le haut du sous-titre atteint 50 % de l'image, soit, sur un plan de face d'une personne qui parle, sa poitrine ou son visage.
Les côtés ont le même problème dans l'autre sens. Les valeurs par défaut MarginL=10 et MarginR=10 deviennent environ 28 pixels sur une image de 1080 de large (passer MarginL de 10 à 23 a décalé le texte de 37 pixels, soit 2.85 par unité), donc le texte renvoyé à la ligne peut déborder dans les bandes de 6 % où se trouvent les boutons j'aime et partager.
Travailler plutôt en pixels réels
Le correctif le plus simple consiste à dire à libass que le canevas du script, c'est la vidéo. PlayResX et PlayResY sont des champs de la section d'informations du script, mais libass les accepte aussi dans force_style, et alors chaque marge et chaque taille s'expriment en pixels de l'image :
def safe_style(top=0.08, bottom=0.20, sides=0.06, baseline=0.72, size=80):
"""force_style that works in real pixels, with the caption sitting on `baseline`."""
return ",".join([
f"PlayResX={W}", f"PlayResY={H}",
"Alignment=2",
f"MarginV={round((1 - baseline) * H)}",
f"MarginL={round(sides * W) + 15}", f"MarginR={round(sides * W) + 15}",
f"Fontsize={size}", "Outline=5",
])
On obtient PlayResX=1080,PlayResY=1920,Alignment=2,MarginV=538,MarginL=80,MarginR=80,Fontsize=80,Outline=5, la dernière ligne du tableau : deux lignes, bord inférieur à 71.1 %, et le texte entre x = 200 et 877, bien à l'écart des deux bandes latérales de 65 pixels. Le retour à la ligne intelligent que libass applique par défaut coupe la ligne en deux lignes de largeur proche, au lieu d'une longue et d'une courte.
En ligne de commande :
ffmpeg -i in.mp4 -vf "subtitles=captions.srt:force_style='PlayResX=1080,PlayResY=1920,Alignment=2,MarginV=538,MarginL=80,MarginR=80,Fontsize=80,Outline=5'" -c:a copy out.mp4
Un détail quand on fait le calcul : MarginV se mesure jusqu'au bas de la boîte de ligne, qui inclut la place des jambages descendants, et non jusqu'à la ligne de base. Avec MarginV=540, le bas d'un H majuscule se trouvait à la ligne 1351, environ 29 pixels au-dessus de 1920 − 540 = 1380. Si quelques pixels comptent, mesurez votre propre police plutôt que de vous fier au nombre.
Si vous préférez ne pas dépendre de force_style, écrire vous-même le fichier .ass avec PlayResX: 1080 et PlayResY: 1920 dans l'en-tête donne le même résultat et permet de styler chaque ligne séparément.
Ce que coûte l'incrustation
Sur un clip 1080×1920 de 32 secondes avec 13 sous-titres, l'incrustation avec le style ci-dessus a pris 13.2 secondes sur un Mac M1 avec ffmpeg 7.1 et les réglages par défaut de libx264. Réencoder le même clip sans filtre de sous-titres a pris 13.3 secondes. Le dessin du texte se perd dans le bruit ; tout le coût vient de l'encodage.
Quelle bande viser
Les valeurs de zone sûre ne relèvent pas d'une norme unique. Celles ci-dessus sont propres à l'éditeur d'osFoundry (SAFE_AREAS dans son code de canevas : en 9:16, 8 % en haut, 20 % en bas, 6 % sur les côtés), et elles décrivent ce qu'un fil affiche par-dessus une publication ordinaire. Les plateformes publient des recommandations surtout pour les publicités :
| Emplacement | Zone à laisser libre | Source |
|---|---|---|
| Publicités Instagram Reels | 14 % en haut, 35 % en bas, 6 % sur les côtés | Meta Ads Guide, spécifications vidéo Reels |
| Publicités in-feed TikTok | dépend de la longueur du texte de l'annonce ; fichiers de zone sûre téléchargeables, avec des versions distinctes pour les régions qui lisent de droite à gauche et pour les annonces avec anchors | Centre d'aide TikTok Ads |
| Publicités YouTube Shorts | un modèle de zone sûre 1080×1920 ; les éléments superposés varient selon le format | Aide Google Ads |
| Éditeur osFoundry, 9:16 | 8 % en haut, 20 % en bas, 6 % sur les côtés | SAFE_AREAS |
Pour une publicité sur Reels, la même fonction avec bottom=0.35 et une ligne de base autour de 0.62 garde le sous-titre au-dessus de la bande de Meta. L'intérêt de calculer le style au lieu de le taper, c'est que changer de cible se résume à un argument.
L'app déduit la hauteur des sous-titres à partir de la zone sûre dans une petite fonction, dont le commentaire explique pourquoi :
/// Where captions sit, as a fraction of frame height from the top.
///
/// 🔴 Derived from the platform safe area, not from a fixed margin. On a vertical feed the
/// bottom fifth of the frame is under the platform's own caption and buttons, so a caption
/// placed at the conventional 90% is guaranteed to be covered — which is the whole reason C1
/// shipped the safe-area overlay.
pub fn caption_baseline(aspect: &str) -> f64 {
match aspect {
"9:16" => 0.72,
"1:1" => 0.84,
_ => 0.88,
}
}
L'application de bureau avec cet éditeur est gratuite sur la page de téléchargement d'osFoundry, et la page local-first explique comment osFoundry garde ce type de travail sur votre propre machine.

Top comments (0)