Python+FFmpeg实现逐字歌词字幕:ASS卡拉OK效果与视频烧录实战
2026/8/31 4:13:57 网站建设 项目流程

平时做短视频合成或二创剪辑时,最让人头疼的往往不是视频本身,而是字幕。尤其是英文歌里带拼字效果的口播或歌词,比如 “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

如果输出里包含类似asssubtitles的行,说明当前 FFmpeg 支持字幕滤镜。如果没有,建议换成完整版 FFmpeg,或者用包管理器安装:

# Ubuntu / Debian sudo apt install ffmpeg libass-dev # macOS brew install ffmpeg --with-libass

Windows 用户推荐从 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

几个关键点:

  • PlayResXPlayResY是字幕画布分辨率,通常和视频分辨率一致,推荐设为 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会被拆成LICIOUS七个字母,每个字母分配一段高亮时间。

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()

这段代码做了几件事:

  1. 读取timeline.json,拿到时间和文本数据;
  2. seconds_to_ass_time把秒换算成 ASS 的时间格式;
  3. split_karaoke_units把歌词拆成“词”或“字母”的高亮单元;
  4. 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.mp4

5.2 中文显示为方框的问题

这是最常遇到的问题。如果烧录出来的视频里中文全部变成方框,通常不是 ASS 文件的问题,而是 FFmpeg 渲染时找不到中文字体。

排查分三步:

  1. 确认fonts/目录里确实存在中文字体文件;
  2. 确认 ASS 的Fontname写的是系统可识别的字体名,而不是随意起的名字;
  3. 确认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.00seconds_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先出一版草稿,确认效果后再用mediumslow压正式版。

8. 总结

这篇文章从一个具体的短视频需求出发,完整演示了逐字歌词字幕的制作链路:

  • 用 JSON 组织歌词时间轴;
  • 用 Python 把歌词文本拆成词或字母级别的高亮单元;
  • 生成 ASS 卡拉 OK 字幕;
  • 用 FFmpeg 把字幕烧录到视频。

通过这个案例,你不仅掌握了一条实用的短视频字幕生产流程,也理解了 ASS 字幕的核心语法和 FFmpeg 滤镜的基本用法。下一步可以继续学习 ASS 样式进阶,比如动态字体、滚动字幕、跑马灯效果,或者研究用librosa自动检测 BPM 来对齐音乐节奏。

如果你正在做系列短视频内容,建议把时间轴 JSON、字体模板、Python 脚本都沉淀成团队内部工具,后续新视频只需要填时间轴就能出片,效率会提升非常多。如果这篇文章对你有帮助,可以收藏备用,也欢迎在实际项目中复现后根据自己的需求继续扩展。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询