TikTok या Reels के लिए 1080×1920 वीडियो में कैप्शन burn करते समय शुरुआत आमतौर पर एक समझदारी भरे प्लान से होती है: फ़ीड फ्रेम के नीचे का करीब पाँचवाँ हिस्सा ढक लेती है, तो कैप्शन उसके ऊपर रखो। ffmpeg के subtitles फ़िल्टर के साथ आप force_style लेते हैं और MarginV सेट करते हैं। फिर रेंडर करते हैं, और कैप्शन किसी बिल्कुल दूसरी जगह होता है, और उम्मीद से कहीं बड़ा।
वजह यह है कि .srt फ़ाइल को पता ही नहीं होता कि आपका वीडियो कितना बड़ा है। ffmpeg उसे एक तय और बहुत छोटे canvas वाली ASS script में बदल देता है, और force_style का हर नंबर उसी canvas की यूनिट में पढ़ा जाता है। नीचे एक असली 1080×1920 रेंडर पर लिए गए माप हैं, और एक force_style जो कैप्शन को ठीक वहीं रखता है जहाँ हिसाब कहता है।
हर .srt के पीछे का 384×288 canvas
कोई भी .srt ffmpeg से कन्वर्ट करें और header देखें:
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 और PlayResY: 288 script का coordinate system हैं, यानी एक छोटा 4:3 canvas। libass इसे उस फ्रेम पर स्केल करता है जिस पर वह रेंडर कर रहा है: वर्टिकल वैल्यू 1920 / 288 = 6.67 से, और हॉरिज़ॉन्टल वैल्यू 1080 / 384 = 2.81 से। force_style में MarginV, Fontsize, Outline, MarginL और MarginR सब इन्हीं script पिक्सल में होते हैं।
इसलिए MarginV=28 का मतलब न "28 पिक्सल" है, न "ऊँचाई का 28%"। 1920 पिक्सल के फ्रेम पर यह करीब 187 पिक्सल है। Fontsize=22 करीब 147 पिक्सल है। दूसरी बात मैंने सीधे जाँची: Fontsize=22 पर और PlayResY=1920,Fontsize=146.67 पर बड़ा अक्षर H बिल्कुल एक जैसे पिक्सल घेरता है (row 1258 से 1351 तक)।
टेक्स्ट असल में कहाँ आता है, यह मापना
अंदाज़े की जगह नंबर पाने के लिए, एक कैप्शन को सादे ग्रे 1080×1920 फ्रेम पर रेंडर करें, फ्रेम को raw ग्रे bytes के रूप में वापस पढ़ें, और सफ़ेद पिक्सल ढूँढें। इसके लिए Python की standard library काफ़ी है:
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
one.srt में कैप्शन एक असली ट्रांसक्रिप्ट की 38 कैरेक्टर की लाइन है, "the Saharan dust cloud that we've seen", जो 0 से 2 सेकंड तक दिखती है। 200 का threshold सफ़ेद fill को पकड़ता है और काली outline को छोड़ देता है; 20 से ज़्यादा खाली rows का गैप टेक्स्ट की नई लाइन गिना जाता है।
नतीजे
टारगेट वही है जो osFoundry वीडियो एडिटर 9:16 के लिए इस्तेमाल करता है: फ्रेम का नीचे का 20% फ़ीड से ढका होता है, और कैप्शन ऊपर से 72% पर बैठते हैं, यानी 1920 पिक्सल पर टेक्स्ट को लगभग row 1382 के आसपास खत्म होना चाहिए।
| force_style | टेक्स्ट का ऊपरी किनारा | टेक्स्ट का निचला किनारा | लाइनें |
|---|---|---|---|
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 |
इससे तीन बातें दिखती हैं।
MarginV=28, यानी वह नंबर जो margin को प्रतिशत मानने पर मिलता है (100 − 72), आखिरी लाइन को ऊँचाई के 89% पर छोड़ देता है, ढके हुए हिस्से के काफ़ी अंदर। MarginV=81 वही 28% है, script पिक्सल में (0.28 × 288), और इसमें निचला किनारा सच में 70% पर आता है।
लेकिन सिर्फ़ margin ठीक करना काफ़ी नहीं है। Fontsize=22 पर 38 कैरेक्टर की लाइन 147 पिक्सल ऊँचे टेक्स्ट की तीन लाइनें बन जाती है, और क्योंकि कैप्शन नीचे से anchored है, एक्स्ट्रा लाइनें ऊपर की ओर बढ़ती हैं। कैप्शन का ऊपरी किनारा फ्रेम के 50% तक पहुँच जाता है, जो talking-head क्लिप में बोलने वाले की छाती या चेहरा होता है।
साइड में भी यही दिक्कत है, बस उल्टी दिशा में। डिफ़ॉल्ट MarginL=10 और MarginR=10 1080 चौड़े फ्रेम पर करीब 28 पिक्सल बनते हैं (MarginL को 10 से 23 करने पर टेक्स्ट 37 पिक्सल खिसका, यानी 2.85 प्रति यूनिट), इसलिए wrap हुआ टेक्स्ट उन 6% पट्टियों में जा सकता है जहाँ like और share बटन होते हैं।
इसकी जगह असली पिक्सल में काम करें
सबसे आसान हल यह है कि libass को बता दें कि script का canvas ही वीडियो है। PlayResX और PlayResY script-info फ़ील्ड हैं, लेकिन libass इन्हें force_style में भी मान लेता है, और तब हर margin और साइज़ फ्रेम के पिक्सल में होता है:
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",
])
इससे PlayResX=1080,PlayResY=1920,Alignment=2,MarginV=538,MarginL=80,MarginR=80,Fontsize=80,Outline=5 बनता है, जो टेबल की आखिरी row है: दो लाइनें, निचला किनारा 71.1% पर, और टेक्स्ट x = 200 से 877 के बीच, दोनों 65 पिक्सल वाली साइड पट्टियों से काफ़ी दूर। libass की डिफ़ॉल्ट smart wrapping लाइन को एक लंबी और एक छोटी के बजाय मिलती-जुलती चौड़ाई की दो लाइनों में बाँटती है।
कमांड लाइन पर:
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
हिसाब लगाते समय एक बारीकी: MarginV baseline तक नहीं, बल्कि लाइन बॉक्स के निचले हिस्से तक मापता है, जिसमें descenders की जगह भी शामिल होती है। MarginV=540 पर बड़े H का निचला हिस्सा row 1351 पर था, 1920 − 540 = 1380 से करीब 29 पिक्सल ऊपर। अगर कुछ पिक्सल मायने रखते हैं, तो नंबर पर भरोसा करने के बजाय अपना फ़ॉन्ट खुद मापें।
अगर आप force_style पर निर्भर नहीं रहना चाहते, तो header में PlayResX: 1080 और PlayResY: 1920 के साथ .ass फ़ाइल खुद लिखने से भी यही नतीजा मिलता है, और आप हर लाइन को अलग स्टाइल दे सकते हैं।
burn करने में कितना समय लगता है
13 कैप्शन वाली 32 सेकंड की 1080×1920 क्लिप पर, ऊपर वाले स्टाइल से burn करने में M1 Mac पर ffmpeg 7.1 और डिफ़ॉल्ट libx264 सेटिंग्स के साथ 13.2 सेकंड लगे। उसी क्लिप को बिना subtitle फ़िल्टर के दोबारा encode करने में 13.3 सेकंड लगे। टेक्स्ट बनाने का समय noise में खो जाता है; पूरा खर्च encode का है।
किस हिस्से को टारगेट करें
safe-area के नंबर कोई एक स्टैंडर्ड नहीं हैं। ऊपर के आँकड़े osFoundry एडिटर के अपने हैं (उसके canvas कोड में SAFE_AREAS: 9:16 के लिए ऊपर 8%, नीचे 20%, साइड 6%), और ये बताते हैं कि फ़ीड एक आम पोस्ट के ऊपर क्या बनाती है। प्लेटफ़ॉर्म जो गाइडेंस पब्लिश करते हैं, वह ज़्यादातर ऐड के लिए है:
| प्लेसमेंट | खाली रखें | सोर्स |
|---|---|---|
| Instagram Reels ऐड | ऊपर 14%, नीचे 35%, साइड 6% | Meta Ads Guide, Reels वीडियो specs |
| TikTok in-feed ऐड | ऐड कैप्शन की लंबाई पर निर्भर; डाउनलोड होने वाली safe-zone फ़ाइलें, right-to-left क्षेत्रों और anchors वाले ऐड के लिए अलग | TikTok Ads हेल्प सेंटर |
| YouTube Shorts ऐड | 1080×1920 safe-zone टेम्पलेट; overlays फ़ॉर्मेट के हिसाब से बदलते हैं | Google Ads हेल्प |
| osFoundry एडिटर, 9:16 | ऊपर 8%, नीचे 20%, साइड 6% | SAFE_AREAS |
Reels पर ऐड के लिए, वही फ़ंक्शन bottom=0.35 और करीब 0.62 के baseline के साथ कैप्शन को Meta के हिस्से से ऊपर रखता है। स्टाइल टाइप करने के बजाय उसे कैलकुलेट करने का फ़ायदा यही है कि टारगेट बदलना बस एक argument बदलना है।
ऐप कैप्शन की ऊँचाई safe area से एक छोटे फ़ंक्शन में निकालता है, और उसका कमेंट इसकी वजह बताता है:
/// 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,
}
}
यह एडिटर वाला डेस्कटॉप ऐप osFoundry के डाउनलोड पेज से मुफ़्त है, और osFoundry इस तरह का काम आपकी अपनी मशीन पर कैसे रखता है, इसके बारे में और जानकारी उसके local-first पेज पर है।

Top comments (0)