Communitygithub.com

字幕燒錄 (Subtitle burning)

使用真實的 ASS 字幕檔案和 ffmpeg 的 libass 渲染器(非 drawtext 繞道),將美觀、可讀的標題/字幕燒錄到影片中。當使用者要求為影片加入或燒錄 titlovi/字幕/標題、需要與語音同步的文字疊加,或要求更改現有字幕影片的字型/大小/位置/換行時(即使他們沒有明確說出「ASS」或「libass」),請使用此技能。

字幕燒錄 (Subtitle burning) 是什麼?

此技能提供了一種嚴謹的方法,用於使用 ASS(Advanced SubStation Alpha)字幕檔案和 ffmpeg 的 libass 渲染器,將高品質、樣式化的字幕燒錄到影片中。它解決了常見的痛點:標準的 Homebrew ffmpeg 缺少必要的字幕濾鏡;自動換行會產生不合邏輯的斷句;以及邊距不足導致字幕在 Instagram 等平台被裁剪。該技能強制採用一系列步驟:驗證 ffmpeg 建造環境、根據最終影片時間軸計算精確的時間碼、手動編寫帶有精確 `\N` 換行的 ASS 檔案、在渲染前透過靜止幀檢查驗證寬度和位置、設定頂部錨點以避免字幕跳動,以及在連續字幕之間保留短暫空白以提升可讀性。它還包括一個經實戰驗證的標點符號慣例(移除最後一行末尾的句號和逗號)、一個基於自然語音停頓斷行的指南,以及一個用於處理使用者回饋的疊代工作流程,允許直接編輯 .ass 檔案,並透過互動式計劃編輯器工具進行文字修正。支援的平台包括任何使用 ffmpeg 的桌面環境,特別是 macOS(使用 `ffmpeg-full`),並涵蓋 Instagram 等常見輸出目標。該技能是為 Claude Code、Cursor 和 Codex 等 AI 代理開發者設計的,為原本容易出錯的任務提供了一個結構化、可重複且高品質的解決方案。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Istok-Media-Tool/video-edit-skills/tree/HEAD/video-edit/skills/subtitle

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

字幕燒錄

為何需要此技能

為了讓字幕看起來真正正確,在實際專案中經歷了幾輪修正,每一輪都值得記下來,以免重蹈覆轍:

  1. 系統預設的 Homebrew ffmpeg 完全沒有 drawtext/subtitles 濾鏡(未以 libass/freetype 建置)——要燒錄真正的樣式化字幕需要不同的 ffmpeg 建置版本,而非繞道解決方案。
  2. 為了平衡字元數而強行斷行會產生無意義的結果——換行必須落在人們朗讀句子時自然會停頓的位置,絕不能在中間斷開。
  3. 靠近左右邊框邊緣的文字行可能會被 Instagram 自己的裁剪功能在發布時切掉——需要真正的邊距,而不僅僅是「符合畫框」。
  4. 字型大小和垂直位置因專案而異,值得確認,但在完整渲染前,無論如何都要先進行視覺驗證——靜止幀檢查能在幾秒內發現溢出/裁剪問題,而完整重新渲染則無法做到。
  5. 字幕不能在畫面之間移動。第一個版本將字幕錨定在底部(對齊方式 2,MarginV 500),因此 3 行字幕會比 2 行或 1 行字幕起始位置更高,而且重疊前一個字幕(僅差幾百分之一秒)的字幕會因為 libass 的碰撞避免機制而在整個持續時間內被往上推——使用者會看到標題「上下跳動」。修正規則:錨定到頂部(對齊方式 8,MarginV = 第一行的 y 座標;在 1080x1920 解析度下設為 1215,可將 3 行字幕的最低行保持在舊的底部錨點位置),這樣每個字幕的第一行都位於相同位置,額外的行數會向下延伸,並且絕不讓兩個 Dialogue 事件在時間上重疊。此外,在連續字幕之間留一個短暫的空白時刻:每個事件在其下一個事件開始前 0.12 秒(25fps 下為 3 幀)結束。這是使用者明確要求的——短暫的空白會告訴大腦字幕已更改;背靠背的字幕會被解讀為相同文字在閃爍。縮短前一個字幕的結束時間,切勿延遲下一個字幕的開始時間(開始時間需與語音同步)。
  6. 字幕文字不必是逐字稿——修正文法/措辭問題(聽錯的詞、彆扭的結構),並根據使用者的要求增刪詞語,就像編輯任何人都會閱讀的文字一樣。

第一步:確保 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 換行。然後對已編輯的畫面重新執行第六步的幀檢查(較長的單字可能會溢出邊距),並使用第七步重新燒錄。

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

相關技能