关于 wjs-burning-subtitles
wjs-burning-subtitles 是用于 FFmpeg 视频字幕与配音合成的技能,可将 SRT 硬字幕烧录或软字幕封装,并在单次编码中混合配音与原声。适合制作本地化视频和社交平台成片。
主要功能
- 01烧录始终显示的字幕
- 02封装可切换的 mov_text 字幕轨
- 03合并字幕、配音与原声
- 04按字幕、配音模式渲染视频
适用场景
- 01为不支持字幕轨的平台制作硬字幕视频
- 02为播放器添加可切换字幕轨
- 03将翻译字幕与配音合成为成片
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jianshuo/ claude-skills --skill wjs-burning-subtitles安装后在 Claude Code 中输入/wjs-burning-subtitles确认已加载。
附带文件scripts/render.py
详细介绍
wjs-burning-subtitles
视频 + SRT → 带字幕的视频。也可用于本地化工作流的最终编码阶段:接收视频、来自 /wjs-dubbing-video 的可选配音音轨,以及要烧录的可选 SRT,通过一次 ffmpeg 处理生成可上传的 MP4。不会多次连续解码和重新编码。
适用场景
- 用户有 SRT,希望字幕始终显示在视频上(烧录字幕,适用于微信视频号 / 抖音 / WeChat 等不会识别内嵌字幕轨道的播放器)。
- 用户想要可切换的字幕轨道(软封装),适用于支持
mov_text的 QuickTime / VLC / IINA / 移动端播放器。 - 在
/wjs-dubbing-video之后合成最终版本:烧录目标语言字幕,并在一次编码中将配音与作为底音的原音混合。
不适用场景
- 还没有 SRT → 先运行
/wjs-transcribing-audio,再运行/wjs-translating-subtitles。 - 在 HyperFrames 中制作的片段需要 HTML/CSS 字幕(动态字幕、逐词高亮、自定义字体)→ 改用
/wjs-overlaying-video。不要在同一输出中混用 libass 烧录字幕和 HyperFrames 字幕。 - “字幕”实际上是动态图形(动画标注、带 Logo 的下三分之一字幕条、动态排版)→ 应使用
/wjs-overlaying-video,而不是此工具。
render.py 的 3 种模式
scripts/render.py 会根据标志自动检测模式:
- 仅字幕 —
--video + --srt→ 重新编码视频并烧录字幕,原音频直接透传。 - 仅配音 —
--video + --dub→ 保留原视频流;替换或混合音轨。 - 完整本地化版本 —
--video + --srt + --dub→ 烧录字幕并混合配音。默认将原音频调低音量,作为配音下方的“底音”(设置--bed-volume 0或--no-original-audio可移除原音)。
烧录字幕需要使用带 libass 的 ffmpeg。首次使用时,如果需要,脚本会自动从 evermeet.cx 下载启用了 libass 的静态构建版本到 /tmp/ff_bin/。
软封装(可切换的字幕轨道)
播放器应用可以显示或隐藏字幕。适用于任何 ffmpeg 构建版本——不需要 libass:
ffmpeg -i input.mp4 -i input.zh-CN.srt \
-map 0:v -map 0:a -map 1:0 \
-c:v copy -c:a copy -c:s mov_text \
-metadata:s:s:0 language=zho -metadata:s:s:0 title="中文" \
output.mp4
此方法速度快(流复制),且可逆。适用于以下情况:
- 目标平台支持内嵌字幕(YouTube 会自动检测;VLC/QuickTime 会识别)。
- 用户希望观众可以关闭字幕。
- 不想重新编码视频。
render.py --video IN.mp4 --srt SUB.srt --soft-mux 会执行此流程。
硬字幕烧录(始终可见,libass)
适用于微信 / 抖音 / 朋友圈等播放器不会识别内嵌字幕轨道的场景。
承诺烧录字幕前,先确认 libass 可用
ffmpeg -filters 2>&1 | grep -E "subtitles|^.. ass "
如果没有显示 subtitles 或 ass,则该构建版本缺少 libass。Homebrew 默认的 ffmpeg formula 通常经过精简(没有 --enable-libass、--enable-libfreetype 和 drawtext)。不要浪费时间处理 force_style 中的逗号转义——无论 shell 如何引用,都会报错 No such filter: 'subtitles'。
macOS 上最快的修复方式——放入静态构建版本,不更改系统设置
curl -fsSL -o /tmp/ff.zip https://evermeet.cx/ffmpeg/getrelease/zip
unzip -o /tmp/ff.zip -d /tmp/ff_bin >/dev/null
FF=/tmp/ff_bin/ffmpeg
$FF -version | grep -oE -- "--enable-(libass|libfreetype)"
然后在渲染时使用 $FF,而不是 ffmpeg。其他操作(探测、提取音频、软封装)使用 brew 二进制文件即可。如果默认 ffmpeg 缺少 libass,render.py 会自动回退。
使用样式覆盖选项进行烧录渲染
🛑 检查点——开始完整渲染前先确认。 烧录字幕会重新编码整个视频(5 分钟的片段需要消耗数分钟 CPU 时间)。开始前:
- 使用
-t 30只渲染前 30 秒,快速预览。 - 从最长行的字幕条目中提取一帧(参见下方“字号校准”),并读取该帧。
- 向用户展示预览帧和字幕条目文本,并询问:“字号/字体/边距 OK 吗?OK 才跑全片。”等待用户明确确认。
只有当用户在同一对话中已批准使用完全相同的字体配置对这段视频进行完整渲染时,才跳过此检查点。
$FF -i input.mp4 \
-vf "subtitles=input.zh-CN.srt:force_style='Fontname=PingFang SC\,Fontsize=12\,PrimaryColour=&H00FFFFFF\,OutlineColour=&H00000000\,BorderStyle=1\,Outline=2\,Shadow=1\,MarginL=20\,MarginR=20\,MarginV=40'" \
-c:v libx264 -crf 18 -preset medium -pix_fmt yuv420p \
-c:a copy output.mp4
在 force_style 中,将每个逗号转义为 \,(滤镜图解析器会把未转义的逗号当作链式分隔符)。其他特殊字符均可直接使用。
字号校准——至关重要
libass 会将内部 PlayRes 放大到实际视频分辨率。传入的数字不是输出中的像素值。以 544×960 的竖屏手机视频为例,校准时 Fontsize=22 会将每个汉字渲染为约 55px 宽,超出画面;而 Fontsize=12 会将每个汉字渲染为约 30–35px 宽,15 个字符一行时可以清晰显示。
经验法则:从 Fontsize=12 开始,渲染后务必提取一帧并检查:
$FF -ss 30 -i output.mp4 -frames:v 1 /tmp/frame.png -y
# then Read /tmp/frame.png to verify the longest-line cue fits
选择一个对应字幕行字符数最多的时间戳——短行无法暴露溢出问题。添加 MarginL=20 MarginR=20 作为安全边距;绝不要相信默认的左右边距。
样式速查表
重要参数(libass force_style):
Fontname=PingFang SC— macOS 默认的 CJK 字体;备选:Songti SC、Heiti SC、STHeiti、Hiragino Sans GB。Fontsize=12— 从较小字号开始,检查画面后再逐步调大。PrimaryColour=&H00FFFFFF— 白色文字(BBGGRR + alpha)。OutlineColour=&H00000000— 黑色描边。BorderStyle=1— 仅描边(适合各种背景,画面简洁)。背景复杂时,使用BorderStyle=3在文字后添加不透明底框。Outline=2— 2px 描边粗细。Shadow=1— 轻微投影。MarginL=20 MarginR=20— 确保文字位于画面内。MarginV=40— 与画面底边的垂直距离。
烧录字幕的 SRT 行长规范
即使 Fontsize 设置正确,行长过长仍会导致换行或溢出。每行屏幕字幕应控制在约 15 个汉字以内(约 42 个拉丁字符)。在 SRT 字幕块中使用显式 \n 换行——不要依赖自动换行。两行短句始终胜过一行长句。(这属于上游规范——/wjs-translating-subtitles 应已将字幕行限制在这些长度内。)
音频混音——将原声作为低音量底声保留
只有配音的音轨听起来会像配音(因为它确实如此)。在配音下方以低音量混入原声,会营造出“专业翻译”的效果——仍能听到说话者的呼吸、强调和笑声,只是音量低于新配音。
$FF -i original.mp4 -i dub.mp4 \
-filter_complex "[0:a]volume=0.18[orig];\
[1:a]volume=1.0[dub];\
[orig][dub]amix=inputs=2:duration=longest:normalize=0[a]" \
-map 0:v -map "[a]" \
-c:v copy -c:a aac -b:a 192k mixed.mp4
建议的初始音量:
- 原声底音设为
0.15–0.25(约 −16 至 −12 dB) - 配音设为
1.0 - 使用
normalize=0,避免 amix 在两路音频同时播放时自动降低音量。
要彻底移除原声:--no-original-audio(等同于 --bed-volume 0)。
合并配音、烧录字幕和原声底音(完整处理)
一次 ffmpeg 调用即可完成全部三项操作——将目标字幕烧录到视频流,并混合两条音轨:
$FF -i original.mp4 -i dub.mp4 \
-filter_complex "[0:v]subtitles=input.zh-CN.srt:force_style='Fontname=PingFang SC\,Fontsize=12\,PrimaryColour=&H00FFFFFF\,OutlineColour=&H00000000\,BorderStyle=1\,Outline=2\,Shadow=1\,MarginL=20\,MarginR=20\,MarginV=40'[v];\
[0:a]volume=0.18[orig];[1:a]volume=1.0[dub];\
[orig][dub]amix=inputs=2:duration=longest:normalize=0[a]" \
-map "[v]" -map "[a]" \
-c:v libx264 -crf 18 -preset medium -pix_fmt yuv420p \
-c:a aac -b:a 192k final.mp4
这是“发布到社交媒体”的最终剪辑。render.py --video original.mp4 --dub dub.mp4 --srt input.zh-CN.srt 会运行这一完整流程。
运行 render.py
# Subtitles only (burn):
python3 ~/.claude/skills/wjs-burning-subtitles/scripts/render.py \
--video IN.mp4 --srt SUB.srt --out OUT.mp4
# Dub only (replace audio, no subs):
python3 ~/.claude/skills/wjs-burning-subtitles/scripts/render.py \
--video IN.mp4 --dub IN_zh_dub.mp4 --out OUT.mp4
# Full localized cut (burn + dub + original bed):
python3 ~/.claude/skills/wjs-burning-subtitles/scripts/render.py \
--video IN.mp4 --srt IN.zh-CN.srt --dub IN_zh_dub.mp4 --out OUT.mp4
# Soft-mux (no re-encode):
python3 ~/.claude/skills/wjs-burning-subtitles/scripts/render.py \
--video IN.mp4 --srt SUB.srt --soft-mux --out OUT.mp4
完整的样式和音频参数列表请参阅 render.py --help(--font、--fontsize、--color、--outline-color、--margin-v、--bed-volume、--no-original-audio)。
输出
- 烧录模式:
<source>_burned.mp4(重新编码,字幕由 libass 渲染) - 软字幕封装模式:
<source>_softsub.mp4(流复制,使用mov_text轨道) - 完整剪辑:
<source>_final.mp4(重新编码的视频,带有烧录字幕和混合音频)
反模式
- ❌ 未验证 libass 就承诺烧录字幕。 先检查
ffmpeg -filters | grep subtitles;如果缺少该滤镜,则自动回退到 evermeet 静态构建。 - ❌ 未检查帧就提交烧录结果。 开始完整渲染前,务必提取最长字幕行对应的帧并进行查看。
- ❌ 在
force_style中直接使用逗号。 滤镜图解析器会将其吞掉。将所有内部逗号转义为\,。 - ❌ 混用 libass 烧录字幕和 HyperFrames 字幕。 每个输出视频只能选择一种字幕系统。如果你在
/wjs-overlaying-video中使用 HTML/CSS 字幕,也不要在这里烧录字幕。 - ❌ 在 SRT 中使用句点表示毫秒。 Whisper local 会写入
.mmm;libass 虽然可以容忍,但其他下游工具会出错。请统一改为,mmm。 - ❌ 默认使用
BorderStyle=3(不透明框)。 除非背景确实很杂乱,否则请使用BorderStyle=1(仅描边)——字幕框显得厚重又过时。
上游
/wjs-transcribing-audio+/wjs-translating-subtitles— 生成 SRT 输入。/wjs-dubbing-video— 为完整本地化剪辑模式生成*_<lang>_dub.mp4输入。纯配音文件在技术上已经是成品视频;此技能会在其下方混入原音并烧录字幕,使其可供交付。
常见问题
- 在一段视频上合适的字号,在另一段视频上却显得很小或很大。 libass 按 PlayRes 比例缩放,而非按像素缩放。请针对每种视频分辨率重新校准,不要依赖硬编码值。
- 默认边距会导致竖屏手机视频中的文字被裁切。 务必显式设置
MarginL=20 MarginR=20和MarginV=40(或更高)。 mov_text轨道能在 QuickTime 中显示,但在某些 Android 播放器中无法显示。 如果目标受众是移动端中文用户,软封装不可靠;请改用烧录字幕。- 背景杂乱或对比度不足。 将
Outline=2增大为Outline=3,或切换到BorderStyle=3使用半透明框(BackColour=&H80000000,即 50% 黑色)。