做视频这几年,我养成了一个习惯:哪怕只是十几秒的短视频,我也坚持用脚本生成字幕,而不是在剪辑软件里一条条打字。原因很简单——字幕的本质是一份带时间轴的数据,而凡是数据,用代码处理都比手工可靠得多。这个习惯在我接触Deep Code之后被进一步放大了。Deep Code不是那种界面上摆着一堆按钮的工具,更准确地说,它是一类用代码、脚本去驱动视频制作的工作方式。这篇文章就把这套思路完整拆开,用最开源、最便宜的工具,带你从零做出一条能批量生产字幕的视频生产线。
说实话,目前网上关于Deep Code的资料比较零散,我没找到一个能严格定义它对应哪款具体软件的文档。所以我更愿意把它理解为一套“用代码驱动视频生产”的实践框架。文章里的所有步骤都基于FFmpeg、Python和本地语音识别模型实现,完全免费,你把这套流程跑通之后,无论以后用什么自动化工具,底层的逻辑都是通的。
1. 为什么字幕这件事非常适合“写代码”来完成
1.1 字幕的本质是一份带时间轴的数据
我们平时看到的一条字幕,在计算机眼里其实非常朴素:一段文字、一个开始时间、一个结束时间、一种显示样式。如果再加上序号,它就构成了一条标准的结构化记录。
结构化数据最擅长的事情是什么?是批量处理、统一修改、精准定位。你在剪辑软件里手动加字幕时,每一条都要重新打字、重新拖时间轴、重新调整样式。而用代码处理时,你只需要维护一份文本文件,改一个字、挪一段字幕的起止时间,都是毫秒级的事。
拿我自己的经验来说,一个20分钟的访谈视频,手工加字幕往往要两个小时,其中一半时间浪费在重复操作上。脚本处理的话,大约十几分钟就能完成从语音识别到字幕烧录的全流程,而且每一帧画面里的字幕样式都完全一致。这就是把视频里的字幕当成数据来处理的价值。
1.2 Deep Code 更像是一套工作流,不是一个按钮
我再强调一次:Deep Code如果只是被理解成某个软件,那你很难套用。真正有效的理解方式,是把Deep Code看成“用代码组织视频生产工序”的工作流。你通过命令行和脚本,把音频提取、语音识别、字幕生成、画面渲染这些环节串联起来。
传统剪辑软件里,你面对的是时间线上的一块块素材;而在Deep Code这类工作流里,你面对的是一个个可复用的函数和命令。区别在哪里?我打个比方:手工加字幕就像每次做饭都重新摸索火候,脚本生产则是把菜谱写下来,以后任何时候照着执行都能复现,还能随时调整配方。
尤其是字幕这种天然结构化的任务,一旦进入代码工作流,就会有四个非常明显的好处:效率高、样式统一、可批量操作、可回滚修改。
1.3 手工加字幕和脚本生产拉开差距的地方
我整理了一个对比表,可以很直观地看到差距:
| 对比项 | 手工剪辑软件加字幕 | 脚本自动生产 |
|---|---|---|
| 20分钟视频耗时 | 约1.5到2小时 | 约10到15分钟 |
| 样式一致性 | 依赖肉眼微调 | 全局统一 |
| 改错字成本 | 需定位到具体字幕逐条改 | 改源文件后重新渲染 |
| 批量处理多个视频 | 几乎不可能 | 一个命令跑完 |
| 可追溯性 | 操作记录难以回看 | 所有脚本和中间文件可保留 |
很多视频创作者一听“代码”两个字就头大,但这里并不需要你成为程序员。你需要做的只是照着脚本命令运行,理解几个关键参数的用途。这套工作流真正吃掉的是重复劳动,而不是替代你的审美判断。
2. 开始之前:先搭一条最小可用的字幕生产线
2.1 这一套工具组合解决什么问题
在动手之前,先想清楚我们需要哪几个工具,以及每个工具承担什么职责:
- FFmpeg:负责从视频里抽取音频,最后一键把字幕烧进画面。
- Python:负责写胶水脚本,把各个工具串起来。
- faster-whisper:本地语音识别模型,把音频转成带时间戳的文本。
- 中文字体:负责让字幕渲染时不变成方块。
- mkvmerge(可选):需要做软字幕封装时用来合成MKV。
这里先说一个原则:我全部选择本地工具,不依赖在线服务。原因有三个:第一,素材不外传,不会因为网络上传产生隐私顾虑;第二,批量处理时不限次数、不限时长;第三,定制性强,识别结果和字幕样式都可以完全掌控。
2.2 从视频里抽出“干净”的音频
语音识别模型最喜欢的输入,是16kHz采样率、单声道、无损PCM格式的音频。视频里的音轨往往带着各种编码和声道干扰,直接丢给识别模型会影响准确率,所以第一步先转换。
ffmpeg -i input.mp4 -vn -acodec pcm_s16le -ar 16000 -ac 1 audio_16k.wav逐个解释参数:
-vn:丢弃视频流,只处理音频。-acodec pcm_s16le:输出无损PCM编码,避免二次压缩损失细节。-ar 16000:采样率降到16kHz,这是大多数语音识别模型训练的常见规格。-ac 1:转成单声道,因为语音识别本来就不需要立体声信息。
这一步做对了,后面的识别效果会稳定很多。如果你发现视频里背景音乐太响,可以在后面加一个简单的滤波,比如-af "highpass=f=200,lowpass=f=8000",去掉极低频率的轰头感和极高频的噪声,能让人声更突出。
2.3 语音识别:本地Whisper和它的Python接口
音频准备好之后,就轮到语音识别出场。目前最方便的选择是faster-whisper,它是OpenAI Whisper模型的高效实现,对CPU比较友好,也能在显卡上跑得更快。
安装只需要一行命令:
pip install faster-whisper然后写一个最简单的转录脚本:
from faster_whisper import WhisperModel model = WhisperModel("small", device="cpu", compute_type="int8") segments, info = model.transcribe("audio_16k.wav", language="zh", vad_filter=True) for seg in segments: start = seg.start end = seg.end text = seg.text.strip() print(f"{start:.2f} -> {end:.2f} : {text}")这里有几个参数值得多说两句:
- 模型尺寸
small:中文场景下,从small起步比较合理,识别率能看,速度也快。如果视频里有明显口音或专业名词,可以换medium甚至large-v3,代价是时间和内存消耗增加。 compute_type="int8":CPU上运行时的定点化计算,速度提升明显,损失一点精度,日常够用。vad_filter=True:自动过滤静音片段,可以显著减少无意义的空白识别结果。
运行之后,你会得到一长串带开始时间和结束时间的文本片段。这就是字幕文件最原始的数据来源。
2.4 识别结果不等于字幕,还差关键一步
很多新手第一次跑完语音识别就急着生成字幕,结果发现识别出来的文本是一长串没有标点、没有断句的句子,画面里字数爆满,根本没法看。这里要明确:识别结果只是原料,离可以播出的字幕还差“分段”这一步。
分段的原则有两个:
- 按时间长度:每句字幕的显示时长控制在2到4秒左右。
- 按内容长度:中文字幕建议单行不超过18个字,超过就要考虑拆成两行。
faster-whisper默认切出来的片段通常比较碎,有时候两三秒就是一条,但有时候又会合并出10多秒的长句。所以我的做法是拿到识别结果后,再写一段规则脚本,对时间间隔太短的相邻字幕做合并,对时间过长、字数过多的句子再拆分。这部分逻辑我会在第3章详细说明。
3. 把识别结果变成字幕文件:SRT和ASS的结构拆解
3.1 SRT是所有工作流里的“通用货币”
SRT是地球上兼容性最好的字幕格式,几乎任何播放器、剪辑软件都认识它。先看一个标准示例:
1 00:00:01,000 --> 00:00:04,500 这是第一句字幕 2 00:00:04,600 --> 00:00:07,200 这是第二句字幕结构非常简单:序号、时间戳、字幕文本、空行。注意时间戳的格式是小时:分钟:秒,毫秒,中间是英文逗号,不是小数点。很多新手第一次写SRT会把毫秒写成点号,或者没有空行分隔,导致播放器识别混乱。
在脚本里写SRT时,用Python的标准写法:
def write_srt(entries, path): lines = [] for idx, entry in enumerate(entries, start=1): start_srt = format_ts(entry["start"]) end_srt = format_ts(entry["end"]) lines.append(str(idx)) lines.append(f"{start_srt} --> {end_srt}") lines.append(entry["text"]) lines.append("") with open(path, "w", encoding="utf-8-sig") as f: f.write("\n".join(lines))这里我特意用了utf-8-sig编码,因为Windows平台的老牌播放器对无BOM的UTF-8识别并不可靠,加上BOM之后乱码问题能少一大半。
3.2 为什么成产线里通常用ASS而不是SRT
SRT能用的原因是最小可用,但它几乎没有样式控制能力。你不能在SRT里指定字幕用的是黑体还是宋体,不能加描边,不能调整位置。如果你只是给自己本地看,那无所谓;但只要是发布到平台或者给客户看,字幕就需要立体感、描边、底部定位这些视觉效果。
这个时候就需要ASS格式。ASS是Advanced SubStation Alpha的缩写,它的核心优势是用头部样式块统一声明字体、大小、颜色、描边、阴影、对齐方式,然后在事件区里只负责内容和时间。
一个最基本的ASS文件长这样:
[Script Info] ScriptType: v4.00+ PlayResX: 1920 PlayResY: 1080 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: Default,Noto Sans CJK SC,68,&H00FFFFFF,&H00FFFFFF,&H00000000,&H96000000,0,0,0,0,100,100,0,0,1,3,2,2,60,60,40,1 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text Dialogue: 0,0:00:01.00,0:00:04.50,Default,,0,0,0,,这是第一句字幕这里面有几个要注意的地方:
- 颜色格式是
&H00BBGGRR,白字是&H00FFFFFF,如果你想要黄字,应该写&H0000FFFF,红色和蓝色在十六进制里是颠倒的。 Fontname建议用完整的中文字体名,比如Noto Sans CJK SC,也可以直接填字体文件路径,避免系统字体名不匹配导致渲染成方块。Alignment=2表示底部居中,这是大多数视频字幕的默认位置。
3.3 用Python把SRT转成ASS
有了SRT文件之后,把它转成ASS是字幕工作流里最常见的操作。转换的难点不在文本,而在时间戳格式:SRT用毫秒,ASS用厘秒。
def srt_ts_to_ass(ts): # "00:00:01,234" -> "0:00:01.23" h, m, rest = ts.split(":") s, ms = rest.split(",") s = int(s) ms = int(ms) cs = round(ms / 10) if cs >= 100: s += 1 cs -= 100 return f"{int(h)}:{int(m)}:{s:02d}.{cs:02d}"毫秒转厘秒看起来简单,但四舍五入有坑。比如00:00:01,999,毫秒转成厘秒会变成100厘秒,这时候必须给秒进位,否则时间戳会非法。这也是很多转换脚本生成的字幕时间轴错乱的原因。
我通常的做法是,在转换完ASS之后,还要做一遍时间戳合法性检查:如果某一行的开始时间小于上一行的结束时间,就把下一行开始时间改成上一行结束时间加上0.01秒,避免字幕重叠闪烁。
4. 两条烧字幕的路线:硬字幕和软字幕
4.1 硬字幕:用FFmpeg把字幕画进每一帧
硬字幕就是把字幕直接渲染在画面上,相当于“烧”进每一个像素。用FFmpeg做这件事非常简单:
ffmpeg -i input.mp4 -vf "ass=subs.ass" -c:v libx264 -crf 18 -preset slow -c:a copy output.mp4这里的关键是ass=subs.ass这个滤镜,它调用libass库把ASS字幕渲染成画面。如果你运行的时候提示找不到滤镜,说明你用的FFmpeg没编译libass支持,可以先验证一下:
ffmpeg -hide_banner -filters | grep ass看到输出里有ass这个滤镜,说明没问题。没有的话,换个带libass的构建版本,或者用包管理器安装完整版FFmpeg。
硬字幕的好处是兼容性无敌:无论什么播放器、什么网站,你看到的字幕就在画面里,不可能消失。代价是修改成本高——改一个字也要重新渲染一遍整段视频。
4.2 软字幕:把字幕作为独立轨道封装
软字幕则是把字幕文件和视频封装在同一个容器里,播放时由播放器解码显示。字幕不是画面的一部分,所以画质零损失,而且可以随时开关、切换语言。
封装方面,我常用两个工具:
用mkvmerge封装MKV:
mkvmerge -o output.mkv input.mp4 subs.ass用FFmpeg封装MP4:
ffmpeg -i input.mp4 -f srt -i subs.srt -map 0:v -map 0:a -map 1:0 -c copy -c:s mov_text output.mp4注意MP4容器对视认格式支持有限,一般是SRT转MOV_TEXT,样式基本都会丢失。所以需要保留进度、描边等复杂样式的软字幕,推荐用MKV容器配合ASS字幕。
4.3 到底选哪种:分发平台决定一切
我的选择逻辑很明确,可以直接抄作业:
- 要发短视频平台、给客户审片、导出到手机相册:选硬字幕,保证任何人打开都能看到。
- 自己收藏、做双语字幕、存无损片源:选软字幕,保留完整画质和可编辑性。
- 多语言字幕版本:先做软字幕确认时间轴无误,再统一烧录多个语言版本,避免每改一次就渲染一次。
不要一上来就硬字幕。我的习惯是:视频ER先做一份软字幕预览,确认时间轴和文字没问题后,再跑一遍硬字幕流程。这样既不浪费渲染时间,又能保证成品质量。
4.4 转码参数怎么取舍
转码参数里最影响观感的是-crf。CRF值越低画质越好,默认23已经不错,我一般用18,属于接近视觉无损的水平。18到23之间,数值越小文件越大。短视频不值得用超高码率,18足够。
CPU强的话用-preset slow,压缩率更高,文件更小;CPU不够时用medium也行。音频方面,如果源文件音轨本身是AAC,直接-c:a copy保原样;如果需要重新编码,建议-c:a aac -b:a 192k,这个码率在大多数平台听感都够。
5. 批量生产:让脚本自动处理整个素材文件夹
5.1 一个脚本管理从识别到烧录的完整流程
当你有几十个视频要加字幕时,手工一个接一个跑命令肯定不现实。我把整个流程攒成一个Python脚本,遍历指定文件夹里的所有MP4或MOV文件,逐个生成字幕并烧录。
脚本骨架差不多是这样:
import subprocess from pathlib import Path INPUT_DIR = Path("./raw") SRT_DIR = Path("./captions") ASS_DIR = Path("./captions") OUTPUT_DIR = Path("./rendered") for video in INPUT_DIR.glob("*.mp4"): stem = video.stem wav_path = SRT_DIR / f"{stem}.wav" srt_path = SRT_DIR / f"{stem}.srt" ass_path = ASS_DIR / f"{stem}.ass" out_path = OUTPUT_DIR / f"{stem}_burned.mp4" # 1. 抽音频 subprocess.run([ "ffmpeg", "-y", "-i", str(video), "-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1", str(wav_path) ], check=True) # 2. 语音识别生成 srt(实际会用 faster_whisper 接口) # 3. srt 转 ass # 4. 烧录硬字幕 subprocess.run([ "ffmpeg", "-y", "-i", str(video), "-vf", f"ass={ass_path}", "-c:v", "libx264", "-crf", "18", "-preset", "medium", "-c:a", "copy", str(out_path) ], check=True)这个脚本理解起来不难。每个步骤用subprocess.run调用FFmpeg,check=True保证任何一步出错都会立刻报出来,不会带病往下走。
5.2 输出目录和命名规范别忽略
批量处理时,最怕的就是把原料覆盖掉。所以我的目录设计一直是这样:
raw/:原始视频,永远不动。captions/:中间产物,音频、SRT、ASS都放这里。rendered/:最终成品,带_burned后缀。
这样设计的好处是,任何一步想重跑,都有中间文件可以直接复用。比如字幕改了文本,只需要从步骤4重新渲染,不用重新识别语音。
5.3 CPU和内存怎么分配
faster-whisper在CPU上跑的时候,默认会占用不少资源。如果你的电脑是八核以上,可以并行处理两个视频;如果是普通笔记本,我还是建议老老实实串行跑。
另外长视频识别很容易把内存吃满。我的处理办法是先把长视频按15分钟一段切分,识别完把时间戳加上偏移量再拼接:
ffmpeg -i input.mp4 -ss 00:00:00 -t 00:15:00 -vn -ar 16000 -ac 1 chunk1.wav ffmpeg -i input.mp4 -ss 00:15:00 -t 00:15:00 -vn -ar 16000 -ac 1 chunk2.wav识别第二段时,所有分段时间统一加上900秒的偏移量,再写入完整SRT。这个方法在对接访谈、课程录屏这类超长视频时几乎必用。
6. 字幕不只是文字:时间轴、排版和视觉细节
6.1 每行字幕到底应该显示多久
字幕显示时间需要平衡两个极端:太短看不清,太长观众会重读或者干脆忽略文本。我自己的经验值如下:
- 最短不要少于0.8秒,否则观众还没反应过来字幕就没了。
- 最长不要超过7秒,超过之后阅读压力变大。
- 中文单行控制在12到18个汉字,两行封顶。
- 时间轴的断点尽量放在句子自然停顿处,而不是硬切。
用whisper识别出来的片段之间通常有停顿,正好可以利用。如果停顿太短,我这个合并思路是:相邻两段字幕间隔小于0.4秒时,直接合并成一条;合并后的总时长超过7秒或总字数超过36个字,就放弃合并。
6.2 中文断句和换行策略
中文没有空格,断句全看标点和语义。识别模型在语气词、连接词附近经常出错,所以断句要保守一点。我的原则是:
- 优先按句号、逗号切分。
- 没有标点时,超过18字后在语气词或动词短语后断开。
- 两行字幕时,上面一行尽量短一点,避免把画面主体完全挡住。
- 断句时不切开完整的词语,比如“人工智能”不会断成“人工/智能”。
这些细节决定了字幕是“舒服”还是“让人总想暂停”,属于那种自动化工具做完之后仍然需要人工目光过一遍的地方。
6.3 字体、描边和阴影不是摆设
字幕可读性不只是字体大小的问题。白墙、天空、雪景这些高亮度画面里,如果没有描边,字幕就会“融”进背景。ASS样式里三个关键参数直接影响可读性:
Outline:描边宽度,1920x1080分辨率下我常用2到3。Shadow:阴影距离,1到2就够,太大会显得脏。MarginV:底部边距,40像素左右比较合适,不要在画面最边缘。
字体选择方面,中文场景优先思源黑体、思源宋体、Noto Sans CJK这类开源中文字体。不要用什么纯西文字体渲染中文,容易缺字形,烧录时全是方框。
7. 实操踩坑记录与对应解法
7.1 中文字体渲染成方块
症状很典型:字幕文件在播放器里预览完全正常,一跑FFmpeg烧录,画面上的中文全变成方块。原因基本是libass找不到中文字体。
我的解决方法是,在ASS样式里直接把字体名写成系统中存在的名字,同时在FFmpeg所在目录放好字体文件。如果是Windows环境,字体名经常匹配不上,不如直接用字体文件名作为Fontname,比如Fontname: Noto Sans CJK SC,确保libass能找到。
某些FFmpeg版本还支持在ass滤镜后追加fontsdir参数,指定自定义字体目录。运行ffmpeg -h filter=ass可以看到它支持的选项,如果有,就把字体文件统一丢进一个目录,再在滤镜里显式指定,问题会好解决很多。
7.2 字幕时间戳重叠导致闪烁
SRT转ASS时,毫秒转厘秒的四舍五入会产生一个很隐蔽的bug:一句的结束时间和下一句的开始时间重叠,播放时会看到字幕闪烁或者两行同时出现。
我在转换脚本里加了一个强制修正逻辑:
for i in range(1, len(entries)): if entries[i]["start"] < entries[i - 1]["end"]: entries[i]["start"] = entries[i - 1]["end"] + 0.01这个0.01秒的间隔足够避免重叠,又不会让观众察觉。转换完ASS之后,再跑一遍检查脚本,确认每一行的Start都大于等于上一行的End,才放心进入渲染环节。
7.3 识别结果掺杂语气词和错别字
faster-whisper不会自动清理“嗯”“啊”“这个”这类语气词,专业名词也容易识别成同音字。我的做法很简单,在脚本里维护一个替换表:
REPLACES = [ ("嗯", ""), ("啊", ""), ("这个这个", "这个"), ("人工智能", "AI"), # 按项目需要自行补全 ]同时,调用Whisper时通过initial_prompt把视频主题词写进去,识别准确率能立竿见影地提升。这个方法成本极低,但大多数教程都没提到。
7.4 背景音乐把语音识别带偏
背景音乐一响,识别很容易断句错乱。vad_filter=True能滤掉一部分静音,但解决不了音乐里的唱词和人声重叠问题。如果视频里人声清晰度尚可,我一般会在抽音频这步加上轻量的高通和低通滤波,压掉低频BGM带来的人声浑浊感。
如果人声和音乐分离度实在太差,那就别指望自动识别一次出准确结果了。先识别生成大体时间轴,再让助手人工过一遍文字,或者直接用剪辑软件自带的识别功能做二次对比,把自动流程当作草稿生成器来用,效率依然远高于从零开始。
以我现在的固定流程来说,每期视频的大致顺序是:抽音频,识别,脚本清理文本,生成SRT,转ASS并检查时间戳,最后烧录硬字幕。整套跑顺之后,我已经很久没有回到手工一条条加字幕的老路上去了。字幕制作这件事,说到底就是一次“把重复劳动交给数据”的实践。你只要能看明白这个逻辑,再上手去跑一遍,之前的效率瓶颈基本就能直接翻过去。