mcpField
W
wjs-dubbing-video

wjs-dubbing-video

github.com/jianshuo创意与媒体137181 次安装今天更新
代码仓库

关于 wjs-dubbing-video

wjs-dubbing-video 是将视频与目标语言 SRT 转为配音视频的技能,适用于需要为视频替换语言音轨的制作者。它按语音 ID 选择 TTS 引擎,并让配音音频与字幕时间及原视频长度对齐。

主要功能

  1. 01按语音 ID 路由 TTS 引擎
  2. 02逐条合成 SRT 配音
  3. 03超时音频加速或补静音
  4. 04保留原视频流并合成输出
  5. 05支持单人及多说话人配音

适用场景

  1. 01为视频添加目标语言旁白
  2. 02让访谈中的不同说话者使用不同声音

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jianshuo/claude-skills --skill wjs-dubbing-video

安装后在 Claude Code 中输入/wjs-dubbing-video确认已加载。

附带文件scripts/dub.pyscripts/visual_diarize.py

详细介绍

wjs-dubbing-video

视频 + 目标语言 SRT → *_<lang>_dub.mp4,并配上与时间轴对齐的 TTS 语音。此技能仅处理配音轨。 烧录字幕 + 混合音频底床属于下一项技能(/wjs-burning-subtitles/render.py 会通过一次最终编码合成所有内容)。

何时使用

  • 用户已有目标语言 SRT(例如 entrevista.zh-CN.srt),并希望视频能以该语言播放。
  • 用户说“中文配音 / 配音 / 帮我做配音 / dub it / voice over”。
  • 视频中有多位出镜者,且用户希望为每位说话者使用不同的声音。

何时不使用

  • 还没有 SRT → 先运行 /wjs-transcribing-audio,再运行 /wjs-translating-subtitles。
  • 仅使用源语言进行 TTS(较少见;通常会先翻译)→ 仍使用此技能,但传入源语言 SRT。
  • 仅烧录字幕,不更改音频 → 直接使用 /wjs-burning-subtitles。

说话者数量 — 默认一位

默认假设只有一位说话者。 整段配音使用同一个声音。这适用于独白、视频博客、录制的演讲、仅有旁白的片段,以及绝大多数用户提出需求的视频。不要进行说话者区分,不要在 SRT 中添加 [A] / [B] 标签,也不要提及多说话者的复杂性。

仅当用户明确提出时才切换为多说话者模式——例如“有两个人”“访谈”“对话”“两人之间的谈话”“区分说话者”“每个人使用不同的声音”,或直接要求进行说话者区分。触发后,请遵循下文的“多说话者配音”部分。

如果不确定视频中有一位还是多位说话者,先交付单一音色版本。之后添加说话者区分很简单(只需重新生成配音);默认交付混淆的多说话者版本会浪费用户时间。

引擎路由 — 按语音 ID

scripts/dub.py 会根据语音 ID 前缀自动路由:

语音 ID 模式引擎身份验证
zh_..._bigttsVolcano(字节跳动豆包)TTSVOLC_TTS_APPID + VOLC_TTS_ACCESS_TOKEN
zh-CN-...Neural / en-US-...Neural / 等edge-tts(Microsoft Edge 神经语音)无(免费)

对于普通话,Volcano 的自然度明显胜过 edge-tts,尤其适用于富有情感或沉思性的内容。没有 Volcano 凭据时,或作为调试备用方案时,使用 edge-tts。

Volcano TTS(仅中文)

端点:https://openspeech.bytedance.com/api/v3/tts/unidirectional(TTS 1.0 和 2.0 均使用此端点;Resource-Id 请求头用于选择后端)。

请求头:

X-Api-App-Id:       (env: VOLC_TTS_APPID)         # 10-digit speech App ID
X-Api-Access-Key:   (env: VOLC_TTS_ACCESS_TOKEN)  # 32-char token from speech console
X-Api-Resource-Id:  volc.service_type.10029       # see resource ID note below
Content-Type:       application/json

加载凭据:大多数用户将凭据保存在 ~/code/.env 中。每次会话开始时,通过以下方式读取:

set -a; source ~/code/.env; set +a
Resource ID — 一个重要的特殊情况

文档将 seed-tts-2.0 列为“TTS 2.0(推荐)”资源,但典型的 TTS-SeedTTS2.0 控制台实例并不包含常见的 *_bigtts 说话者目录(爽快斯斯、高冷御姐、开朗姐姐等)。使用这些说话者调用 seed-tts-2.0 会返回 200 code=55000000 "resource ID is mismatched with speaker related resource"。解决方法是使用 volc.service_type.10029(TTS 1.0 V3 端点)——bigtts 说话者的音质完全相同,而且都能使用此资源。随附的 dub.py 默认使用 volc.service_type.10029;如果你使用的是其他实例,可通过 VOLC_TTS_RESOURCE 环境变量覆盖。

