这套东西的起点不是“做一个 AI 换声 Demo”,而是我手里真的有三种素材要处理:只有我说话的讲解视频、麦克风和游戏声分轨录制的游戏视频,还有一首单人独唱;我希望它们都能在自己的笔记本上离线跑,也希望以后换一个输入文件、输出目录或目标音色时,不用再手工拼一遍 FFmpeg 命令。

我最早想的是 FFmpeg → UVR5 → RVC → 混音 → FFmpeg,这个方向没有错,不过跑完第一条讲解视频后我就把它拆掉了,因为纯讲解里只有我的声音,先做一次人声分离没有意义,还会让辅音和尾音多损失一遍。后面我搭出来的不是一条固定直线,而是一个入口、三种模式。

我搭出来的整体结构

我把项目放在 D:\codex_bookmarks\ai-voice-pipeline,外面只留两个 PowerShell 入口:

  • setup.ps1 负责安装环境和模型依赖;
  • run.ps1 负责接收模式、输入、输出和目标音色,再调用 Python 流水线。

真正干活的组件有四个,FFprobe 先看输入文件里有几条流,FFmpeg 负责提取、混音和封装,Audio Separator + BS-RoFormer 只在游戏漏音和歌曲分轨时使用,Seed-VC 负责把我的声音换成目标参考音色;另外我接了 Kokoro,用它生成中文参考 WAV,也可以直接把文字读成旁白。

为了不让几个项目的依赖互相打架,我给 Seed-VCAudio SeparatorKokoroRVC 分别建了虚拟环境,完整安装用的是:

cd D:\codex_bookmarks\ai-voice-pipeline
.\setup.ps1 -WithTTS -WithSeparator

模型会在第一次推理时下载,8GB 显存下我把分离器的 batch size 固定为 1,并启用了半精度;我的机器是 i9-13980HX、64GB 内存和 RTX 4060 Laptop 8GB,讲解、游戏双轨和单人独唱都已经在本机跑通过。

一份文件进来以后发生了什么

我在 run.ps1 后面接了 diagnosettsconvertrun 四个子命令,其中 convert 是纯讲解的快捷入口,完整的三模式入口则是 run

每次执行时,程序会先检查输入文件和目标参考 WAV 是否存在,也会阻止输入与输出指向同一个文件;通过检查以后,它会建立临时工作目录,把指定音轨提取为 48 kHz PCM,再根据模式决定要不要做分离。

输入文件
  → 提取指定音轨为 source.wav
  → 按模式决定是否运行 BS-RoFormer
  → Seed-VC 换声
  → 高通、低通、限幅和双声道整理
  → 需要时与背景轨混合
  → 写入新文件

Seed-VC 的基础参数我固定为 length-adjust=1.0inference-cfg-rate=0.7fp16=True,讲话默认跑 20 个扩散步;普通讲话会打开自动 F0 调整,歌曲则打开 f0-condition、关闭自动 F0,再根据需要通过 --pitch 改半音。

换声结束后,我用 FFmpeg 做 65 Hz 高通、15 kHz 低通和 0.95 限幅,再统一到 48 kHz 双声道;如果输入是 .mp4.mkv.mov.webm,画面会使用 -c:v copy 原样写到新文件,只把音频编码成 256 kbps AAC。也就是说,换声不会重新压一遍画面。

我怎样替换纯讲解视频

纯讲解是最短的一支,我不做人声分离,只提取第 0 条音轨,然后交给 Seed-VC

input.mp4
→ 提取讲话音轨
→ Seed-VC
→ 音量处理
→ input_ai.mp4

实际使用时,我只需要替换 --input--output 后面的路径:

.\run.ps1 convert `
  --input "D:\video\lesson.mp4" `
  --output "D:\video\lesson_ai.mp4"

这条命令等价于 run --mode speech,默认使用 references\kokoro_zm_025_steady.wav 作为目标音色;我的完整输入视频是 480.534 秒、2560×1440、60fps,转换以后仍然是 480.534 秒,原片与成片的视频码流 SHA-256 完全相同,说明画面确实没有被改动。

我怎样替换游戏双轨

我的游戏录屏约定轨道 0 是麦克风,轨道 1 是干净游戏声;因为平时可能外放,麦克风轨里会漏进少量游戏声音,所以我没有直接拿它去换声,而是先让 BS-RoFormer 把轨道 0 分成 VocalsInstrumental

后面我只保留 Vocals,把这部分交给 Seed-VC,分离出来的 Instrumental 会被丢掉;混音时使用的是输入文件原本那条干净游戏轨,这样不会把漏录的游戏声再叠一层。

轨道 0:麦克风 + 少量漏音
  → BS-RoFormer
  → 只取 Vocals
  → Seed-VC ──────────────┐
                          ├→ game_ai.mkv
轨道 1:干净游戏声 ──────┘

我会先检查素材,避免把轨道序号写反:

.\run.ps1 diagnose --input "D:\video\game.mkv"

确认后再执行:

