这套东西的起点不是“做一个 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-VC、Audio Separator、Kokoro 和 RVC 分别建了虚拟环境,完整安装用的是:
cd D:\codex_bookmarks\ai-voice-pipeline
.\setup.ps1 -WithTTS -WithSeparator
模型会在第一次推理时下载,8GB 显存下我把分离器的 batch size 固定为 1,并启用了半精度;我的机器是 i9-13980HX、64GB 内存和 RTX 4060 Laptop 8GB,讲解、游戏双轨和单人独唱都已经在本机跑通过。
一份文件进来以后发生了什么
我在 run.ps1 后面接了 diagnose、tts、convert 和 run 四个子命令,其中 convert 是纯讲解的快捷入口,完整的三模式入口则是 run。
每次执行时,程序会先检查输入文件和目标参考 WAV 是否存在,也会阻止输入与输出指向同一个文件;通过检查以后,它会建立临时工作目录,把指定音轨提取为 48 kHz PCM,再根据模式决定要不要做分离。
输入文件
→ 提取指定音轨为 source.wav
→ 按模式决定是否运行 BS-RoFormer
→ Seed-VC 换声
→ 高通、低通、限幅和双声道整理
→ 需要时与背景轨混合
→ 写入新文件
Seed-VC 的基础参数我固定为 length-adjust=1.0、inference-cfg-rate=0.7 和 fp16=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 分成 Vocals 和 Instrumental。
后面我只保留 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,但它要同时保留分离出来的 Vocals 和 Instrumental;Vocals 进入 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.wav、stems\vocals.wav、stems\instrumental.wav、converted_mastered.wav 和 mixed.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-VC、Audio Separator、Kokoro。