Imprimere i sottotitoli in un video 1080×1920 per TikTok o Reels di solito parte da un piano ragionevole: il feed copre più o meno il quinto inferiore dell'inquadratura, quindi il sottotitolo va tenuto sopra. Con il filtro subtitles di ffmpeg si usa force_style e si imposta MarginV. Poi si fa il render, e il sottotitolo finisce da tutt'altra parte, ed è molto più grande del previsto.
Il motivo è che un file .srt non sa quanto è grande il tuo video. ffmpeg lo converte in uno script ASS con una tela fissa e minuscola, e ogni numero in force_style viene letto nelle unità di quella tela. Qui sotto ci sono misure su un render reale 1080×1920 e un force_style che mette il sottotitolo esattamente dove dicono i conti.
La tela 384×288 dietro ogni .srt
Converti un .srt qualsiasi con ffmpeg e guarda l'intestazione:
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 e PlayResY: 288 sono il sistema di coordinate dello script, una piccola tela 4:3. libass la scala sul fotogramma su cui disegna: i valori verticali per 1920 / 288 = 6.67, quelli orizzontali per 1080 / 384 = 2.81. MarginV, Fontsize, Outline, MarginL e MarginR in force_style sono tutti espressi in questi pixel dello script.
Quindi MarginV=28 non vuol dire "28 pixel" e nemmeno "il 28% dell'altezza". Su un fotogramma da 1920 pixel sono circa 187 pixel. Fontsize=22 sono circa 147 pixel. Il secondo l'ho verificato direttamente: una H maiuscola con Fontsize=22 e con PlayResY=1920,Fontsize=146.67 occupa esattamente gli stessi pixel (righe da 1258 a 1351).
Misurare dove finisce il testo
Per avere numeri invece di impressioni, si renderizza un sottotitolo su un fotogramma grigio uniforme 1080×1920, si rilegge il fotogramma come byte grezzi in scala di grigi e si cercano i pixel bianchi. Basta la libreria standard di Python:
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 Il sottotitolo in one.srt è una vera riga di 38 caratteri presa da una trascrizione, "the Saharan dust cloud that we've seen", mostrata da 0 a 2 secondi. La soglia di 200 cattura il riempimento bianco e ignora il contorno nero; uno stacco di più di 20 righe vuote conta come una nuova riga di testo.
I risultati
L'obiettivo è quello che usa l'editor video di osFoundry per il 9:16: il 20% inferiore dell'inquadratura è coperto dal feed e i sottotitoli stanno al 72% dell'altezza, il che su 1920 pixel significa che il testo dovrebbe finire intorno alla riga 1382.
| force_style | Bordo superiore del testo | Bordo inferiore del testo | Righe |
|---|---|---|---|
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 |
Saltano all'occhio tre cose.
MarginV=28, il numero che viene fuori se tratti il margine come una percentuale (100 − 72), lascia l'ultima riga all'89% dell'altezza, in piena fascia coperta. MarginV=81 è lo stesso 28% espresso in pixel dello script (0.28 × 288), e il bordo inferiore cade davvero al 70%.
Sistemare solo il margine però non basta. Con Fontsize=22 la riga di 38 caratteri diventa tre righe di testo da 147 pixel, e siccome il sottotitolo è ancorato in basso, le righe in più crescono verso l'alto. La parte alta del sottotitolo arriva al 50% dell'inquadratura, che in una clip con una persona che parla in camera vuol dire il petto o la faccia.
Ai lati c'è lo stesso problema nella direzione opposta. I valori predefiniti MarginL=10 e MarginR=10 diventano circa 28 pixel su un fotogramma largo 1080 (portare MarginL da 10 a 23 ha spostato il testo di 37 pixel, 2.85 per unità), quindi il testo andato a capo può finire nelle fasce del 6% dove ci sono i pulsanti mi piace e condividi.
Meglio lavorare in pixel reali
La soluzione più semplice è dire a libass che la tela dello script è il video stesso. PlayResX e PlayResY sono campi della sezione di informazioni dello script, ma libass li accetta anche in force_style, e a quel punto ogni margine e ogni dimensione sono in pixel del fotogramma:
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", ]) Il risultato è PlayResX=1080,PlayResY=1920,Alignment=2,MarginV=538,MarginL=80,MarginR=80,Fontsize=80,Outline=5, l'ultima riga della tabella: due righe, bordo inferiore al 71.1% e il testo tra x = 200 e 877, ben lontano da entrambe le fasce laterali da 65 pixel. L'a capo intelligente che libass usa di default divide la riga in due di larghezza simile invece che in una lunga e una corta.
Da riga di comando:
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 dettaglio quando fai i conti: MarginV misura fino al fondo del box della riga, che include lo spazio per i discendenti, non fino alla linea di base. Con MarginV=540 il fondo di una H maiuscola stava alla riga 1351, circa 29 pixel sopra 1920 − 540 = 1380. Se contano pochi pixel, misura il tuo font invece di fidarti del numero.
Se preferisci non dipendere da force_style, scrivere tu il file .ass con PlayResX: 1080 e PlayResY: 1920 nell'intestazione dà lo stesso risultato e ti permette di dare uno stile a ogni riga separatamente.
Quanto costa imprimerli
Su una clip 1080×1920 di 32 secondi con 13 sottotitoli, l'impressione con lo stile qui sopra ha richiesto 13.2 secondi su un Mac M1 con ffmpeg 7.1 e le impostazioni predefinite di libx264. Ricodificare la stessa clip senza filtro dei sottotitoli ha richiesto 13.3 secondi. Il disegno del testo si perde nel rumore; tutto il costo è nella codifica.
Quale fascia puntare
I valori delle aree sicure non seguono un unico standard. Quelli sopra sono dell'editor di osFoundry (SAFE_AREAS nel suo codice della tela: per il 9:16, 8% in alto, 20% in basso, 6% ai lati) e descrivono ciò che un feed disegna sopra un post normale. Le piattaforme pubblicano indicazioni soprattutto per gli annunci:
| Posizionamento | Da lasciare libero | Fonte |
|---|---|---|
| Annunci Instagram Reels | 14% in alto, 35% in basso, 6% ai lati | Meta Ads Guide, specifiche video per Reels |
| Annunci in-feed TikTok | dipende dalla lunghezza del testo dell'annuncio; file di area sicura scaricabili, con versioni separate per le regioni con scrittura da destra a sinistra e per gli annunci con anchor | Centro assistenza TikTok Ads |
| Annunci YouTube Shorts | un modello di area sicura 1080×1920; gli elementi sovrapposti variano in base al formato | Guida di Google Ads |
| Editor osFoundry, 9:16 | 8% in alto, 20% in basso, 6% ai lati | SAFE_AREAS |
Per un annuncio su Reels, la stessa funzione con bottom=0.35 e una linea di base intorno a 0.62 tiene il sottotitolo sopra la fascia di Meta. Il senso di calcolare lo stile invece di scriverlo a mano è che cambiare destinazione è questione di un argomento.
L'app ricava l'altezza dei sottotitoli dall'area sicura in una piccola funzione, che nel commento spiega il perché:
/// 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'app desktop con questo editor è gratuita dalla pagina di download di osFoundry, e su come osFoundry tiene questo tipo di lavoro sulla tua macchina c'è di più nella sua pagina local-first.