其他 401/403 错误:

  • 401 code=45000010 "load grant: requested grant not found in SaaS storage" — App ID + 密钥组合可通过网关验证,但用户尚未启用此资源。用户必须前往火山引擎 → 语音技术 → 语音合成大模型 → 实例管理并开通服务。没有其他解决办法。
  • 403 code=45000030 — 用户的实例套餐中不包含该说话者。
响应格式

尽管文档表述比较随意,响应实际为流式 NDJSON,不是单个 JSON 对象,也不是原始音频字节。每一行都是独立的 JSON 事件,其中 data 包含经 Base64 编码的 MP3 音频块。结束事件的 code 为 20000000(在此 API 的成功代码中表示成功,不同于 code: 0)。将解码后的音频块拼接起来即可得到完整的 MP3。

import base64, json, requests
audio = b""
r = requests.post(url, headers=h, json=payload, timeout=60, stream=True)
for line in r.iter_lines():
    if not line: continue
    evt = json.loads(line)
    if evt.get("code") not in (0, None, 20000000):
        raise RuntimeError(f"code={evt.get('code')} {evt.get('message')}")
    if evt.get("data"):
        audio += base64.b64decode(evt["data"])
说话人目录(已确认可在 volc.service_type.10029 下正常使用)

完整列表见 volcengine.com/docs/6561/1257544 — 但可用性取决于你的实例套餐。已确认可在典型的 SeedTTS-2.0 入门实例中使用的女声:

说话人 ID中文名感觉
zh_female_gaolengyujie_moon_bigtts高冷御姐最适合沉思、灵性内容。 成熟、克制、平静。
zh_female_kailangjiejie_moon_bigtts开朗姐姐温暖的大姐姐讲述风格。
zh_female_shuangkuaisisi_moon_bigtts爽快斯斯百搭、对话感强,适合作为基准。
zh_female_linjianvhai_moon_bigtts邻家女孩轻松随意,适合生活类 vlog。
zh_female_yuanqinvyou_moon_bigtts元气女友活泼、积极。
zh_female_meilinvyou_moon_bigtts美丽女友柔和、亲密。
zh_female_shuangkuaisisi_emo_v2_mars_bigtts斯斯情感版情感表现力丰富 — 搭配明确的情绪和强度使用。

尽管文档列出了以下声音,但在典型实例中调用时会返回 55000000:vv_uranus_bigtts、wenroushunv_moon_bigtts、qingxin_moon_bigtts、yingmaoxiaoyuan_moon_bigtts、tianxinxiaoling_moon_bigtts、shaoergushi_moon_bigtts。未经测试,不要承诺这些声音可用。

音频参数

speech_rate 是 Volcano 原生的刻度 [-50, +100],数值表示百分比变化(因此,-8 表示慢 8%)。脚本会将 --rate -8% 作为 -8 传递。

实用的情绪预设:

  • emotion="calm"、emotion_scale=4 — 沉思风格,是此技能灵性内容细分领域的默认设置。
  • emotion="gentle" — 更柔和、更亲密。
  • emotion="neutral" — 平淡、信息传达风格。
  • emotion="sad" — 忧郁风格。请谨慎使用。

无需修改代码,即可通过 VOLC_TTS_EMOTION 和 VOLC_TTS_EMOTION_SCALE 环境变量覆盖 dub.py 的默认设置。

此技能未配置任何 Volcano 英语声音 — 英语内容请使用 edge-tts(下一节)。Volcano 确实提供英语说话人(en_male_*_bigtts、en_female_*_bigtts),但它们通常不包含在 TTS-SeedTTS-2.0 入门实例中。确认可用后,可扩展 dub.py 中的声音路由来添加。

edge-tts(Microsoft Edge 神经网络 TTS)

免费,无需 API 密钥,音质出色,但表现力不如 Volcano。请安装到项目虚拟环境中 — 不要 每个片段都通过 uvx 调用。每次调用 uvx 都会启动一个全新的 Python 进程;快速连续调用几次后,Bing 端点会进行速率限制或重置连接,从而导致渲染中途失败。

uv venv .venv
uv pip install --python .venv/bin/python edge-tts

然后通过单个长期运行的 Python 进程直接调用 edge_tts.Communicate(...),并加入失败重试逻辑。随附的 scripts/dub.py 已实现此功能。

语音选择 — 匹配原说话人

