2026/9/26 9:23:57

Deep Code实战:用FFmpeg和Python搭建自动字幕生产线

Deep Code实战:用FFmpeg和Python搭建自动字幕生产线 做视频这几年我养成了一个习惯哪怕只是十几秒的短视频我也坚持用脚本生成字幕而不是在剪辑软件里一条条打字。原因很简单——字幕的本质是一份带时间轴的数据而凡是数据用代码处理都比手工可靠得多。这个习惯在我接触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 highpassf200,lowpassf8000去掉极低频率的轰头感和极高频的噪声能让人声更突出。2.3 语音识别本地Whisper和它的Python接口音频准备好之后就轮到语音识别出场。目前最方便的选择是faster-whisper它是OpenAI Whisper模型的高效实现对CPU比较友好也能在显卡上跑得更快。安装只需要一行命令pip install faster-whisper然后写一个最简单的转录脚本from faster_whisper import WhisperModel model WhisperModel(small, devicecpu, compute_typeint8) segments, info model.transcribe(audio_16k.wav, languagezh, vad_filterTrue) 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_typeint8CPU上运行时的定点化计算速度提升明显损失一点精度日常够用。vad_filterTrue自动过滤静音片段可以显著减少无意义的空白识别结果。运行之后你会得到一长串带开始时间和结束时间的文本片段。这就是字幕文件最原始的数据来源。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, start1): 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, encodingutf-8-sig) as f: f.write(\n.join(lines))这里我特意用了utf-8-sig编码因为Windows平台的老牌播放器对无BOM的UTF-8识别并不可靠加上BOM之后乱码问题能少一大半。3.2 为什么成产线里通常用ASS而不是SRTSRT能用的原因是最小可用但它几乎没有样式控制能力。你不能在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也可以直接填字体文件路径避免系统字体名不匹配导致渲染成方块。Alignment2表示底部居中这是大多数视频字幕的默认位置。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 asssubs.ass -c:v libx264 -crf 18 -preset slow -c:a copy output.mp4这里的关键是asssubs.ass这个滤镜它调用libass库把ASS字幕渲染成画面。如果你运行的时候提示找不到滤镜说明你用的FFmpeg没编译libass支持可以先验证一下ffmpeg -hide_banner -filters | grep ass看到输出里有ass这个滤镜说明没问题。没有的话换个带libass的构建版本或者用包管理器安装完整版FFmpeg。硬字幕的好处是兼容性无敌无论什么播放器、什么网站你看到的字幕就在画面里不可能消失。代价是修改成本高——改一个字也要重新渲染一遍整段视频。4.2 软字幕把字幕作为独立轨道封装软字幕则是把字幕文件和视频封装在同一个容器里播放时由播放器解码显示。字幕不是画面的一部分所以画质零损失而且可以随时开关、切换语言。封装方面我常用两个工具用mkvmerge封装MKVmkvmerge -o output.mkv input.mp4 subs.ass用FFmpeg封装MP4ffmpeg -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) ], checkTrue) # 2. 语音识别生成 srt实际会用 faster_whisper 接口 # 3. srt 转 ass # 4. 烧录硬字幕 subprocess.run([ ffmpeg, -y, -i, str(video), -vf, fass{ass_path}, -c:v, libx264, -crf, 18, -preset, medium, -c:a, copy, str(out_path) ], checkTrue)这个脚本理解起来不难。每个步骤用subprocess.run调用FFmpegcheckTrue保证任何一步出错都会立刻报出来不会带病往下走。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 filterass可以看到它支持的选项如果有就把字体文件统一丢进一个目录再在滤镜里显式指定问题会好解决很多。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_filterTrue能滤掉一部分静音但解决不了音乐里的唱词和人声重叠问题。如果视频里人声清晰度尚可我一般会在抽音频这步加上轻量的高通和低通滤波压掉低频BGM带来的人声浑浊感。如果人声和音乐分离度实在太差那就别指望自动识别一次出准确结果了。先识别生成大体时间轴再让助手人工过一遍文字或者直接用剪辑软件自带的识别功能做二次对比把自动流程当作草稿生成器来用效率依然远高于从零开始。以我现在的固定流程来说每期视频的大致顺序是抽音频识别脚本清理文本生成SRT转ASS并检查时间戳最后烧录硬字幕。整套跑顺之后我已经很久没有回到手工一条条加字幕的老路上去了。字幕制作这件事说到底就是一次“把重复劳动交给数据”的实践。你只要能看明白这个逻辑再上手去跑一遍之前的效率瓶颈基本就能直接翻过去。