MoneyPrinterTurbo 字幕生成机制详解:edge 与 whisper 双引擎的配置切换与源码实现
MoneyPrinterTurbo 的视频流水线中,字幕是从"配音音频"到"成片合成"之间承上启下的一环:它既要为视频合成提供带时间轴的 SRT 文本,又要保证每句字幕与 AI 生成的视频文案逐字对应。本篇以仓库文档 subtitle-generation 为主线,讲清楚 edge 与 whisper 两种字幕生成方案的取舍与切换方式,并深入到 app/services/subtitle.py、app/services/voice.py、app/services/task.py 的源码,解析两种方案的真实调用链、whisper 本地模型部署路径以及字幕纠错算法的实现细节。
两种字幕生成方案:edge 与 whisper
MoneyPrinterTurbo 当前支持 2 种字幕生成方式:
- edge:生成速度快,性能更好,对电脑配置没有要求,但是质量可能不稳定;
- whisper:生成速度慢,性能较差,对电脑配置有一定要求,但是质量更可靠。
官方建议优先使用 edge 模式,只有在生成的字幕质量不好时,再切换到 whisper 模式。这个建议背后有源码依据:从 app/services/task.py 的 generate_subtitle 调用链可以看到,edge 方案本质上是"复用 TTS 引擎返回的逐词时间戳"来反推字幕,不做任何音频识别,因此快且零硬件门槛;而 whisper 方案则需要对整个音频做一次完整的语音识别,成本明显更高。
切换方式只需修改 config.toml 配置文件中的 subtitle_provider 项。示例配置位于 config.example.toml:
# Subtitle Provider, "edge" or "whisper"
# If empty, the subtitle will not be generated
subtitle_provider = "edge"
两个注意事项:
- whisper 模式下需要到 HuggingFace 下载一个模型文件,大约 3GB 左右,请确保网络通畅;
- 如果
subtitle_provider留空,表示不生成字幕。
从 app/services/task.py 的源码看,这一"留空即关闭"的行为与实现完全对应:generate_subtitle 中只有当 subtitle_provider 等于 "edge" 或 "whisper" 时才会进入生成分支,留空时两条分支都不会执行,最终 file_to_subtitles 读不到有效内容,函数返回空字符串,字幕流程被整体跳过。此外还有一个文档未强调但源码可见的行为:当 subtitle_provider = "edge" 时,若 edge 方案未能写出字幕文件(os.path.exists(subtitle_path) 为 False),任务会自动回退(fallback)到 whisper 重新生成,日志中会打印 subtitle file not found, fallback to whisper。
whisper 本地模型部署
由于国内网络环境可能无法直接访问 HuggingFace,使用 whisper 方案前需要手动部署模型文件。文档给出的下载地址:
- 百度网盘: https://pan.baidu.com/s/11h3Q6tsDtjQKTjUu3sc5cA?pwd=xjs9
- 夸克网盘:https://pan.quark.cn/s/3ee3d991d64b
模型下载后解压,将整个目录放到项目根目录下的 models 文件夹里,最终路径应为 MoneyPrinterTurbo/models/whisper-large-v3,目录结构如下:
MoneyPrinterTurbo
├─models
│ └─whisper-large-v3
│ config.json
│ model.bin
│ preprocessor_config.json
│ tokenizer.json
│ vocabulary.json
这个路径约定不是约定俗成,而是写死在源码里的:app/services/subtitle.py 中 create 函数在首次加载模型时,优先查找 {项目根目录}/models/whisper-{model_size},并校验目录内是否存在 model.bin;只有当本地目录或 model.bin 缺失时,才会把 model_size 原样传给 faster_whisper,由它尝试联网从 HuggingFace 下载:
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
如果联网下载失败,日志会给出明确指引(见 app/services/subtitle.py):please download the model manually and put it in the 'models' folder。
whisper 运行参数配置
除了 subtitle_provider,config.toml 中还有一个 [whisper] 配置段,仅在 subtitle_provider 为 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 的模块加载阶段被读取,并带有默认值:model_size 默认 large-v3、device 默认 cpu、compute_type 默认 int8,与示例配置一致。值得注意的是模型实例是模块级单例(global model),进程内只在第一次调用时加载一次,后续任务复用,这解释了文档中"whisper 对电脑配置有一定要求"——large-v3 级别的模型在纯 CPU + INT8 下推理一次完整音频耗时较长,而 cuda + float16 的组合则要求本机具备 NVIDIA GPU。
edge 模式实现原理:用 TTS 词边界反推字幕
edge 模式不走音频识别,而是"搭" TTS 合成的便车。调用链为:generate_subtitle(app/services/task.py)→ voice.create_subtitle(text=video_script, sub_maker=..., subtitle_file=...)(app/services/voice.py)。
其核心机制有三点:
- 词级时间戳来源:TTS 引擎(如 Azure Speech SDK 分支)在合成时会请求
RequestWordBoundary事件,每个词回调中携带evt.audio_offset(起始偏移)与evt.duration(持续时长)。这些事件被累积到sub_maker.offset/sub_maker.subs中,字幕的起止时间直接来自 TTS 音频的偏移量,天然与音画同步; - 逐行匹配脚本:
create_subtitle先把视频文案用utils.split_string_by_punctuations(app/utils/utils.py)按标点拆分成句子数组script_lines,再依次累加 TTS 词文本,通过三级match_line判定累加结果是否"命中"当前脚本行——先做完全相等,再去掉非单词字符后比较,最后去掉所有\W字符后比较。命中即输出一条带时间戳的 SRT 条目,并把起始时间重置; - 全量校验后才落盘:只有当生成的字幕条数与脚本句数完全相等(
len(sub_items) == len(script_lines))时才写入 SRT 文件;否则仅打印 warning 并丢弃结果。这就是 edge 模式"质量可能不稳定"的具体表现——只要 TTS 的分句与文案标点切句有一处对不齐,整个 edge 结果作废,随后由task.py中的回退逻辑接管,改用 whisper 重新生成。
whisper 模式实现原理:识别、切句与 SRT 输出
whisper 方案的入口是 app/services/subtitle.py 的 create(audio_file, subtitle_file),它基于 faster_whisper 库加载本地模型并转写音频:
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 使用 5 束搜索提升识别准确率;word_timestamps=True 要求模型输出逐词时间戳——这是后续"按标点切句"的前提;vad_filter=True 开启语音活动检测过滤静音段,min_silence_duration_ms=500 表示不足 500ms 的静音不会触发分段。识别完成后会打印检测到的语言及其置信度(info.language / info.language_probability)。
拿到逐词时间戳后,代码并不直接使用 whisper 原始的分段,而是按标点重新切句(app/services/subtitle.py):逐词累加文本,一旦某个词包含标点(utils.str_contains_punctuation 会检查 app/models/const.py 定义的标点集合),就把当前片段定稿为一条字幕 {msg, start_time, end_time}。这保证了每条字幕以完整句子为单位显示,与 edge 模式和文案脚本的切句逻辑保持一致。
最终,每条字幕经 app/utils/utils.py 的 text_to_srt 格式化为标准 SRT 结构(序号、HH:MM:SS,mmm --> HH:MM:SS,mmm 时间轴、文本行),写入任务目录下的 subtitle.srt。file_to_subtitles(app/services/subtitle.py)则提供反向解析能力,用正则 ([0-9]*:[0-9]*:[0-9]*,[0-9]*) 提取时间轴行,供后续纠错与视频渲染使用。
字幕纠错:基于编辑距离对齐视频文案
whisper 识别出的字幕在文字内容上可能与 AI 生成的视频文案存在差异(识别错误、空格、标点)。为此 app/services/subtitle.py 实现了 correct(subtitle_file, video_script),在识别后对字幕文件做原地修正,流程如下:
- 解析 SRT 为
(序号, 时间轴, 文本)列表,同时把视频文案按标点拆成script_lines; - 双指针顺序对齐:脚本行与字幕行完全一致则直接通过;
- 不一致时做跨行合并——从当前字幕行开始向后拼接后续字幕行,只要拼接后与脚本行的相似度提升就继续合并(同时把时间轴的结束时间延长到最后一行的结束时间),以处理"一句话被 whisper 切成多条字幕"的情况;
- 相似度基于编辑距离计算:
levenshtein_distance(app/services/subtitle.py)实现经典的 DP 算法,similarity = 1 - 距离/最长文本长度,不区分大小写; - 合并后与脚本行相似度超过 0.8 时记为
Merged/Corrected,否则记为Mismatch——但两种情况下都会用脚本行文本替换字幕内容并保留时间轴,确保成片字幕逐字与配音文案一致; - 若脚本行有剩余而字幕已耗尽,剩余行会借用现有字幕时间轴或写入零时间轴占位;仅当发生任何修正时才重写文件,否则打印
Subtitle is correct。
该函数由 app/services/task.py 在 whisper 生成字幕后立即调用;app/services/subtitle.py 底部的 __main__ 入口则提供了一个独立调试方式:指定 task_id,读取任务目录中的 subtitle.srt 与 script.json,单独运行纠错与重新生成。
字幕在流水线中的位置与后续渲染
从 app/services/task.py 的主流程看,字幕是任务的第四阶段:生成脚本 → 生成音频(TTS,产出 sub_maker 与 audio.mp3)→ 生成字幕(generate_subtitle)→ 下载素材与合成视频。generate_subtitle 还支持 stop_at = "subtitle" 的调试模式,即任务到字幕为止直接返回 subtitle_path,便于单独验证字幕产物。
字幕生成后,app/services/video.py 的 generate_video 使用 moviepy 的 SubtitlesClip 解析 subtitle.srt,再为每条字幕构建 TextClip 叠加到视频上:默认字体 STHeitiMedium.ttc,位置支持 bottom(默认,距底部约 5% 高度)、top、center 与 custom(按百分比 Y 坐标并约束在屏幕安全区内),这些外观参数在请求模型 app/models/schema.py 中定义为 subtitle_enabled(默认 True)与 subtitle_position(默认 "bottom")等字段,可通过 API 或 WebUI 请求逐项控制。
实践建议与故障排查
结合文档与源码,整理出以下可操作的结论:
| 场景 | 建议 |
|---|---|
| 日常生成、无 GPU | subtitle_provider = "edge",速度快、零硬件要求 |
| edge 字幕质量不佳(句数对不齐导致回退) | 切换 subtitle_provider = "whisper" |
| 国内环境使用 whisper | 按文档网盘地址下载 whisper-large-v3,解压至 models/whisper-large-v3,确保其中存在 model.bin |
| 有 NVIDIA GPU | 将 [whisper] 段配置为 device="cuda"、compute_type="float16"(或 int8_float16) |
| 不需要字幕 | subtitle_provider 留空,或请求参数中关闭 subtitle_enabled |
常见故障可对照源码日志定位:模型加载失败会打印 failed to load model 并提示检查 models 目录(网络不通导致 HuggingFace 下载失败是最常见原因);edge 匹配失败会打印 failed, sub_items len: X, script_lines len: Y,此时任务自动回退 whisper;whisper 生成后会依次出现 correcting subtitle 与 Merged/Corrected / Mismatch / Subtitle is correct 日志,可据此判断纠错是否实际生效。
适用前提:以上行为基于当前仓库代码(faster_whisper 依赖、默认 large-v3 + CPU + int8),修改 [whisper] 配置后需重启服务才会生效,因为模型参数在模块导入时读取且模型实例在进程内复用。
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