跨语言匹配无法做到完美 — 请仔细选择性别、年龄感和语气,再通过语速和音高进行调整。

中文语音(优先使用 Volcano,备用 edge-tts)

Volcano 的 zh_female_gaolengyujie_moon_bigtts(高冷御姐,沉稳,speech_rate=-8)是成熟、沉思型女性说话者的验证基线,对于这类风格,其表现与任何 edge-tts 选项相当或更好。其余语音请参阅上方的 Volcano 说话者表格。

edge-tts 语音目录(中文):

语音性别默认风格
zh-CN-XiaoxiaoNeural女温暖,适合新闻/小说
zh-CN-XiaoyiNeural女活泼,年轻
zh-CN-YunjianNeural男激情,适合体育内容
zh-CN-YunxiNeural男阳光,活泼
zh-CN-YunyangNeural男专业新闻播报员
zh-HK-HiuMaanNeural女友好,略显成熟
zh-TW-HsiaoChenNeural女友好
英语语音(edge-tts 神经语音,均支持多语言)

以下所有语音都能流利地说美式/英式/澳大利亚英语;*Multilingual* 语音还可以处理西班牙语姓名、法语/意大利语借词等,不会出现误读。

语音性别默认风格
en-US-AvaMultilingualNeural女最适合温暖/成熟/关怀的风格——适合灵性或教练类内容
en-US-EmmaMultilingualNeural女开朗、健谈,较年轻
en-US-AndrewMultilingualNeural男温暖、自信、真诚
en-US-BrianMultilingualNeural男平易近人、随意
en-US-AriaNeural女清晰的新闻播报员
en-US-GuyNeural男稳重的男性新闻播报员
en-GB-SoniaNeural女英式女性(RP)
en-GB-RyanNeural男英式男性(RP)
en-AU-WilliamMultilingualNeural男澳大利亚男性
fr-FR-VivienneMultilingualNeural女成熟的欧洲女性,也能朗读英语

若要匹配成熟、沉思型的西班牙语女性说话者(此技能的典型用例),请从 en-US-AvaMultilingualNeural 开始,并设置 --rate -5% --pitch -3Hz。不要将新闻播报风格的 Aria 或 Guy 用于灵性内容——听起来会很生硬。

选择启发
  • 成熟、沉思型女性说话者(瑜伽/灵性/教练): zh-CN-XiaoxiaoNeural,搭配 --rate=-8% --pitch=-10Hz(或 Volcano gaolengyujie)。
  • 成熟的专业男性: zh-CN-YunyangNeural,搭配 --rate=-5%。避免使用 Yunjian/Yunxi(过于活跃)。
  • 年轻、随意的说话者: 使用默认设置;不调整音高。
  • 西式口音风格: 使用 *MultilingualNeural 语音之一。

提交前务必先试听

🛑 检查点 — 完整配音前先试听。 完整视频配音是成本最高的步骤(TTS API 调用 + atempo + ffmpeg 混流)。在对整个 SRT 运行 dub.py 前:

  1. 选取文本最长的字幕条目(最容易超时)和一条较短、较随意的字幕条目(检查音色)。
  2. 合成 3–4 组音色、语速和音高组合,每段 3–8 秒。
  3. 向用户展示音频面板并询问:“选哪个 voice?rate/pitch 要调吗?确认后我再跑全片。” 等待用户明确选择。

只有在用户一开始就指定了特定音色,且已经听过该音色在这个视频中的试听时,才跳过此检查点。

脚本中的 scripts/sample_voices.py(如果存在)正是为此提供的轻量封装;否则,使用与配音脚本相同的 Python 循环。

在新账户上使用任何 Volcano 音色前,必须先进行冒烟测试: 先用该说话人 ID 合成一条约 5 个词的字幕。只有冒烟测试返回非空 MP3 时,才能向用户推荐该音色。如果冒烟测试返回 401,且 code=45000010(“grant not found”),请告诉用户他们需要在火山引擎控制台开通资源 — 不要假装重试后就能用。

运行 dub.py

.venv/bin/python ~/.claude/skills/wjs-dubbing-video/scripts/dub.py [voice] [rate] [pitch]

# Mature Chinese contemplative female (Volcano):
.venv/bin/python ~/.claude/skills/wjs-dubbing-video/scripts/dub.py \
    zh_female_gaolengyujie_moon_bigtts -8% +0Hz

# Warm English caring female (edge-tts, multilingual):
.venv/bin/python ~/.claude/skills/wjs-dubbing-video/scripts/dub.py \
    en-US-AvaMultilingualNeural -5% -3Hz

