字幕燒錄
為何需要此技能
為了讓字幕看起來真正正確,在實際專案中經歷了幾輪修正,每一輪都值得記下來,以免重蹈覆轍:
- 系統預設的 Homebrew
ffmpeg完全沒有drawtext/subtitles濾鏡(未以 libass/freetype 建置)——要燒錄真正的樣式化字幕需要不同的 ffmpeg 建置版本,而非繞道解決方案。 - 為了平衡字元數而強行斷行會產生無意義的結果——換行必須落在人們朗讀句子時自然會停頓的位置,絕不能在中間斷開。
- 靠近左右邊框邊緣的文字行可能會被 Instagram 自己的裁剪功能在發布時切掉——需要真正的邊距,而不僅僅是「符合畫框」。
- 字型大小和垂直位置因專案而異,值得確認,但在完整渲染前,無論如何都要先進行視覺驗證——靜止幀檢查能在幾秒內發現溢出/裁剪問題,而完整重新渲染則無法做到。
- 字幕不能在畫面之間移動。第一個版本將字幕錨定在底部(對齊方式 2,MarginV 500),因此 3 行字幕會比 2 行或 1 行字幕起始位置更高,而且重疊前一個字幕(僅差幾百分之一秒)的字幕會因為 libass 的碰撞避免機制而在整個持續時間內被往上推——使用者會看到標題「上下跳動」。修正規則:錨定到頂部(對齊方式 8,MarginV = 第一行的 y 座標;在 1080x1920 解析度下設為 1215,可將 3 行字幕的最低行保持在舊的底部錨點位置),這樣每個字幕的第一行都位於相同位置,額外的行數會向下延伸,並且絕不讓兩個 Dialogue 事件在時間上重疊。此外,在連續字幕之間留一個短暫的空白時刻:每個事件在其下一個事件開始前 0.12 秒(25fps 下為 3 幀)結束。這是使用者明確要求的——短暫的空白會告訴大腦字幕已更改;背靠背的字幕會被解讀為相同文字在閃爍。縮短前一個字幕的結束時間,切勿延遲下一個字幕的開始時間(開始時間需與語音同步)。
- 字幕文字不必是逐字稿——修正文法/措辭問題(聽錯的詞、彆扭的結構),並根據使用者的要求增刪詞語,就像編輯任何人都會閱讀的文字一樣。
第一步:確保 ffmpeg 能夠燒錄字幕
ffmpeg -hide_banner -filters 2>&1 | grep -E "subtitles|^\s*\S*\s+ass\b"
如果沒有匹配項,則常規的 ffmpeg 無法執行此操作。請安裝完整建置版本:
brew install ffmpeg-full
ffmpeg-full 是 keg-only(不同時鏈接到 PATH)——在字幕燒錄步驟中應明確使用它:/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg。
實際發生過的陷阱: 安裝 ffmpeg-full 會拉入一個較新的共享 x265,並破壞 PATH 上的常規 ffmpeg/ffprobe(出現 dyld "Library not loaded: libx265.*.dylib" 錯誤),因為它們原本連結的是舊版本。安裝 ffmpeg-full 後,立即執行:
ffmpeg -version && ffprobe -version
如果任一指令報錯,請使用 brew reinstall ffmpeg(常規配方,而非 -full)修復,並在繼續之前重新驗證兩個指令均能正常執行——不要假設常規 ffmpeg 在安裝過程中完好無損。
第二步:獲取精確的字幕文字和時間碼
如果影片來自基礎技能的剪輯工作流程,您已經知道每個片段的確切保留文字及其原始來源時間戳——但一旦片段被剪輯並拼接,時間碼就會重置為新影片自己的時間軸。透過依序累加實際渲染的剪輯片段時長,計算每個片段在最終影片中的位置:
ffprobe -v error -show_entries format=duration -of csv=p=0 clip_01.mp4
……並累加。在每個片段的時間視窗內,稍微從剪輯自身的開始/結束位置向內偏移,以匹配語音實際開始/停止的位置(如果剪輯在裁剪步驟中帶有前/後填充,則將其減去),這樣字幕就不會在靜音期間出現/消失。
第三步:編寫 ASS 檔案
使用此為起始範本——這是在實際專案中確認的確切樣式(Inter Bold, 白色文字, 不透明黑色背景框, 中下位置, 寬敞的側邊距):
[Script Info]
Title: <video name> - titlovi
ScriptType: v4.00+
WrapStyle: 2
ScaledBorderAndShadow: yes
PlayResX: 1080
PlayResY: 1920
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Inter,70,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,-1,0,0,0,100,100,0,0,3,16,0,8,120,120,1215,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.15,0:00:05.93,Default,,0,0,0,,Zamisli univerzum koji postoji\Ntoliko dugo da su sve zvezde\Nodavno ugašene
在燒錄之前,讓每個事件在下一個事件開始前 0.12 秒結束(將較早事件的 End 設為下一個 Start - 0.12;切勿移動 Start)——參見經驗教訓 5。
將 PlayResX/PlayResY 調整為符合實際影片解析度(此範例為 1080x1920 垂直解析度)。WrapStyle: 2 表示 libass 不會自動換行——每個換行都必須是 Text 欄位中的顯式 \N。這是故意的:它強制您審慎選擇每個換行點(參見第四步),而不是讓渲染器猜測,同時也能讓超出邊框的行表現為裁剪邊緣,而不是悄悄增加額外行數。
字型/大小/顏色/位置都是起始的預設值,而非固定規則——如果這是新專案,請與使用者確認;如果您已知道該使用者的先前設定,則重複使用他們上次確認的值。
在實際專案中確認的標點符號慣例
字幕畫面最後一行末尾的句號和逗號會被刪除(字幕不需要像散文那樣有句號——在該字型大小下會顯得多餘)。即使句子在下一幀字幕畫面繼續,此規則也適用:畫面切換本身已足夠解讀為停頓,因此「svoje znanje kroz praksu,」會顯示為「svoje znanje kroz praksu」。這是使用者明確要求的(2026-11-05,「如同句號一樣」)。問號和驚嘆號則保留。畫面內部的逗號(行中間,或非畫面最後一行的行尾)則保留。預設情況下對每個影片都應用此規則,包括雙語字幕中的第二語言行,以及使用者在計劃編輯器中編輯的字幕文字。
第四步:在人們實際說話會停頓的地方進行換行
這是最需要做對、也最容易偷懶做錯的部分。朗讀句子,在自然的呼吸/短語邊界處斷行——子句邊界、完整想法之後、代表實際口語停頓的逗號處。絕不要因為一行「需要」變短就在自然斷點之外的其他地方斷行——如果自然斷點在其他地方,請尋找一個更短的自然斷點(較長的句子通常有不止一個有效的停頓點),不要僅僅為了達到寬度目標而強行進行不自然的斷行。
來自實際修正的具體範例——錯誤(根據字元數平衡,中間斷開):
Zamisli univerzum koji postoji toliko
dugo da su sve zvezde odavno ugasene
正確(在自然的語音停頓處斷行):
Zamisli univerzum koji postoji
toliko dugo da su sve zvezde
odavno ugašene
1、2 或 3 行都可以——沒有固定的最大行數。重要的是每個斷行都是說話者會實際停頓的地方,並且每一行都能容納在邊距內(下一步)。
第五步:根據實際的側邊距(而非原始畫面寬度)驗證寬度
樣式中的 MarginL/MarginR(在上述範本中,1080 寬度的畫面上各為 120px——約每側 11%)之所以存在,是因為影片目標平台(Instagram 等)在上傳後會進行自己的輕微裁剪,而太靠近原始邊緣的文字不僅會被畫面邊界裁剪,還可能被平台裁剪。在判斷一行是否太長時,應將 PlayResX - MarginL - MarginR 視為實際可用寬度,而非整個畫面寬度。
由於 WrapStyle: 2 禁用了自動換行,因此實際上太寬的行不會自動換行——它會溢出邊距,看起來像是被裁剪/截斷的文字。在第六步中捕捉此問題,以免最終出現在完整渲染中。
第六步:在燒錄整個影片之前渲染測試幀
選取 2-3 個最長/風險最高的字幕中的時間戳,僅渲染該幀:
/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg -nostdin -y -i video.mp4 -ss <t> -update 1 \
-vf "subtitles=captions.ass" -frames:v 1 check.png -loglevel error
檢查每一幀:文字是否溢出邊距、行數是否超出預期(表示行寬超出預期,需要調整上游的措辭/換行),以及垂直位置是否確實看起來像「中下」而非太靠近最底部邊緣或高到遮擋臉部。修正並重新檢查,然後再繼續——這個循環非常快(每幀只需幾秒),並且能準確捕捉到完整渲染本會浪費數分鐘才能再現的錯誤。
第七步:燒錄最終影片
/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg -nostdin -y -i video.mp4 \
-vf "subtitles=captions.ass" -c:v libx264 -preset medium -crf 18 -pix_fmt yuv420p \
-c:a copy output_titlovi.mp4 -loglevel error
音訊保持不變(-c:a copy),因為字幕僅影響影片。如果輸出需要通過有檔案大小限制的工具傳送,則製作一個二次壓縮的副本,而不是壓縮主要版本——模式與基礎和縮放技能相同:
ffmpeg -nostdin -y -i output_titlovi.mp4 -c:v libx264 -preset slow -crf 24 -pix_fmt yuv420p -c:a aac -b:a 128k output_titlovi_share.mp4 -loglevel error
根據回饋進行疊代
字幕工作通常會經歷幾輪小而具體的修正(字型大小、位置、錯誤的單字、標點符號)。每一輪應僅修改變更的部分——直接編輯 .ass 檔案(純文字),對受影響的部分重新執行第六步的快速幀檢查,然後使用第七步重新燒錄。除非底層影片本身發生了變化,否則無需重複第一步到第二步。
透過互動式工具修正錯誤的單字
使用者傾向於在同一個計劃編輯器工具(plan-editor 技能——使用者的標準編輯器)中自行修正字幕文字:使用「Izmeni tekst」切換開關,每個句子都會變成可即時編輯。每當存在字幕時,使用 --ass captions.ass --durations ...(參見 plan-editor 技能)顯示該工具,而不是要求以文字形式進行修正——即使僅需要檢查字幕且未計劃縮放時也是如此(那麼只需將每個剪輯片段以廣角起始,並保持縮放計劃不變)。
回覆中會攜帶 CAPTIONS_JSON: {"<dialogue index>": "line 1\nline 2"},其中僅包含已更改的畫面。套用方法如下:
python3 scripts/apply_caption_edits.py captions.ass caption_edits.json captions_fixed.ass
它僅更改文字(時間碼和樣式保持不變),並將每一行轉換為顯式的 \N 換行。然後對已編輯的畫面重新執行第六步的幀檢查(較長的單字可能會溢出邊距),並使用第七步重新燒錄。