ASS 編寫 / libass 燒錄 實戰手冊
1. 檔案頭模板(直接抄)
[Script Info]
ScriptType: v4.00+
PlayResX: 2560
PlayResY: 1080
WrapStyle: 2
ScaledBorderAndShadow: yes
YCbCr Matrix: TV.709
[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: X,Microsoft YaHei,100,&H00F4F1EA,&H00F4F1EA,&H00140F11,&HFF000000,0,0,0,0,100,100,0,0,1,0,0,5,30,30,30,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.39,0:00:03.75,X,,0,0,0,,{\an5\pos(500,300)\fnAnton\fs150\1c&H00F4F1EA&\bord3\3c&H00140F11&\frz-4\fscx118\fscy118\t(0,130,\fscx100\fscy100)\fad(100,100)}WALK
2. 四個致命格式坑(本項目全踩過)
- 時間戳必須是
H:MM:SS.cc- 對:
0:00:01.50、0:01:02.30 - 錯:
0:01.50(libass 報Bad timestamp,整句被丟棄 → 整個視頻沒字,但 ffmpeg 返回 0)
- 對:
- Style 行顏色結尾不能帶
&- 對:
&H00F4F1EA,&H00F4F1EA,&H00140F11,&HFF000000 - 錯:
&H00F4F1EA&,...→ Style 行解析失敗 → 同樣「成功但沒字」 - 事件內聯標籤裡的顏色要帶
&:\1c&H00F4F1EA&
- 對:
- 字體目錄
fontsdir用相對路徑 + 工作目錄- 在輸出目錄放
fonts/,cd到該目錄再ass=x.ass:fontsdir=fonts。 - 絕對路徑帶盤符冒號在 filtergraph 裡容易翻車;中文相對路徑沒事(cwd 可以中文,但 filter 參數裡盡量 ASCII)。
- 在輸出目錄放
- ASS 檔案用
utf-8-sig寫、ASCII 檔案名- 中文檔案名在 filter 參數裡不穩;寫
_s4.ass這種。
- 中文檔案名在 filter 參數裡不穩;寫
診斷命令(看 libass 到底讀到什麼):
& $ff -hide_banner -v verbose -f lavfi -i "color=c=0x303040:s=2560x1080:r=30:d=2" `
-vf "ass=_s4.ass:fontsdir=fonts" -c:v libx264 -crf 22 -pix_fmt yuv420p _t.mp4 2>&1 | Select-String 'Bad timestamp|Style|font file'
3. 顏色 / alpha 語義
- 顏色:
&HBBGGRR&(藍綠紅順序),例:白&H00F4F1EA&、墨黑&H00140F11&、粉&H00B35CFF&、薰衣草&H00FFA4C4&、奶油&H00D7EEF8&、金&H0054C9FF&。 - 透明度
\1a:&H00&完全不透明;&HFF&完全透明(十六進制越高越透明)。早期炸場版閃爍失效就是 alpha 反了。 - Style 行的 PrimaryColour 是
&HAABBGGRR(AA 在前,不帶尾 &)。
4. 字體
- libass 用 fontconfig/directwrite,family name 必須對。用 PIL 查:
from PIL import ImageFont
ImageFont.truetype(r"...\fonts\Anton-Regular.ttf", 40).getname() # ('Anton','Regular')
- 本項目字體:Anton / Archivo Black / Bebas Neue / Permanent Marker / Silkscreen / Rubik Glitch / Bungee / Monoton / Pirata One / UnifrakturCook;系統 Georgia、Microsoft YaHei 可直接
\fnGeorgia、\fnMicrosoft YaHei。 - Permanent Marker 在 libass 裡實際約 0.79×fs,要按這個係數放大字號 / 色塊尺寸。
- 缺字形(如 ♥ ★)會自動回退 ArialMT,能顯示但字形不同;要精確就用
\fnMicrosoft YaHei。
5. 動畫標籤速查
| 標籤 | 用法 |
|---|---|
\fad(in,out) | 毫秒淡入淡出 |
\move(x1,y1,x2,y2,t1,t2) | 位移(與 \pos 二選一,move 優先) |
\t(t1,t2,\tag...) | 定時插值,例 \t(0,130,\fscx100\fscy100\blur(0)) |
\frz / \fscx / \fscy | 旋轉/縮放(負值可做翻轉) |
\blur(n) | 模糊(大詞落地前模糊→清晰很有效) |
\clip(x1,y1,x2,y2) | 矩形裁切(做切片/掃光/顯現) |
\org(x,y) | 旋轉/縮放中心點 |
\p1+繪圖 | m 0 0 l w 0 w h 0 h 矩形等 |
坑:同一屬性連續兩個 \t 會只取一個(例如規則線收縮動畫),拆成兩個事件/兩次重繪解決。
坑:\p1 繪圖座標是左上角錨點,libass 還會把整塊按高度上移 h/2;用 \an7\pos(左上角) + 絕對座標最穩,旋轉/掃出用 \org 定原點。
坑:螢光筆色塊想在詞後面“掃出”,用繪圖層 \fscx0 → \t(0,150,\fscx100),原點用 \org。
6. 渲染命令
6.1 直接燒錄(最常用)
cd 輸出目錄
& $ff -v error -y -i _base_30s.mp4 -vf "ass=_s4.ass:fontsdir=fonts" `
-c:v libx264 -preset medium -crf 16 -pix_fmt yuv420p -movflags +faststart OUT.mp4
30s / 2560×1080 / 中文字幕量 ≈ 1–3 分鐘。
6.2 透明疊加層(交給剪輯軟體用)
& $ff -v error -y -f lavfi -i "[email protected]:s=2560x1080:r=30,format=rgba" `
-vf "fps=30,ass=_sub.ass:alpha=1" -t 154.6 `
-c:v libvpx-vp9 -pix_fmt yuva420p -b:v 0 -crf 30 -row-mt 1 -auto-alt-ref 0 `
-metadata:s:v:0 alpha_mode=1 OUT_alpha.webm
- 解碼必須
-c:v libvpx-vp9;原生 vp9 解碼會丟 alpha。 - 相容性優先的“濾色疊加”mp4:黑底 + 亮度字,剪輯軟體用 Screen/Lighten 混合。
6.3 抽幀驗收(別看渲染日誌,看圖)
& $ff -v error -ss 12.4 -i OUT.mp4 -frames:v 1 -q:v 2 -y _check.jpg
注意:
-ss放在-i前面(輸入 seek)時,某些情況下 ASS 時間軸相對輸出被重定向,預覽會空白;驗證抽幀用輸出側 seek(-i OUT.mp4 -ss 12.4)或直接抽整段再選。
7. 故障速查表
| 現象 | 原因 | 修法 |
|---|---|---|
| 全片一個字都沒有 | 時間戳格式 / Style 顏色尾 & | 改 H:MM:SS.cc;Style 顏色去尾 & |
| 部分句沒有 | 該句時間戳寫錯或 start>end | 逐句檢查 |
| 字很小/字型不對 | 字體 family 名錯 | PIL getname() 核對 |
\p1 色塊偏上/偏下 | 繪圖錨點 + libass 上移 h/2 | \an7 + 左上絕對座標 + 實測標定幀 |
| 旋轉中心不對 | 預設中心不是想要的 | 加 \org(x,y) |
| 淡出沒生效/閃爍反了 | alpha 語義反了 | &H00 不透明、&HFF 透明 |
| 30s 渲染只有前 4 句 | 迴圈只遍歷了前幾句 | 檢查事件生成迴圈範圍 |
| 中文路徑/檔名渲染失敗 | filter 參數裡帶非 ASCII | ASS 與底片用 ASCII 檔名 + 中文 cwd |
8. 生成器骨架(Python)
def ts(sec):
h=int(sec//3600); m=int(sec%3600//60)
return "%d:%02d:%05.2f" % (h,m,sec-h*3600-m*60)
class Ass:
def __init__(self,path,w=2560,h=1080): self.ev=[]; self.path=path; self.w=w; self.h=h
def add(self,layer,t0,t1,tags,text): self.ev.append((layer,t0,t1,tags,text))
def write(self):
head = open("template_header.ass",encoding="utf-8").read() # 或在代碼裡內聯
lines=[]
for layer,t0,t1,tags,text in sorted(self.ev,key=lambda e:(e[0],e[1])):
lines.append("Dialogue: %d,%s,%s,X,,0,0,0,,{%s}%s"%(layer,ts(t0),ts(t1),tags,text))
open(self.path,"w",encoding="utf-8-sig",newline="\n").write(head+"\n".join(lines)+"\n")
參考實現:字幕/build/styles_anim.py(S2/S4/S6)、flare_wide.py、neon_wide.py、avoid_side2.py。