# Default Chinese fallback (no Volcano creds needed):
.venv/bin/python ~/.claude/skills/wjs-dubbing-video/scripts/dub.py \
    zh-CN-XiaoxiaoNeural -8% -10Hz

该脚本:

  1. 读取 SRT(自动检测 *.zh-CN.srt、*.en.srt 等文件,也可以传入 --srt)。
  2. 在 dub_work/seg_NN.mp3 下为每条字幕合成一个 MP3。
  3. 使用 ffprobe 检查每段音频的实际时长。
  4. 对每条字幕:如果 TTS 音频长于 SRT 时段,则串联 atempo 滤镜来加速;如果较短,则在后面补入静音。
  5. 为 SRT 中的间隙以及末尾多余部分插入静音片段,使输出音频长度与源视频完全一致。
  6. 将新音频混流到 *_zh_dub.mp4 / *_en_dub.mp4,并通过 -c:v copy 保留原视频流。

输出:<source-stem>_<lang>_dub.mp4(例如 entrevista_zh_dub.mp4)。这是下一步 — /wjs-burning-subtitles/render.py — 的输入文件,该步骤会合成最终视频。

填补不自然的静音

表达相同内容时,普通话所需时间是西班牙语的 60–80%。在严格按字幕条目对齐时,这会导致大多数字幕条目末尾出现 2–4 秒的不自然静音。英语所需时间则接近西班牙语的 ~85%。以下三种方法效果依次增强:

  1. 降低原生 TTS 语速。 将 --rate 从 +0% 改为 -12% 或 -15%,可以生成清晰、自然的慢速语音(远胜于事后拉伸音频)。先尝试 -12%;对于非常沉思的内容,可试试 -15%/-20%。

  2. 对每条字幕进行轻微慢速拉伸。 如果某条字幕的 TTS 音频仍短于对应时段,可将 atempo 设为 0.82× 到 0.95× 之间。dub.py 会自动处理:当剩余时长 > 0.5 秒时,它会设置 atempo = max(0.82, tts_dur / target_dur),并在剩余部分补入静音。低于 0.82× 时,声音会开始听起来像被下了药;高于 0.92× 时,拉伸基本难以察觉。

  3. 扩充最难处理字幕条目的目标语言文本。 如果时段太长,即使以 0.82× 拉伸后仍留下 >2 秒静音,最妥善的解决方法就是延长译文。加入自然的普通话语气词(“嗯,”“其实”“也就是说”“你知道”),或将压缩的短语展开,表达完整含义。这会改变屏幕上的字幕,因此修改前请先征得用户同意。编辑 SRT 后,删除对应片段的 dub_work/seg_NN.mp3 并重新运行 dub.py,只重新生成这些片段。

组合使用这些方法:原生语速 -12% + 拉伸至适配时长,可以处理约 80% 的情况。只有对 2–3 个最极端的条目才扩充文本。

多说话人配音(可选)

只有当用户明确表示源视频中有多个说话人时,才执行本节(例如“采访”“两个人”“对话”“区分说话人”“每个人用不同音色”,或直接要求进行说话人分离)。

触发后,为每位说话人使用不同音色生成配音,让听众能分辨是谁在说话。有两种方案:

路径 1(推荐用于出镜说话者):视觉说话人分离

scripts/visual_diarize.py 会逐帧监测每张脸的嘴部动作,并为每条字幕标记主要说话者。无需 API 密钥或音频指纹识别,可独立运行。

uv pip install --python .venv/bin/python mediapipe opencv-python

.venv/bin/python ~/.claude/skills/wjs-dubbing-video/scripts/visual_diarize.py \
    --video input.mp4 --srt input.en.srt \
    --out input.en.diarized.srt \
    --report diarization_report.json \
    --sample-fps 5 --num-speakers 2

工作原理:

  1. 每秒采样 N 帧(默认为 5)。
  2. 使用 MediaPipe FaceLandmarker(Tasks API),每帧最多检测 --num-speakers 张脸,每张脸 478 个关键点。
  3. 通过内侧上唇(idx 13)与内侧下唇(idx 14)之间的垂直距离,测量每张脸的嘴部张开幅度。
  4. 按脸在屏幕上的水平位置(x 分位数)分组,按从左到右的顺序标记为说话者 A、B,依此类推。
  5. 对每条字幕的 [start, end] 时间窗口,累计各说话者逐帧嘴部张开幅度的变化量。变化量最大者获得该标签。
  6. 写入带有 [A]/[B] 前缀的 SRT,以及包含每条字幕得分和置信度比率(胜出者 / 第二名)的 JSON 报告。

首次运行时,会将 FaceLandmarker 模型(约 3.6 MB)下载到 /tmp/mp_models/face_landmarker.task。

