MoneyPrinterTurbo 字幕生成实战:edge 与 whisper 双引擎的切换、配置与源码解析
在 MoneyPrinterTurbo 的自动化短视频工作流中,字幕是语音(TTS)与成片之间承上启下的一环:既要保证文字与口播逐句对齐,又要控制生成耗时与对机器配置的要求。本文围绕项目文档 subtitle-generation 讲解的两种字幕生成方式(edge 与 whisper)展开,结合 config.example.toml 的配置项、app/services/subtitle.py 与 app/services/voice.py 的源码实现,说明 subtitle_provider 的切换方法、两种引擎的底层原理、whisper 模型文件的准备方式,以及字幕修正与最终渲染到成片的完整链路。读完本篇,你可以独立完成字幕引擎的选择与调优,并理解 subtitle.srt 文件从产生到烧录进视频的每一步。
一、两种字幕生成方式:edge 与 whisper
文档给出了当前支持的两种字幕生成方式,并明确推荐优先使用 edge、质量不佳时再切换到 whisper:
| 方式 | 生成速度 | 性能开销 | 硬件要求 | 质量 |
|---|---|---|---|---|
edge |
快 | 低 | 无特殊要求 | 可能不稳定 |
whisper |
慢 | 较高 | 对电脑配置有一定要求 | 更可靠 |
两者在实现上完全不同,可以从源码中印证这一差异:
- edge 模式不“听”音频,而是复用 TTS 返回的词边界时间戳。项目通过
edge_tts合成语音时,会同时捕获服务端的WordBoundary事件,得到每个词的偏移量和时长,直接据此拼出字幕(见下文第二节)。因此它对电脑配置几乎没有要求,速度快,但字幕内容依赖 TTS 服务返回的切分结果,质量“可能不稳定”。 - whisper 模式是真正的离线语音识别。它加载 faster-whisper 模型对生成的
audio.mp3做转写,再用脚本原文对识别结果做相似度修正(见第三、四节)。质量更可靠,但需要加载约 3GB 的模型(large-v3),转写速度较慢。
二、配置切换:subtitle_provider 与 whisper 参数
2.1 在 config.toml 中切换引擎
切换方式就是修改 config.toml 中 [app] 段的 subtitle_provider:
[app]
# Subtitle Provider, "edge" or "whisper"
# If empty, the subtitle will not be generated
subtitle_provider = "edge"
三个取值的行为如下(取值逻辑见 app/services/task.py 中的 generate_subtitle):
| 取值 | 行为 |
|---|---|
"edge" |
用 TTS 词边界生成字幕;若最终没产生字幕文件,自动回退到 whisper |
"whisper" |
用 faster-whisper 对音频转写,并执行字幕修正 |
留空("") |
不生成任何字幕 |
文档明确建议:“推荐使用 edge 模式,如果生成的字幕质量不满意,再切换到 whisper 模式”(It is recommended to use edge mode, and switch to whisper mode if the quality of the subtitles generated is not satisfactory)。
需要说明的是,subtitle_provider 只是全局默认值;任务参数中的 subtitle_enabled 为 false 时,无论该配置取何值都会跳过字幕生成(generate_subtitle 第一行即 if not params.subtitle_enabled: return "")。
配置文件本身的加载逻辑在 app/config/config.py:程序启动时若根目录下没有 config.toml,会自动复制 config.example.toml 生成它,再按 app / whisper / proxy 等段解析,因此只需对照示例文件修改即可。
2.2 [whisper] 段参数
仅当 subtitle_provider = "whisper" 时,[whisper] 段生效。config.example.toml 中给出了完整示例:
[whisper]
# Only effective when subtitle_provider is "whisper"
# Run on GPU with FP16
# model = WhisperModel(model_size, device="cuda", compute_type="float16")
# Run on GPU with INT8
# model = WhisperModel(model_size, device="cuda", compute_type="int8_float16")
# Run on CPU with INT8
# model = WhisperModel(model_size, device="cpu", compute_type="int8")
# recommended model_size: "large-v3"
model_size="large-v3"
# if you want to use GPU, set device="cuda"
device="CPU"
compute_type="int8"
这三个参数在 app/services/subtitle.py 中被读取,并在模型加载时传入 WhisperModel:
| 参数 | 默认值(源码兜底) | 说明 |
|---|---|---|
model_size |
large-v3 |
模型规格;注释推荐的 GPU 组合为 cuda + float16 或 cuda + int8_float16,CPU 组合为 cpu + int8 |
device |
cpu |
运行设备,用 GPU 时设为 cuda |
compute_type |
int8 |
计算精度类型 |
注意源码中的默认值是小写的 cpu,而示例配置里写的是 device="CPU",两者配合 faster_whisper.WhisperModel 的容错均可工作;若使用 GPU,请按注释改成 device="cuda" 并搭配对应的 compute_type。
三、edge 模式:复用 TTS 词边界生成字幕
edge 模式的入口在 app/services/task.py:
subtitle_fallback = False
if subtitle_provider == "edge":
voice.create_subtitle(
text=video_script, sub_maker=sub_maker, subtitle_file=subtitle_path
)
if not os.path.exists(subtitle_path):
subtitle_fallback = True
logger.warning("subtitle file not found, fallback to whisper")
if subtitle_provider == "whisper" or subtitle_fallback:
subtitle.create(audio_file=audio_file, subtitle_file=subtitle_path)
logger.info("\n\n## correcting subtitle")
subtitle.correct(subtitle_file=subtitle_path, video_script=video_script)
可以看到 edge 模式有一个重要的兜底行为:只有当字幕文件确实生成失败时才回退 whisper,回退后同样会执行 whisper 修正。
3.1 词边界从哪来:tts 返回 SubMaker
sub_maker 并非字幕阶段的产物,而是 TTS 阶段就准备好的。app/services/voice.py 的 azure_tts_v1 在流式接收 edge_tts.Communicate 输出时,除了写入音频块,还拦截 WordBoundary 事件:
async for chunk in communicate.stream():
if chunk["type"] == "audio":
file.write(chunk["data"])
elif chunk["type"] == "WordBoundary":
sub_maker.create_sub(
(chunk["offset"], chunk["duration"]), chunk["text"]
)
其中 offset 与 duration 是 100 纳秒为单位的 Tick(这也是 get_audio_duration 用最后一个 offset 除以 10000000 得到秒数的原因)。因此 edge 模式的“零成本”体现在:音频与字幕时间轴是同一次 TTS 请求的副产品,不需要二次识别。
3.2 字幕文本与脚本逐行对齐
word 时间戳只有“逐词”粒度,而 SRT 需要“逐句”:create_subtitle 先用 utils.split_string_by_punctuations 把脚本按标点切成行(小数点如 2.5% 中的 . 不会被误判为断句),再把 TTS 返回的词逐个拼接到 sub_line,并与脚本行做三级匹配(原文相等 → 去标点符号后相等 → 去所有非单词字符后相等)。只有当前缀累积到恰好匹配某一行时,才用该行的首词 offset 作开始时间、末词 offset 作结束时间输出一条 SRT 条目。
这里还有一个“全有或全无”的校验:
if len(sub_items) == len(script_lines):
with open(subtitle_file, "w", encoding="utf-8") as file:
file.write("\n".join(sub_items) + "\n")
...
else:
logger.warning(
f"failed, sub_items len: {len(sub_items)}, script_lines len: {len(script_lines)}"
)
即:匹配成功的行数必须等于脚本总行数,字幕文件才会落盘;否则只打印告警、不写文件,从而触发上文 task.py 中的 whisper 回退。这正是文档所说 edge 模式“质量可能不稳定”的具体表现——任何一次 TTS 词切分与脚本对不上,整条链路就换引擎重来。
四、whisper 模式:本地模型转写与 SRT 生成
4.1 模型加载:本地优先,失败可诊断
app/services/subtitle.py 对 WhisperModel 做了惰性单例加载,并且优先使用本地模型目录:
model_path = f"{utils.root_dir()}/models/whisper-{model_size}"
model_bin_file = f"{model_path}/model.bin"
if not os.path.isdir(model_path) or not os.path.isfile(model_bin_file):
model_path = model_size # 交给 faster-whisper 自行(联网)下载
这意味着如果你已把模型手动放到项目根目录的 models/ 下,程序会直接加载本地文件;否则 WhisperModel(model_size) 会走 HuggingFace 在线下载。中文文档 zh/guide/subtitle-generation 说明了模型文件约 3GB,并给出了国内网络环境的离线下载方式:下载 whisper-large-v3 后解压,整个目录放入 models/,最终目录结构应为:
MoneyPrinterTurbo
└─models
└─whisper-large-v3
├─ config.json
├─ model.bin
├─ preprocessor_config.json
├─ tokenizer.json
└─ vocabulary.json
判断标准就是上面源码检查的 models/whisper-{model_size}/model.bin 是否存在。若加载抛出异常,日志会明确提示可能是网络问题、建议手动下载模型放入 models 目录,并返回 None 让任务失败而不是崩溃(subtitle.py)。
4.2 转写参数与按标点断句
create 的转写调用固定了以下参数:
segments, info = model.transcribe(
audio_file,
beam_size=5,
word_timestamps=True,
vad_filter=True,
vad_parameters=dict(min_silence_duration_ms=500),
)
beam_size=5:束搜索宽度,兼顾准确率与速度;word_timestamps=True:开启词级时间戳,这是后续断句的基础;vad_filter=True且min_silence_duration_ms=500:先用 VAD 过滤静音段,缩短无效转写区间。
拿到每个 segment 后,代码并不直接使用 whisper 的句子边界,而是按词遍历、遇到标点即断句:
seg_text += word.word
if utils.str_contains_punctuation(word.word):
seg_text = seg_text[:-1] # 去掉末尾标点
recognized(seg_text, seg_start, seg_end)
is_segmented = False
seg_text = ""
断句依据的标点表是 app/models/const.py 中的 PUNCTUATIONS,由 utils.str_contains_punctuation 判定。同时源码还修正了时间边界:段首词早于 segment 起始、或段尾词晚于 segment 结束的情况,都以实际词的时间戳为准(subtitle.py),避免首尾出现多余空白。最终每条字幕经 utils.text_to_srt 格式化为标准 SRT(序号 / HH:MM:SS,mmm --> HH:MM:SS,mmm / 文本),写入 subtitle.srt。
五、字幕修正:用 Levenshtein 相似度对齐脚本原文
whisper(以及 edge 回退)生成字幕后,还会执行 subtitle.correct。动机很直接:ASR 识别出的文字与 LLM 生成的脚本可能不完全一致(同音字、断句差异),而字幕最终要“以脚本为准”。
核心算法分三步:
- 解析:file_to_subtitles 用正则
([0-9]*:[0-9]*:[0-9]*,[0-9]*)提取时间轴行,把 SRT 解析为(序号, 时间轴, 文本)三元组;脚本则按标点切成行。 - 相似度判定:用双行滚动数组实现的 levenshtein_distance 计算编辑距离,
similarity = 1 - distance / max_length。 - 合并与替换:若当前字幕行与脚本行不相等,代码会向后尝试合并相邻字幕行(“跑步是一项运动”被 ASR 拆成两条时,合并后与脚本行的相似度更高则纳入合并,结束时间顺延到最后一行的结束时间);随后若
similarity > 0.8记为 Merged/Corrected,否则记为 Mismatch——两种情况都会把字幕文本替换为脚本原文并保留合并后的时间轴。脚本剩余行还会兜底补进字幕(时间轴复用现有条目或置零)。
只要发生过修正,就重写整个 subtitle.srt(subtitle.py)。这一步是 whisper 模式“质量更可靠”的关键:识别只负责提供时间轴,文字内容以脚本为最终事实来源。
六、完整链路与成片渲染
把前面的模块串起来,task.py 主流程中字幕是第 4 步(在音频之后、素材下载之前),进度推进到 40%:
1. generate_script → 2. generate_terms → 3. generate_audio (edge_tts, 产出 audio.mp3 + sub_maker)
4. generate_subtitle (edge/whisper → subtitle.srt) → 5. 下载素材 → 6. generate_final_videos
注意 stop_at 机制支持在 "script" / "audio" / "subtitle" / "materials" 处提前结束任务,因此只生成字幕而不出片是受支持的调用方式。
字幕最终烧录进成片发生在 app/services/video.py 的 generate_video:当 params.subtitle_enabled 且 subtitle.srt 存在时,用 moviepy 的 SubtitlesClip 按 UTF-8 解析 SRT,对每条字幕生成 TextClip,支持 bottom / top / center / custom(按 custom_position 百分比定位并做边界钳制)等位置,叠加字体(默认 STHeitiMedium.ttc)、字号、描边、背景色等参数后再与视频合成。也就是说,前面两节产出的 subtitle.srt 是唯一的中间产物:引擎只负责“写对时间和文字”,渲染细节由 video 模块统一负责。
七、选型与排查清单
结合文档建议与源码行为,实操时可按以下清单处理:
- 默认用
subtitle_provider = "edge":无硬件门槛、速度快;TTS 词切分与脚本对不上时(日志出现sub_items len ... != script_lines len或subtitle file not found, fallback to whisper),会自动回退 whisper。 - 切换 whisper 时先备模型:把
whisper-large-v3(约 3GB)放到models/whisper-large-v3/(必须有model.bin),避免运行时联网下载失败;日志中的this may be caused by network issue即指向该问题。 - 按硬件调整
[whisper]段:CPU 用device="CPU" + compute_type="int8";有 NVIDIA GPU 时按注释改为device="cuda" + compute_type="float16"(或int8_float16)提速。 - 想不出字幕:把
subtitle_provider留空即可,任务流程会跳过第 4 步,generate_video也不会叠加任何文字轨道。 - 字幕与口播对不上:确认走的是修正链路——只有 whisper(或 edge 回退)路径会执行
subtitle.correct,edge 直接生成的字幕不做脚本对齐,这是两种模式质量差异的另一层来源。
以上路径均以当前仓库为准:配置文件为 config.example.toml(实际运行时的 config.toml 由它自动复制而来),核心实现集中在 app/services/subtitle.py、app/services/voice.py、app/services/task.py 与 app/services/video.py,可据此进一步深入阅读。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00