.\run.ps1 run `
  --mode game `
  --input "D:\video\game.mkv" `
  --output "D:\video\game_ai.mkv" `
  --mic-track 0 `
  --background-track 1

这里的序号从 0 开始,如果我的下一份 OBS 素材把游戏声放在第一条、人声放在第二条,就只要把两个数字对调,流水线本身不用改。

我怎样替换单人独唱

歌曲这一支仍然使用 BS-RoFormer,但它要同时保留分离出来的 VocalsInstrumentalVocals 进入 Seed-VC F0 歌声模式,转换完成后再与原伴奏混回。

song.wav
  → BS-RoFormer
  ├→ Vocals → Seed-VC F0 ─┐
  └→ Instrumental ────────┴→ song_ai.wav

我实际使用的命令是:

.\run.ps1 run `
  --mode song `
  --input "D:\audio\song.wav" `
  --output "D:\audio\song_ai.wav" `
  --pitch 0 `
  --diffusion-steps 30

--pitch 0 表示不做固定移调,高音明显不适合目标音色时,我才会按半音往上或往下试;歌曲比讲话慢,也更依赖干声分离质量,和声、重混响和尖锐高音都可能留下伪影。

输入、输出和目标音色到底怎么换

这几个参数我刻意分开了:

我想替换的内容要改的参数示例
原始素材--input--input "D:\video\new.mp4"
成品位置--output--output "D:\output\new_ai.mp4"
换声参考--target-ref--target-ref "D:\voice\reference.wav"
游戏人声轨--mic-track--mic-track 0
游戏背景轨--background-track--background-track 1
歌声音高--pitch--pitch -2

路径里有空格时我会完整加上引号,输出目录不存在会自动创建;输入和输出不能写成同一个路径,避免把原素材直接覆盖,不过另一个已经存在的输出文件仍可能被 -y 覆盖,所以我通常会在文件名后面加 _ai

要换成另一种声音时,讲解、游戏和歌曲都使用 --target-ref

.\run.ps1 convert `
  --input "D:\video\lesson.mp4" `
  --output "D:\video\lesson_new_voice.mp4" `
  --target-ref "D:\voice\my_reference.wav"

参考音频最好是 5~20 秒、单人、干净、没有音乐和混响的 WAV;同一份参考 WAV 可以贯穿三种模式,不需要分别训练三个模型。

Kokoro--voice 是另一回事,它只控制文字朗读用哪个中文声音:

.\run.ps1 tts `
  --text-file "D:\script\lesson.txt" `
  --output "D:\audio\narration.wav" `
  --voice zm_025 `
  --speed 0.95

我现在的默认目标是 Kokoro zm_025,做法是先用 Kokoro 生成 kokoro_zm_025_steady.wav,再把这份 WAV 交给 Seed-VC;前者是文字转语音,后者才是保留我原来语速、停顿和内容的换声。

我为什么改用中文男声

我一开始考虑过塔菲,后来也实际试过黄金船和 ずんだもん,成品对中文都不够自然,而且第三方角色权重还存在授权边界;于是我截取原视频 00:35~01:05 的同一段中文,生成多个候选音色做盲听,统一响度以后再比较声调、齿音、鼻音和长句稳定性。

后来留下的 zm_025 中位 F0 约 120.3 Hz,波动也比当时的其他男声候选小,用在教学讲解里更稳;它同时能给文字朗读、教学视频、游戏解说和歌曲提供同一套目标音色。

我怎样保留中间结果和排查问题

默认情况下,中间 WAV 会放在系统临时目录,任务结束后自动清理;如果效果不对,我会加上 --keep-work

.\run.ps1 run `
  --mode game `
  --input "D:\video\game.mkv" `
  --output "D:\video\game_ai.mkv" `
  --keep-work

这样 source.wavstems\vocals.wavstems\instrumental.wavconverted_mastered.wavmixed.wav 都会留在项目的 work\<输入文件名>\ 下面,我可以逐段听出问题发生在提取、分离、换声还是混音;需要指定位置时则使用 --work-dir,只想检查将要执行的命令可以加 --dry-run

跑完以后我怎么确认它真的能用

我没有只听开头几十秒,完整讲解成片做了整文件解码,视频保留 28,829 帧,音频是 48 kHz 双声道 AAC;另外又跑了 103.92 秒的游戏样本和 88.14 秒的单人领唱样本,两份输出也都能完整解码。

项目自己的测试是 4/4 通过,不过这些检查只能证明音轨映射、封装和文件完整性没有问题,音色自然不自然仍然要听;我现在的习惯是先截同一段 30 秒做音色盲测,选定参考声音以后再跑完整视频,比一次次转换八分钟原片省很多时间。

当前版本也还有我准备继续改的地方,游戏漏音现在用的是 BS-RoFormer,后面可以利用干净游戏轨接入 AEC;歌曲目前沿用了统一的 48 kHz 和 15 kHz 低通,下一版会把歌声链路单独处理;封装仍用了 -shortest,也需要改成更精确的时长对齐。这些属于下一轮优化,本文记录的是我现在真正跑通并验证过的版本。

相关项目:Seed-VCAudio SeparatorKokoro