前言
游戏副本打不过,大佬不带怎么办?
群里打字问问题,直接冷场怎么办?
压力怪不断骚扰,队友不帮怎么办?
公司老板审批慢,已读不回怎么办?
售后敷衍不退款,只发红包怎么办?
教学视频没人看,腼腆害羞怎么办?
俗话说得好,女留微信男自强,但如果学会了用GPT-SoVITS语音克隆,就能1分钟内实现将文本转成指定人的语音(TTS),不是声优请不起,而是语音克隆更有性价比,叫一声哥哥,就能让群友主动帮你解决所有疑难杂症!无论是在面对高难度的游戏副本,还是在群聊中遭遇冷场,亦或是在技术交流中感到力不从心,GPT-SoVITS 语音克隆技术都将成为你的得力助手。
本文将通过简单的实践演示,让你轻松掌握一种新颖而引人入胜的沟通方式,使你在社交中游刃有余,让你不再因为技术问题而束手无策,不再因为弱小在群里尴尬无言,更不再畏惧孤军奋战对抗压力怪,用一种崭新的方式改变人际交往关系,为你的社交体验赋予更丰富的色彩与乐趣。
阶段一:训练TTS模型
安装依赖
前往GPT-SoVITS地址,按照README提示,根据自身情况安装依赖(本文以MacOS M1为例子)
仓库更新比较频繁,请仔细阅读README,如果卡住无法继续,可以先去仓库issue搜报错,实在不行再去提issue,基本当天就能有回复
毕竟每个人遇到的报错都不尽相同:没装python、没装conda、没装pretrained models、github连不上...
bash
cd Desktop
git clone https://github.com/RVC-Boss/GPT-SoVITS.git
cd GPT-SoVITS
conda create -n GPTSoVits python=3.9
conda activate GPTSoVits
bash install.sh
brew install ffmpeg
pip install -r requirements.txt
pip uninstall torch torchaudio
pip3 install --pre torch torchaudio --index-url https://download.pytorch.org/whl/nightly/cpu
启动项目
python3 webui.py
准备人物音频文件
/Users/USER_NAME/Desktop/GPT-SoVITS/DATA/Nemesis/20240127-203452.wav
去噪(音频如果没有背景噪音可以直接下一步)
勾选"是否开启UVR5-WebUI",稍等片刻会自动开启一个新页面,点击转换
输出的去噪文件在output/uvr5_opt
,新建一个DATA
文件夹,里面再建一个模型名文件夹Nemesis
,把降噪后的音频文件放进去
/Users/USER_NAME/Desktop/GPT-SoVITS/DATA/Nemesis/20240127-203452.wav.reformatted.wav_main_vocal.wav
语音切割
"音频自动切分输入路径" 填 DATA/Nemesis
,点击"开启语音切割"
切割后的分片音频文件在/Users/USER_NAME/Desktop/GPT-SoVITS/output/slicer_opt
下
批量ASR
"批量ASR(中文only)输入文件夹路径" 输入 output/slicer_opt
,点开启离线批量ASR
会生成一个list文件/Users/taoxu/Desktop/GPT-SoVITS/output/asr_opt/slicer_opt.list
打标
".list标注文件的路径" 输入output/asr_opt/slicer_opt.list
再勾选 "是否开启打标WebUI",会自动打开一个新页面
这里面是切片语音文件和语音文件台词的一一映射,检查有没有需要修改的,和需要加标点断句的,处理完后点击"Save File"然后关闭这个页面
训练集格式化
从 "0-前置数据集获取工具" 切换到 "1-GPT-SoVITS-TTS"
输入*实验/模型名:Nemesis
*文本标注文件:./output/asr_opt/slicer_opt.list
*训练集音频文件目录:./output/slicer_opt
点击 "开启一键三连",等待结束
微调训练
分别点击 "开启SoVITS训练",结束后点击 "开启GPT训练"
如果顺利的话,会分别在GPT_weights
和SoVITS_weights
文件夹下,创建出对应的GPT模型和SoVITS模型
注意:总训练轮数 ÷ 保存频率=生成模型的数量
GPT模型数量 = 15 ÷ 5 = 3
SoVITS模型数量 = 8 ÷ 4 = 2
markdown
- GPT_weights
- GPT_weights/Nemesis-e5.ckpt
- GPT_weights/Nemesis-e10.ckpt
- GPT_weights/Nemesis-e15.ckpt
- SoVITS_weights
- SoVITS_weights/Nemesis_e4_s48.pth
- SoVITS_weights/Nemesis_e8_s96.pth
e代表训练轮数,s代表训练步数,不一定数值越高,生成音频的效果就越好
如果实在效果不理想,可以提高GPT训练的总轮数,比如从15提到40,再点击"开启GPT训练",会直接从16轮训练
TTS推理
最后切换到 "1C-推理",勾选 "是否开启TTS推理WebUI",等待自动打开新页面
点击 "刷新模型路径",下拉列表选择刚才生成的GPT模型和SoVITS模型(e代表)
上传无噪音的参考音频和文本(影响最终语气),可以直接用之前切割音频时生成的文件
输入 "需要合成的文本" 和 "需要合成的语种",点击 "合成语音",最后在右侧生成语音,可以点击播放听效果,如果不满意可以更改GPT模型和SoVITS模型组合 or 提高GPT模型训练步数 or 选择更清晰的音频从头来过
阶段二:封装本地TTS脚本
虽然已经训练好了模型,后续可以一劳永逸直接用,基本上只需要输入最终想要的文本,就能直接生成语音文件,但总不可能每次启动项目都要上传参考音频,填参考音频文本吧,还有这个生成的音频文件肯定是存在于电脑硬盘里的,那么随着用的次数变多,是不是意味着硬盘空间会越用越少呢,甚至都不知道文件存在哪,想删都删不掉。
既然已经跑通了一次完整的流程,那么有没有什么方法能来给这个固定机械的流程提效加速呢,答案就是通过脚本定制和优化整个流程,即通过命令行的方式来操作现有的网页生成音频并播放,通过代码控制流程相比操作网页,可以操作的空间就有很多:将想看到的结果打印在终端(比如生成音频文件的路径,后续可以直接清空);不再关注UI,不再需要人肉操作页面生成音频,而是直接输入文本回车生成音频;对于训练得很好的模型,可以引入缓存,输入相同的文本就无需再重新生成音频,等等。
调试websocket连接
首先运行项目,打开TTS推理WebUI,打开network,点击 "合成语音",观察页面发送的请求
发现这是一个websocket连接:ws://127.0.0.1:9872/queue/join
,一共发了两次请求:
- 第一次发送的参数是
{"fn_index":3,"session_hash":"bikdoojwuyf"}
- 等接受到响应
{"msg":"send_data"}
会发送第二次请求 - 第二次发送的参数是
kotlin
{
data: [
{
data: "data:audio/wav;base64,UklGRqRjAwBXQVZFZm10IBAAAAAB...",
name: "20240127-203452.wav_781760_892800.wav",
},
"其实,我并没有什么归属感",
"中文",
"哥哥,呆会带我打团本",
"中文",
"按中文句号。切",
],
fn_index: 3,
session_hash: "bikdoojwuyf",
}
- 后续会接受到3个响应,最后的这个响应包含
{"msg":"process_completed"}
说明语音生成好了,存到了硬盘里,并且连带返回了生成的这个语音文件的路径
bash
/var/folders/f3/n23rr3s558x0_hkct1m9w9p80000gn/T/gradio/af3b1224c7ba1619d705b0890ded93887a79909a/audio.wav
简易脚本
拷贝一下network的中请求参数,尝试请求/queue/join
接口,运行一下,看看能不能生成语音文件
js
import WebSocket from "ws";
import player from "play-sound";
const sessionHash = "bikdoojwuyf";
const websocketUrl = "ws://127.0.0.1:9872/queue/join";
const socket = new WebSocket(websocketUrl);
socket.on("open", () => {
const requestData = {
fn_index: 3,
session_hash: sessionHash,
};
socket.send(JSON.stringify(requestData));
});
socket.on("message", (message) => {
const responseData = JSON.parse(message);
if (responseData.msg === "send_data") {
const requestData = {
data: [
{
data: "data:audio/wav;base64,UklGRqRjAwBXQVZFZm10IBAAAAABAAEA...",
name: "20240127-203452.wav_781760_892800.wav",
},
"其实,我并没有什么归属感",
"中文",
"哥哥,呆会带我打团本",
"中文",
"按中文句号。切",
],
fn_index: 3,
session_hash: sessionHash,
};
socket.send(JSON.stringify(requestData));
} else if (responseData.msg === "process_completed") {
console.log("生成音频文件路径:", responseData.output.data[0].name);
player().play(responseData.output.data[0].name);
}
});
bash
node index.js
生成音频文件路径:/var/folders/f3/n23rr3s558x0_hkct1m9w9p80000gn/T/gradio/2dd86c945cee5d876d01c732c23d04ae8177e757/audio.wav
脚本完善
基于简易脚本加入路径缓存、问答式交互、打印语音生成状态、打印语音生成耗时功能
js
import WebSocket from "ws";
import player from "play-sound";
import readline from "readline";
const sessionHash = "bikdoojwuyf";
const websocketUrl = "ws://127.0.0.1:9872/queue/join";
// 路径缓存
const audioPathCache = {};
// 问答交互
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.setPrompt("请输入需要合成的文本:");
rl.prompt();
rl.on("line", (input) => {
handleText(input.trim());
});
rl.on("close", () => {
process.exit();
});
const handleText = async (text) => {
if (text) {
if (audioPathCache[text]) {
player().play(audioPathCache[text]);
process.stdout.write("命中缓存\n\n");
} else {
await sendData(text);
}
}
rl.prompt();
};
const sendData = (input) => {
return new Promise((resolve) => {
const socket = new WebSocket(websocketUrl);
const startTime = new Date();
socket.on("open", () => {
const requestData = {
fn_index: 3,
session_hash: sessionHash,
};
socket.send(JSON.stringify(requestData));
});
socket.addListener("close", () => {
resolve();
});
socket.on("message", (message) => {
const responseData = JSON.parse(message);
if (responseData.msg === "send_data") {
const requestData = {
data: [
{
data: "data:audio/wav;base64,UklGRqRjAwBXQVZFZm10IBAAAAABA",
name: "20240127-203452.wav_781760_892800.wav",
},
"其实,我并没有什么归属感",
"中文",
input,
"中文",
"按中文句号。切",
],
fn_index: 3,
session_hash: sessionHash,
};
socket.send(JSON.stringify(requestData));
} else if (responseData.msg === "process_completed") {
if (!responseData.success) {
process.exit();
}
audioPathCache[input] = responseData.output.data[0].name;
player().play(responseData.output.data[0].name);
process.stdout.write(
"音频生成耗时:" + (new Date() - startTime) / 1000 + "s\n"
);
process.stdout.write(
`生成音频文件路径:${responseData.output.data[0].name}\n\n`
);
} else {
process.stdout.write("音频生成状态:" + responseData.msg + "\n");
}
});
});
};
最终效果
bash
node index.js
请输入需要合成的文本:哥哥
音频生成状态:send_hash
音频生成状态:estimation
音频生成状态:process_starts
音频生成状态:process_generating
音频生成耗时:1.874s
生成音频文件路径:/var/folders/f3/n23rr3s558x0_hkct1m9w9p80000gn/T/gradio/3ed576dd979391d659a14752e3226198275a04d5/audio.wav
请输入需要合成的文本:哥哥
命中缓存
请输入需要合成的文本:哥哥
命中缓存
请输入需要合成的文本:哥哥带带
音频生成状态:send_hash
音频生成状态:estimation
音频生成状态:process_starts
音频生成状态:process_generating
音频生成耗时:6.817s
生成音频文件路径:/var/folders/f3/n23rr3s558x0_hkct1m9w9p80000gn/T/gradio/20af1251af700aef8b04957970531b59512b67a8/audio.wav
请输入需要合成的文本:她不会生气吧
音频生成状态:send_hash
音频生成状态:estimation
音频生成状态:process_starts
音频生成状态:process_generating
音频生成耗时:7.904s
生成音频文件路径:/var/folders/f3/n23rr3s558x0_hkct1m9w9p80000gn/T/gradio/f9887e58c0243914da1ac61c829d966642e4f9f1/audio.wav
使用GPT-SoVITS前
使用GPT-SoVITS后
嗯!效果非常好!大佬已经在一声声哥哥中迷失了自我,以后再也不愁没有混不过去的团本了!