平时做短视频合成或二创剪辑时,最让人头疼的往往不是视频本身,而是字幕。尤其是英文歌里带拼字效果的口播或歌词,比如 “D to the E, to the L-I-C-I-O-U-S”,如果只是把一行字直接压上去,节奏感和观赏性都会差很多。
本文就围绕一个真实场景来写:假设你在维护一个叫“盒盒短视频”的栏目,需要给编号 260809 的物料做逐字歌词字幕,画面里出现的艺人名字分别是玹硕、道荣、炡禹。文章会从字幕格式选型开始,逐步演示如何用 Python 生成 ASS 卡拉 OK 字幕,再用 FFmpeg 把字幕烧录进视频。全程命令和代码都是可以直接复制的,适合短视频运营、二创剪辑、音视频开发入门者。
1. 字幕技术选型:为什么推荐 ASS 加 FFmpeg
1.1 短视频字幕的三种常见做法
给视频加字幕,业内主要有三种做法:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 剪辑软件手动加字幕 | 在 PR、剪映、Final Cut 中逐条添加文字 | 直接、所见即所得 | 无法批量处理,时间轴一长效率低 |
| 硬字幕烧录 | 用 FFmpeg 等工具把文字直接画进画面 | 自动、可控、可批量 | 无法二次编辑 |
| 封装软字幕 | 生成 ASS/SRT 字幕文件,播放器动态渲染 | 可切换、可改样式 | 部分平台不支持软字幕 |
对于逐字变色、逐字母高亮这种效果,最合适的不是 SRT,而是 ASS。SRT 只能整行显示,没法指示“哪个词先亮、哪个词后亮”,而 ASS 自带的卡拉 OK 标签能精确控制每一个词、甚至每一个字母的高亮时间。
1.2 为什么用 Python 生成字幕
手动在 Aegisub 里逐条敲字幕也不是不行,但缺点是慢。如果你的素材很长,或者每周都要产出类似内容,纯手工维护时间轴会非常痛苦。Python 的优势在于可以把“歌词文本”和“时间轴”变成结构化数据,然后一键生成字幕文件。
本文的核心流程是:
歌词文本 + 时间轴(JSON) -> Python 脚本 -> ASS 字幕 -> FFmpeg 烧录 -> 成片每一条链路都可以独立测试,出错时也能快速定位到具体环节。
2. 环境准备与项目结构
2.1 软件环境
本文示例基于以下环境,版本不必完全一致,思路是通用的:
- 操作系统:Windows 10 / macOS / Ubuntu 均可
- Python 3.8 及以上
- FFmpeg 4.4 及以上,编译时需要开启 libass 支持
- 文本编辑器:VS Code、Sublime Text 或任意编辑器
先检查 FFmpeg 是否支持 ASS 滤镜:
ffmpeg -filters | grep ass如果输出里包含类似ass或subtitles的行,说明当前 FFmpeg 支持字幕滤镜。如果没有,建议换成完整版 FFmpeg,或者用包管理器安装:
# Ubuntu / Debian sudo apt install ffmpeg libass-dev # macOS brew install ffmpeg --with-libassWindows 用户推荐从 FFmpeg 官网下载 full build 版本,解压后配置好环境变量即可。
2.2 项目目录设计
为了后续扩展,我们先把目录建清楚:
karaoke_project/ ├── input/ │ └── treasure_260809.mp4 ├── lyrics/ │ └── timeline.json ├── scripts/ │ ├── build_ass.py │ └── burn_subtitle.sh ├── fonts/ │ └── msyh.ttc └── output/ ├── treasure_260809.ass └── treasure_260809_burned.mp4其中:
input/存放原始视频素材;lyrics/存放歌词和时间轴数据;scripts/存放 Python 和 Shell 脚本;fonts/存放自定义字体;output/存放生成的字幕和成品视频。
2.3 素材与字体准备
示例中需要准备一段时长约 10 秒的视频素材。如果手头没有现成视频,可以先用 FFmpeg 生成一段测试视频:
ffmpeg -f lavfi -i color=c=black:s=1920x1080:r=30 -t 10 -c:v libx264 -pix_fmt yuv420p input/treasure_260809.mp4这条命令会生成一段 10 秒、1920x1080、30 帧的黑屏测试视频。实际项目中替换成真实素材即可。
中文字幕最容易踩的坑是字体。如果 FFmpeg 找不到中文字体,中文会显示成方框。建议提前准备一个支持中文的字体文件,例如微软雅黑msyh.ttc、思源黑体SourceHanSansSC-Regular.otf,放到fonts/目录下。
3. ASS 字幕与卡拉 OK 标签原理
3.1 ASS 文件基础结构
ASS(Advanced SubStation Alpha)是一种功能强大的字幕格式。一个最简单的 ASS 文件包含四部分:
[Script Info] ScriptType: v4.00+ PlayResX: 1920 PlayResY: 1080 [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: Karaoke,Arial Black,80,&H00FFFFFF,&H0000FFFF,&H00000000,&H64000000,-1,0,0,0,100,100,0,0,1,4,2,2,40,40,40,1 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text Dialogue: 0,0:00:00.00,0:00:02.00,Karaoke,,0,0,0,,{\k50}Hello几个关键点:
PlayResX和PlayResY是字幕画布分辨率,通常和视频分辨率一致,推荐设为 1920x1080;Style行定义字幕样式,包括字体、字号、颜色、描边、阴影、对齐方式;Event行是真正显示的字幕内容,格式是Dialogue: 图层,开始时间,结束时间,样式,...;{\k50}Hello是卡拉 OK 标签,表示Hello这个词会在 50 厘秒内完成高亮填充。
ASS 中颜色的格式是&HAABBGGRR,其中 AA 是透明度,BB 是蓝色通道,GG 是绿色通道,RR 是红色通道。注意这里不是常规的 RGB 顺序,而是 BGR。
3.2 \k 标签的工作方式
在 ASS 里,卡拉 OK 标签常用三个:
| 标签 | 含义 |
|---|---|
\k | 匀速填充高亮,填充时间单位是厘秒(1/100 秒) |
\K | 填充完再跳变,时间单位同样是厘秒 |
\kf | 等同于\K,用于向后兼容 |
举个例子:
{\k100}D {\k50}to {\k50}the E这段字幕会先高亮D,持续 1 秒;接着高亮to,持续 0.5 秒;最后高亮the E,持续 0.5 秒。高亮颜色由样式里的 SecondaryColour 控制,默认文字颜色由 PrimaryColour 控制。
这里需要特别注意:\k标签是“放在某个词前面”的,控制的是它后面紧接着的那个片段。如果你写过 CSS,可以把它理解为一种“前置样式指令”。
3.3 拼字文本的拆分规则
回到标题里的这句 “D to the E, to the L-I-C-I-O-U-S”。它是一句典型的拼字歌词,把 “Delicious” 拆成了字母。如果整句作为一个卡拉 OK 块,效果是整句一起变,没有“逐个字母点亮”的感觉。
所以要实现最理想的效果,需要把文本拆成高亮单位:
- 普通单词按空格拆分;
- 带连字符的单词按字母逐个拆分;
- 标点符号可以独立作为单位,也可以合并到前一个词。
例如L-I-C-I-O-U-S会被拆成L、I、C、I、O、U、S七个字母,每个字母分配一段高亮时间。
4. 实战:用 Python 生成逐字歌词字幕
4.1 编写时间轴 JSON
为了让脚本可复用,我们把歌词和时间轴单独放到一个 JSON 文件里,避免每次改时间都动代码。
文件路径:lyrics/timeline.json
{ "video_width": 1920, "video_height": 1080, "font_name": "Microsoft YaHei", "lines": [ { "start": 0.5, "duration": 3.5, "text": "D to the E, to the L-I-C-I-O-U-S" }, { "start": 4.2, "duration": 1.5, "text": "TREASURE" }, { "start": 6.0, "duration": 2.5, "text": "玹硕 道荣 炡禹" } ] }这里font_name写的是系统字体名,不一定等于字体文件名。比如微软雅黑的文件名是msyh.ttc,但字幕里使用的字体名通常要写Microsoft YaHei。不同系统字体名可能略有差异,Windows 上一般写Microsoft YaHei,macOS 上可以写PingFang SC,Linux 上可以写Noto Sans CJK SC。
4.2 实现卡拉 OK 文本拆分
文件路径:scripts/build_ass.py
# -*- coding: utf-8 -*- """ 根据 lyrics/timeline.json 生成 ASS 卡拉 OK 字幕。 用法:python scripts/build_ass.py """ import json import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) TIMELINE_PATH = os.path.join(BASE_DIR, "lyrics", "timeline.json") OUTPUT_PATH = os.path.join(BASE_DIR, "output", "treasure_260809.ass") def seconds_to_ass_time(sec): """ 将秒数转换为 ASS 时间格式:H:MM:SS.CC """ total_cs = int(round(sec * 100)) h, rem = divmod(total_cs, 3600 * 100) m, rem = divmod(rem, 60 * 100) s, cs = divmod(rem, 100) return f"{h}:{m:02d}:{s:02d}.{cs:02d}" def split_karaoke_units(text): """ 将一行文本拆分成高亮单元。 带连字符的英文单词按字母拆分,普通单词按整体保留。 """ units = [] for word in text.split(): if "-" in word: # 例如 L-I-C-I-O-U-S -> ['L','I','C','I','O','U','S'] letters = [ch for ch in word if ch.isalpha()] # 去掉连字符,只保留字母 units.extend(letters) else: units.append(word) return units def build_ass(timeline, output_path): width = timeline.get("video_width", 1920) height = timeline.get("video_height", 1080) font_name = timeline.get("font_name", "Microsoft YaHei") header = f"""[Script Info] ScriptType: v4.00+ PlayResX: {width} PlayResY: {height} WrapStyle: 0 ScaledBorderAndShadow: yes [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: Karaoke,{font_name},80,&H00FFFFFF,&H0000FFFF,&H00000000,&H64000000,-1,0,0,0,100,100,0,0,1,4,2,2,40,40,40,1 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text """ events = [] for line in timeline["lines"]: start = line["start"] duration = line["duration"] text = line["text"] units = split_karaoke_units(text) if not units: continue # 每个高亮单元平分该行时长 unit_duration = duration / len(units) unit_cs = int(round(unit_duration * 100)) # 为每个单元生成 {\kXX} 标签 parts = [] for unit in units: parts.append(f"{{\\k{unit_cs}}}{unit}") karaoke_text = "".join(parts) end = start + duration start_str = seconds_to_ass_time(start) end_str = seconds_to_ass_time(end) events.append( f"Dialogue: 0,{start_str},{end_str},Karaoke,,0,0,0,,{karaoke_text}" ) with open(output_path, "w", encoding="utf-8") as f: f.write(header + "\n".join(events) + "\n") print(f"[OK] ASS 字幕已生成:{output_path}") def main(): with open(TIMELINE_PATH, "r", encoding="utf-8") as f: timeline = json.load(f) build_ass(timeline, OUTPUT_PATH) if __name__ == "__main__": main()这段代码做了几件事:
- 读取
timeline.json,拿到时间和文本数据; seconds_to_ass_time把秒换算成 ASS 的时间格式;split_karaoke_units把歌词拆成“词”或“字母”的高亮单元;build_ass生成完整的 ASS 文件内容并写入output/目录。
运行方式:
python scripts/build_ass.py如果一切正常,会看到输出:
[OK] ASS 字幕已生成:output/treasure_260809.ass打开生成的 ASS 文件,可以看到类似这样的内容:
Dialogue: 0,0:00:00.50,0:00:04.00,Karaoke,,0,0,0,,{\k23}D {\k23}to {\k23}the {\k23}E, {\k23}to {\k23}the {\k23}L{\k23}I{\k23}C{\k23}I{\k23}O{\k23}U{\k23}S这里{\k23}表示每个字母高亮约 0.23 秒,总时长 3.5 秒,14 个单元,平均每个单元 0.25 秒,取整后是 23 厘秒左右,合计时长会有一点误差。实际可以接受,如果要求精确,可以改成浮点毫秒再在标签里四舍五入,但多数场景下 1/100 秒级别的误差不影响观看。
4.3 扩展:支持不同单词不同时长
上面的方案是“平均分配”,适合节奏均匀的口播或歌词。但真实音乐里,长单词和短单词占的拍子不一样。如果希望更精确,可以给 JSON 加上自定义时长字段。
例如:
{ "start": 0.5, "duration": 3.5, "text": "D to the E, to the L-I-C-I-O-U-S", "highlights": [ {"unit": "D", "duration": 0.3}, {"unit": "to", "duration": 0.2}, {"unit": "the", "duration": 0.2} ] }脚本中优先读取highlights,如果存在就不做平均分配,而是按照每个单元的时间累加。这样能做出和音乐鼓点严格对齐的效果。
5. 实战:用 FFmpeg 烧录字幕到视频
5.1 基础烧录命令
ASS 文件生成后,接下来就是把字幕画到视频画面上。
在output/目录下执行:
ffmpeg -y \ -i input/treasure_260809.mp4 \ -vf "ass=output/treasure_260809.ass:fontsdir=fonts" \ -c:v libx264 -crf 18 -preset medium \ -c:a copy \ output/treasure_260809_burned.mp4参数说明:
| 参数 | 作用 |
|---|---|
-i | 指定输入视频 |
-vf | 应用视频滤镜,ass=...表示渲染 ASS 字幕 |
fontsdir | 指定字体搜索目录,指向项目里的fonts/文件夹 |
-c:v libx264 | 视频编码使用 H.264 |
-crf 18 | 画质参数,数值越小质量越高,18 是视觉无损的常见值 |
-c:a copy | 音频直接复制,不做重编码,速度快且不损失音质 |
如果你用的 FFmpeg 版本比较老,没有ass滤镜,也可以使用subtitles滤镜:
ffmpeg -y \ -i input/treasure_260809.mp4 \ -vf "subtitles=output/treasure_260809.ass:fontsdir=fonts" \ -c:v libx264 -crf 18 \ -c:a copy \ output/treasure_260809_burned.mp45.2 中文显示为方框的问题
这是最常遇到的问题。如果烧录出来的视频里中文全部变成方框,通常不是 ASS 文件的问题,而是 FFmpeg 渲染时找不到中文字体。
排查分三步:
- 确认
fonts/目录里确实存在中文字体文件; - 确认 ASS 的
Fontname写的是系统可识别的字体名,而不是随意起的名字; - 确认
fontsdir路径正确。
在 Linux 上,如果fontsdir方式不生效,可以先把字体安装到系统字体目录:
mkdir -p ~/.fonts cp fonts/msyh.ttc ~/.fonts/ fc-cache -f然后再执行 FFmpeg 命令。Windows 上通常安装的字体系统本身就能识别,重点检查 ASS 里的字体名是否匹配。
5.3 验证输出结果
烧录完成后,可以用 FFmpeg 抽取几帧画面检查字幕效果:
ffmpeg -i output/treasure_260809_burned.mp4 -ss 1 -vframes 1 output/frame_1s.png打开output/frame_1s.png,如果字幕正常显示,且能看到部分歌词高亮,说明流程已经跑通。
如果你使用的是带图形界面的系统,也可以直接用播放器打开烧录后的视频确认效果。
6. 常见问题与排查思路
下面整理了几个高频问题,基本覆盖了从生成字幕到烧录的完整链路。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
Python 脚本运行报ModuleNotFoundError | 没有安装依赖或路径错误 | 本示例只使用标准库,检查是否误用了第三方库;确认脚本在当前项目根目录运行 |
| ASS 文件生成时间全是 0:00:00.00 | seconds_to_ass_time传入负数或 NaN | 检查timeline.json里的 start 和 duration 是否为正数 |
| 烧录后中文字幕变成方框 | FFmpeg 找不到中文字体 | 使用fontsdir指定字体目录,或在系统里安装中文字体 |
| 字幕整体不同步 | timeline.json里的时间轴不准 | 用播放器逐帧确认歌词开始时间,调整 start 值 |
| 高亮颜色没有变化 | 样式表中 PrimaryColour 和 SecondaryColour 相同 | 检查 ASS 样式,确保两个颜色值不同 |
| 字母逐个高亮效果太碎 | L-I-C-I-O-U-S被平均分配了很短的时长 | 增大该行 duration,或改用highlights手工指定每个字母时长 |
FFmpeg 报No such filter: 'ass' | FFmpeg 编译时未开启 libass | 更换 FFmpeg 版本,或用包管理器重新安装 |
再补充一个容易踩的坑:在 Windows 命令行中,FFmpeg 滤镜里如果用反斜杠路径,需要转义。比如:
-vf "ass=output\\treasure_260809.ass"否则可能解析失败。更稳妥的方式是先把当前目录切到项目根目录,再使用相对路径。
6.1 如何快速定位问题环节
建议按下面的流程图排查:
生成视频素材 -> 运行 build_ass.py -> 打开 ASS 文件检查时间轴和颜色 | v FFmpeg 烧录 | v 播放器预览:字幕是否出现?中文是否正常?高亮是否同步?每一步都能独立验证。不要把 Python 和 FFmpeg 混在一起排查,出错后先确认 ASS 文件在 Aegisub 里能否正常预览,再考虑 FFmpeg 的参数问题。
7. 最佳实践与工程建议
7.1 时间轴设计
逐字歌词字幕的核心是“卡上节奏”。项目初期建议先把音频导入剪辑软件,标记出每个词或每个字母的起止时间,再整理成 JSON。不要靠肉眼猜时间,差 100 毫秒观看体验都会很怪。
如果你的素材是音乐类内容,可以考虑从 BPM 计算单位时长。假设一首歌是 120 BPM,那么每拍 0.5 秒,每半拍 0.25 秒,把歌词单元时长设置成 0.25 秒的整数倍,视觉节奏会更舒服。
7.2 字体与编码
生产环境中,字幕文件统一使用 UTF-8 编码。中文歌词建议使用支持中文的字体,例如思源黑体、阿里巴巴普惠体、汉仪字库等。注意字体授权问题,商用项目优先选择可免费商用的字体,避免字体版权风险。
ASS 文件的样式尽量集中管理。如果做系列视频,可以把[V4+ Styles]部分抽成模板,Python 脚本只负责动态生成[Events],这样后续调样式时只需要改模板,不需要重新改代码。
7.3 关键消息:素材版权与平台合规
这一点必须单独强调。如果你在短视频中使用了艺人素材或带版权的音乐,需要提前确认授权范围。个人学习和技术测试没有问题,但发布到公开平台时,尤其是涉及商业变现的内容,要遵守平台版权规则和著作权法。
“盒盒短视频”这类项目如果涉及真实艺人,建议:
- 优先使用官方提供的素材或已授权素材;
- 背景音乐尽量使用平台版权曲库或原创音乐;
- 字幕内容不要恶意剪辑、恶意解读;
- 面向公开传播的内容,做好版权自查记录。
7.4 自动化集成
如果每周要生产多条类似视频,可以把整个流程做成一条命令:
python scripts/build_ass.py && ffmpeg -y \ -i input/treasure_260809.mp4 \ -vf "ass=output/treasure_260809.ass:fontsdir=fonts" \ -c:v libx264 -crf 18 \ -c:a copy \ output/treasure_260809_burned.mp4也可以再接一个发布脚本,把成品视频上传到对象存储或内容平台。只要时间轴 JSON 足够规范,字幕生成和烧录就能完全自动化。
7.5 性能优化
FFmpeg 烧录字幕属于视频重编码,会比较吃 CPU。如果只是本地调试,可以把-crf调大到 23 以加快速度;如果需要高质量成片,再使用 18 或更低。批量处理时建议用-preset veryfast先出一版草稿,确认效果后再用medium或slow压正式版。
8. 总结
这篇文章从一个具体的短视频需求出发,完整演示了逐字歌词字幕的制作链路:
- 用 JSON 组织歌词时间轴;
- 用 Python 把歌词文本拆成词或字母级别的高亮单元;
- 生成 ASS 卡拉 OK 字幕;
- 用 FFmpeg 把字幕烧录到视频。
通过这个案例,你不仅掌握了一条实用的短视频字幕生产流程,也理解了 ASS 字幕的核心语法和 FFmpeg 滤镜的基本用法。下一步可以继续学习 ASS 样式进阶,比如动态字体、滚动字幕、跑马灯效果,或者研究用librosa自动检测 BPM 来对齐音乐节奏。
如果你正在做系列短视频内容,建议把时间轴 JSON、字体模板、Python 脚本都沉淀成团队内部工具,后续新视频只需要填时间轴就能出片,效率会提升非常多。如果这篇文章对你有帮助,可以收藏备用,也欢迎在实际项目中复现后根据自己的需求继续扩展。