首页
/ MoneyPrinterTurbo 字幕生成实战:edge 与 whisper 双引擎的切换、配置与源码解析

MoneyPrinterTurbo 字幕生成实战:edge 与 whisper 双引擎的切换、配置与源码解析

2026-09-04 16:56:34作者:齐冠琰

在 MoneyPrinterTurbo 的自动化短视频工作流中,字幕是语音(TTS)与成片之间承上启下的一环:既要保证文字与口播逐句对齐,又要控制生成耗时与对机器配置的要求。本文围绕项目文档 subtitle-generation 讲解的两种字幕生成方式(edge 与 whisper)展开,结合 config.example.toml 的配置项、app/services/subtitle.pyapp/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_enabledfalse 时,无论该配置取何值都会跳过字幕生成(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 + float16cuda + 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.pyazure_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"]
        )

其中 offsetduration 是 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.pyWhisperModel 做了惰性单例加载,并且优先使用本地模型目录

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=Truemin_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 生成的脚本可能不完全一致(同音字、断句差异),而字幕最终要“以脚本为准”。

核心算法分三步:

  1. 解析file_to_subtitles 用正则 ([0-9]*:[0-9]*:[0-9]*,[0-9]*) 提取时间轴行,把 SRT 解析为 (序号, 时间轴, 文本) 三元组;脚本则按标点切成行。
  2. 相似度判定:用双行滚动数组实现的 levenshtein_distance 计算编辑距离,similarity = 1 - distance / max_length
  3. 合并与替换:若当前字幕行与脚本行不相等,代码会向后尝试合并相邻字幕行(“跑步是一项运动”被 ASR 拆成两条时,合并后与脚本行的相似度更高则纳入合并,结束时间顺延到最后一行的结束时间);随后若 similarity > 0.8 记为 Merged/Corrected,否则记为 Mismatch——两种情况都会把字幕文本替换为脚本原文并保留合并后的时间轴。脚本剩余行还会兜底补进字幕(时间轴复用现有条目或置零)。

只要发生过修正,就重写整个 subtitle.srtsubtitle.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.pygenerate_video:当 params.subtitle_enabledsubtitle.srt 存在时,用 moviepy 的 SubtitlesClip 按 UTF-8 解析 SRT,对每条字幕生成 TextClip,支持 bottom / top / center / custom(按 custom_position 百分比定位并做边界钳制)等位置,叠加字体(默认 STHeitiMedium.ttc)、字号、描边、背景色等参数后再与视频合成。也就是说,前面两节产出的 subtitle.srt 是唯一的中间产物:引擎只负责“写对时间和文字”,渲染细节由 video 模块统一负责。

七、选型与排查清单

结合文档建议与源码行为,实操时可按以下清单处理:

  1. 默认用 subtitle_provider = "edge":无硬件门槛、速度快;TTS 词切分与脚本对不上时(日志出现 sub_items len ... != script_lines lensubtitle file not found, fallback to whisper),会自动回退 whisper。
  2. 切换 whisper 时先备模型:把 whisper-large-v3(约 3GB)放到 models/whisper-large-v3/(必须有 model.bin),避免运行时联网下载失败;日志中的 this may be caused by network issue 即指向该问题。
  3. 按硬件调整 [whisper]:CPU 用 device="CPU" + compute_type="int8";有 NVIDIA GPU 时按注释改为 device="cuda" + compute_type="float16"(或 int8_float16)提速。
  4. 想不出字幕:把 subtitle_provider 留空即可,任务流程会跳过第 4 步,generate_video 也不会叠加任何文字轨道。
  5. 字幕与口播对不上:确认走的是修正链路——只有 whisper(或 edge 回退)路径会执行 subtitle.correct,edge 直接生成的字幕不做脚本对齐,这是两种模式质量差异的另一层来源。

以上路径均以当前仓库为准:配置文件为 config.example.toml(实际运行时的 config.toml 由它自动复制而来),核心实现集中在 app/services/subtitle.pyapp/services/voice.pyapp/services/task.pyapp/services/video.py,可据此进一步深入阅读。

登录后查看全文
热门项目推荐
相关项目推荐