视觉判断明显优于根据文本猜测。 在一次验证中,基于文本的人工标记将 6/50 条字幕分给一位说话者;视觉说话人分离显示实际分配为 29/27——基于文本的猜测大错特错,因为两人轮流说话的形式很相似。说话者出镜时,请始终优先使用视觉判断。

抽查低置信度字幕。 JSON 报告中任何 confidence_ratio < 1.5 的字幕都属于临界情况,通常是语音重叠或一位说话者短暂离开画面所致。配音前请手动更正。

路径 2(备用):手动标记

适用于非常短的片段(1–2 分钟)、说话者未出镜,或视觉说话人分离失败的情况:

1
00:00:00,000 --> 00:00:03,400
[A] So what about that AI rewrite thing?

2
00:00:03,400 --> 00:00:08,200
[B] Right — let me explain the workflow.

保存为 *.tagged.srt。下游通过 /wjs-burning-subtitles 烧录字幕时,请使用不带标签的干净版 SRT。

在 dub.py 中分配语音

使用 --voice-map 并传入 speaker=voice 键值对。位置参数中的语音是未标记字幕的默认语音。

.venv/bin/python ~/.claude/skills/wjs-dubbing-video/scripts/dub.py \
    en-US-AndrewMultilingualNeural -3% +0Hz \
    --srt input.en.tagged.srt \
    --voice-map "A=en-US-BrianMultilingualNeural,B=en-US-AndrewMultilingualNeural"

语音配对建议:

  • 两位同性说话者: 选择音色差异明显的语音。Brian(随性)+ Andrew(温和)适合两位美国男性;Ava(温和女声)+ Emma(活泼女声)适合两位女性。
  • 不同性别: Ava + Andrew 是稳妥的默认选择。
  • 口音对比: 配对 en-US- 和 en-GB-,以增强区分度。
  • 中文: 混用火山语音,例如 zh_female_gaolengyujie_moon_bigtts(成熟)+ zh_female_kailangjiejie_moon_bigtts(温暖的大姐姐)。
局限

以下情况会导致视觉说话人分离失败:

  • 说话者说话时始终未出镜。
  • 镜头切换或变焦导致不同字幕之间的人脸位置不稳定。
  • 三位或更多说话者的水平位置相近(x 分位数分组过于粗略——请改用基于 (x, y) 的 k-means,或改用基于音频的说话人分离)。

对于纯音频素材(播客、旁白),请改用 pyannote.audio 或 whisperx --diarize。此工具尚未集成基于音频的说话人分离。

输出

  • <source-stem>_<lang>_dub.mp4 — 视频流直接从源文件复制,音频替换为与时间对齐的配音轨道。可直接作为 /wjs-burning-subtitles/render.py 的输入。
  • dub_work/seg_NN.mp3 — 每条字幕对应的 TTS 片段(保留以便恢复或重新生成单条字幕)。

下游处理

  • /wjs-burning-subtitles — 用于将原始音频混合作为低音量背景、烧录 SRT 字幕,或同时完成这两项。最终编码在此处通过一次 ffmpeg 处理完成(不会连续多次编码)。向其 render.py 传入 --video <source.mp4> --dub <source_lang_dub.mp4> [--srt <srt>]。
  • 仅配音文件(*_<lang>_dub.mp4)在技术上已是成片,可以直接交付,但听起来会像配音(因为它就是配音)。在底层混入原始音频会带来「专业翻译」的效果——请在 /wjs-burning-subtitles 中完成此操作。

反模式

  • ❌ 每个提示都调用一次 uvx edge-tts。 每次都会启动一个 Python 进程;bing 端点会限流,或在渲染过程中发送 RST。请使用 dub.py 中的持久化库路径。
  • ❌ 不试听就相信 audio_source。 提交前务必抽听一段 30 s 的片段。
  • ❌ 将速度拉伸到低于 0.82× atempo。 听起来会像嗑了药。请添加静音填充,或扩展文本。
  • ❌ 给单人说话者的 SRT 添加 [A] 标记。 这会浪费时间,而且配音听起来也一样。默认使用一个音色。
  • ❌ 未经用户实例上的冒烟测试,就承诺使用 Volcano 音色。 文档列出了许多音色,但在常见的 SeedTTS-2.0 入门包中会报错 code=55000000。报价前务必合成一个 5 个词的冒烟测试。
  • ❌ 将 Volcano 响应解析为一个 JSON 文档。 它是流式 NDJSON;成功终止码是 code=20000000,而不是 code=0。请拼接 base64 解码后的 data 分块,获取完整 MP3。

评论

支持 Markdown