跳到内容
Caiden's Blog
返回

GPT-SoVITS 语音克隆教程

一个语音克隆,支持微调的 TTS 工具,还支持流式 TTS API 接口。

1. 系统环境

操作系统:Ubuntu 22.04 是在 ucloud 租的 GPU 服务器,4090 48G 显存魔改版

(GPTSoVits) ubuntu@10-60-55-82:~/hou/GPT-SoVITS$ neofetch
            .-/+oossssoo+/-.
        `:+ssssssssssssssssss+:`
      -+ssssssssssssssssssyyssss+-         ubuntu@10-60-55-82
    .ossssssssssssssssssdMMMNysssso.       ------------------
   /ssssssssssshdmmNNmmyNMMMMhssssss/      OS: Ubuntu 22.04.4 LTS x86_64
  +ssssssssshmydMMMMMMMNddddyssssssss+     Host: KVM UCLOUD 1.0.0 O PC (i440FX + PIIX, 1996)
 /sssssssshNMMMyhhyyyyhmNMMMNhssssssss/    Kernel: 5.15.0-113-generic
.ssssssssdMMMNhsssssssssshNMMMdssssssss.   Uptime: 5 days, 46 mins
+sssshhhyNMMNyssssssssssssyNMMMysssssss+   Packages: 1892 (dpkg), 7 (snap)
ossyNMMMNyMMhsssssssssssssshmmmhssssssso   Shell: bash 5.1.16
ossyNMMMNyMMhsssssssssssssshmmmhssssssso   Terminal: node
+sssshhhyNMMNyssssssssssssyNMMMysssssss+   CPU: INTEL XEON GOLD 6530 (16) @ 2.100GHz
.ssssssssdMMMNhsssssssssshNMMMdssssssss.   GPU: NVIDIA 00:03.0 NVIDIA Corporation Device 2684
 /sssssssshNMMMyhhyyyyhdNMMMNhssssssss/    Memory: 26044MiB / 96553MiB
  +sssssssssdmydMMMMMMMMddddyssssssss+
   /ssssssssssshdmNNNNmyNMMMMhssssss/
    .ossssssssssssssssssdMMMNysssso.
      -+sssssssssssssssssyyyssss+-
        `:+ssssssssssssssssss+:`
            .-/+oossssoo+/-.

(GPTSoVits) ubuntu@10-60-55-82:~/hou/GPT-SoVITS$ nvidia-smi
Tue Aug 12 09:29:00 2025
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 570.133.07             Driver Version: 570.133.07     CUDA Version: 12.8     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA GeForce RTX 4090        Off |   00000000:00:03.0 Off |                  Off |
| 63%   29C    P8             24W /  450W |   10430MiB /  49140MiB |      0%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------+

+-----------------------------------------------------------------------------------------+
| Processes:                                                                              |
|  GPU   GI   CI              PID   Type   Process name                        GPU Memory |
|        ID   ID                                                               Usage      |
|=========================================================================================|
|    0   N/A  N/A          525763      C   ...da/envs/python310/bin/python3       2116MiB |
|    0   N/A  N/A          526094      C   python                                  674MiB |
|    0   N/A  N/A          526095      C   python                                  688MiB |
|    0   N/A  N/A          526111      C   python                                 4326MiB |
|    0   N/A  N/A          526205      C   ...t/build/bin/funasr-wss-server       2596MiB |
+-----------------------------------------------------------------------------------------+

2. 项目安装

项目克隆到本地后,需要安装项目依赖和下载模型文件,直接使用 install.sh即可:

# 创建虚拟环境
conda create -n GPTSoVits python=3.10
conda activate GPTSoVits
# 安装
(GPTSoVits) ubuntu@10-60-55-82:~/hou/GPT-SoVITS$ bash install.sh --device CU128 --source HF-Mirror
[INFO]: Detected system: Linux 5.15.0-113-generic x86_64
[INFO]: Detected GCC Version: 11
[INFO]: Skip Installing GCC & G++ From Conda-Forge
[INFO]: Installing libstdcxx-ng From Conda-Forge
[SUCCESS]: libstdcxx-ng=11 Installed...
[INFO]: Installing FFmpeg & CMake...
[SUCCESS]: FFmpeg & CMake Installed
[INFO]: Installing unzip...
[SUCCESS]: unzip Installed
[INFO]: Download Model From HuggingFace-Mirror
[INFO]: Downloading Pretrained Models...
[SUCCESS]: Pretrained Models Downloaded
[INFO]: Downloading G2PWModel..
[SUCCESS]: G2PWModel Downloaded
[INFO]: Checking For Nvidia Driver Installation...
\033[1;32m[INFO]: \033[0mNvidia Driver Founded
[INFO]: Installing PyTorch For CUDA 12.8...
[SUCCESS]: PyTorch Installed
[INFO]: Installing Python Dependencies From requirements.txt...
[SUCCESS]: Python Dependencies Installed
[INFO]: Downloading NLTK Data...
[SUCCESS]: NLTK Data Downloaded
[INFO]: Downloading Open JTalk Dict...
[SUCCESS]: Open JTalk Dic Downloaded
[SUCCESS]: Installation Completed

3. 启动 & 微调

3.1 项目启动

在项目根目录下启动 web 界面,使用中文

python webui.py zh_CN

3.2 数据集准备

准备好 wav 格式的语音数据集和对应的文字

准备 dataset.list 文件

文件格式如下:

语音路径 | 说话人名称 | 语音语言 | 语音内容

我的文件内容:

准备了六条纳西妲的语音数据

/home/ubuntu/hou/GPT-SoVITS/data/gpt/1.wav|naxida|zh|小猫也想变成狐狸,但它尾巴太细小,又长不出富有色彩的皮毛。其他狐狸见了,安慰道:就算如此,你也是我们的同伴。
/home/ubuntu/hou/GPT-SoVITS/data/gpt/2.wav|naxida|zh|怪兽厌恶这种美满,在山中点起一把火。动物们异常惊慌。为了扑灭大火,它们必须做出牺牲。
/home/ubuntu/hou/GPT-SoVITS/data/gpt/3.wav|naxida|zh|一只灰色的狐狸站起来,与怪兽交谈。它说:你是如此聪明,一定能想到帮助我们的办法。
/home/ubuntu/hou/GPT-SoVITS/data/gpt/4.wav|naxida|zh|怪兽将那颗水珠交给小猫,告诉它:大家已经想好让你牺牲了。带上这个,去为你的狐狸同伴而死吧。
/home/ubuntu/hou/GPT-SoVITS/data/gpt/5.wav|naxida|zh|它们说好要相依为命,可小鸟的寿命太短,一下就没了呼吸。小猫埋葬它之后,便离开了那座山。
/home/ubuntu/hou/GPT-SoVITS/data/gpt/6.wav|naxida|zh|…它再也没有爱过山中任何一片树叶与任何一只动物。它流浪在每一个夜晚,对着月光磨牙。

3.3 微调

3.3.1 训练集格式化

  1. 填写实验名称
  2. 填写 list 文件绝对路径
  3. 训练集格式化一键三连,然后观察右下角的输出,等待出现一键三连完成提示

image.png

image.png

格式化后,会在 logs/实验名称 这个文件夹下有对应的数据

image.png

3.3.2. 微调训练

  1. 先点击开启 SoVITS 训练,等待出现 SoVITS 训练完成后
  2. 再点击开启 GPT 训练,等待训练完成

image.png

训练完成示意图

image.png

4. 推理

点击开启 TTS 推理,会跳出如下页面:

  1. 先选择模型
  2. 上传参考音频和对应的参考文本
  3. 填写要合成的文本
  4. 点击合成语音

image.png

5. API 模式

5.1. 启动

微调好模型肯定需要 API 调用

执行 api.py 文件

python api.py -s SoVITS_weights_v2ProPlus/naxida-v2proplus_e8_s48.pth  -g GPT_weights_v2ProPlus/naxida-v2proplus-e15.ckpt

-s 是指定 SoVITS 模型

-g 是指定 gpt 模型

执行完成后这就代表成功启动

(GPTSoVits) ubuntu@10-60-55-82:~/hou/GPT-SoVITS$ python api.py -s SoVITS_weights_v2ProPlus/naxida-v2proplus_e8_s48.pth  -g GPT_weights_v2ProPlus/naxida-v2proplus-e15
.ckpt
INFO:     未指定默认参考音频
INFO:     半精: True
INFO:     编码格式: wav
INFO:     数据类型: int16
INFO:     模型版本: v2ProPlus
INFO:     Started server process [947648]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:9880 (Press CTRL+C to quit)

5.2. 接口文档

访问网址:http://localhost:9880/docs

这里是接口文档

image.png

5.3. TTS 接口(GET)

现在可以通过 api 文字转语音了!

主要填写三部分

  1. 参考音频、文本和语言
  2. 要生成的文本和语言
  3. 默认推理参数

image.png

点击 execute 之后,就可以看到生成的语音文件了

image.png

5.4. 流式 TTS 接口

启动的时候添加流式的参数即可

-sm:流式返回模式, 默认不启用, 可以设置为如下值”close”,“c”, “normal”,“n”, “keepalive”,“k”

-mt:返回的音频编码格式, 流式默认ogg, 非流式默认wav, “wav”, “ogg”, “aac”

测试 ogg,浏览器运行报错了,所以使用 aac 格式

python api.py -s SoVITS_weights_v2ProPlus/naxida-v2proplus_e8_s48.pth  -g GPT_weights_v2ProPlus/naxida-v2proplus-e15.ckpt -sm k -mt aac

配置跨域,修改 api.py 文件

在 main 上面添加如下代码

# 新增代码
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 下面是原有代码
if __name__ == "__main__":
    uvicorn.run(app, host=host, port=port, workers=1)

可以使用 post 接口来请求,以下是流式测试用的 html 代码

<!doctype html>
<html lang="zh-CN">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>流式 TTS 播放器 (POST JSON)</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, "Helvetica Neue", Arial;
            margin: 20px;
            background: #0f172a;
            color: #e6eef8
        }

        .card {
            background: #111827;
            padding: 18px;
            border-radius: 10px;
            box-shadow: 0 6px 18px rgba(2, 6, 23, .6);
            max-width: 820px;
            margin: auto
        }

        textarea,
        input,
        select,
        button {
            width: 100%;
            margin-top: 8px;
            padding: 8px;
            border-radius: 8px;
            border: 1px solid #23303b;
            background: #071022;
            color: #e6eef8
        }

        button {
            cursor: pointer;
            background: #1e293b
        }

        .row {
            display: flex;
            gap: 8px;
            margin-top: 10px;
            flex-wrap: wrap
        }

        .log {
            margin-top: 12px;
            background: #04101a;
            padding: 10px;
            border-radius: 6px;
            height: 120px;
            overflow: auto;
            font-size: 13px
        }
    </style>
</head>

<body>
    <div class="card">
        <h2>流式 TTS 播放器 (POST JSON)</h2>

        <label>TTS 接口 URL</label>
        <input id="ttsUrl" value="http://127.0.0.1:9880" />

        <label>Refer WAV 路径</label>
        <input id="referWav" value="/home/ubuntu/hou/GPT-SoVITS/data/gpt/ref.wav" />

        <label>Prompt 文本</label>
        <textarea id="promptText">我还有些事要研究,你们先转换一下心情吧。</textarea>

        <label>Prompt 语言</label>
        <input id="promptLang" value="zh" />

        <label>合成文本</label>
        <textarea id="text">欢迎使用我们的语音合成服务。</textarea>

        <label>文本语言</label>
        <input id="textLang" value="zh" />

        <label>切分标点</label>
        <input id="cutPunc" value=",。!?" />

        <div class="row">
            <select id="mediaType">
                <option value="ogg">ogg</option>
                <option value="aac">aac</option>
                <option value="wav">wav</option>
            </select>
            <button id="startBtn">开始播放</button>
            <button id="stopBtn" disabled>停止</button>
        </div>

        <audio id="player" controls style="width:100%;margin-top:10px"></audio>
        <div class="log" id="log"></div>
    </div>

    <script>
        const logEl = document.getElementById('log');
        function log(...args) {
            console.log(...args);
            logEl.innerText += args.join(' ') + '\n';
            logEl.scrollTop = logEl.scrollHeight;
        }

        let controller = null;
        let reader = null;
        let mediaSource = null;
        let sourceBuffer = null;
        let queue = [];
        let audioUrl = null;
        let playingBlobParts = [];

        const startBtn = document.getElementById('startBtn');
        const stopBtn = document.getElementById('stopBtn');
        const player = document.getElementById('player');

        startBtn.addEventListener('click', startStreaming);
        stopBtn.addEventListener('click', stopStreaming);

        async function startStreaming() {
            stopStreaming();
            const baseUrl = document.getElementById('ttsUrl').value.trim();
            const mediaType = document.getElementById('mediaType').value;

            const bodyData = {
                refer_wav_path: document.getElementById('referWav').value.trim(),
                prompt_text: document.getElementById('promptText').value,
                prompt_language: document.getElementById('promptLang').value.trim(),
                text: document.getElementById('text').value,
                text_language: document.getElementById('textLang').value.trim(),
                cut_punc: document.getElementById('cutPunc').value
            };

            log('POST 请求:', baseUrl, bodyData);
            controller = new AbortController();
            queue = [];
            playingBlobParts = [];

            if ('MediaSource' in window && mediaType === 'ogg') {
                setupMediaSource('audio/ogg; codecs="opus"');
                fetchAndStreamPOST(baseUrl, bodyData, controller.signal, true);
            } else if ('MediaSource' in window && mediaType === 'aac') {
                setupMediaSource('audio/aac');
                fetchAndStreamPOST(baseUrl, bodyData, controller.signal, true);
            } else {
                fetchAndPlayAsBlobPOST(baseUrl, bodyData, controller.signal);
            }
        }

        function setupMediaSource(mime) {
            cleanupMediaSource();
            mediaSource = new MediaSource();
            audioUrl = URL.createObjectURL(mediaSource);
            player.src = audioUrl;
            mediaSource.addEventListener('sourceopen', () => {
                try { sourceBuffer = mediaSource.addSourceBuffer(mime); } catch (e) { log('addSourceBuffer 失败:', e); return; }
                sourceBuffer.mode = 'sequence';
                sourceBuffer.addEventListener('updateend', () => {
                    if (queue.length > 0 && !sourceBuffer.updating) {
                        const chunk = queue.shift();
                        sourceBuffer.appendBuffer(chunk);
                    } else if (queue.length === 0 && mediaSource.readyState === 'open' && !reader) {
                        mediaSource.endOfStream();
                    }
                });
                if (queue.length > 0 && !sourceBuffer.updating) {
                    const chunk = queue.shift();
                    sourceBuffer.appendBuffer(chunk);
                }
                player.play().catch(() => { });
            });
        }

        function cleanupMediaSource() {
            if (sourceBuffer) { try { if (mediaSource && mediaSource.readyState === 'open') mediaSource.removeSourceBuffer(sourceBuffer); } catch (e) { } sourceBuffer = null; }
            if (mediaSource) { try { URL.revokeObjectURL(audioUrl); } catch (e) { } mediaSource = null; }
        }

        async function fetchAndStreamPOST(url, body, signal, useMediaSource) {
            const resp = await fetch(url, { method: 'POST', signal, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
            if (!resp.ok) throw new Error('HTTP ' + resp.status);
            const ct = resp.headers.get('content-type') || '';
            if (ct.includes('application/json')) { throw new Error('Server error: ' + JSON.stringify(await resp.json())); }

            reader = resp.body.getReader();
            stopBtn.disabled = false;
            startBtn.disabled = true;

            while (true) {
                const { done, value } = await reader.read();
                if (done) break;
                if (!value) continue;
                const chunk = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
                playingBlobParts.push(chunk);
                if (useMediaSource && sourceBuffer) {
                    if (sourceBuffer.updating || queue.length > 0) { queue.push(chunk); }
                    else { try { sourceBuffer.appendBuffer(chunk); } catch (e) { queue.push(chunk); } }
                }
            }
            log('流读取结束');
            reader = null;
            stopBtn.disabled = true;
            startBtn.disabled = false;

            if (!useMediaSource) {
                const mime = resp.headers.get('content-type') || 'audio/ogg';
                const blob = new Blob(playingBlobParts, { type: mime });
                player.src = URL.createObjectURL(blob);
                player.play().catch(() => { });
            }
        }

        async function fetchAndPlayAsBlobPOST(url, body, signal) {
            log('fallback -> blob 下载');
            const resp = await fetch(url, { method: 'POST', signal, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
            if (!resp.ok) { log('HTTP error', resp.status); return; }
            const ct = resp.headers.get('content-type') || 'audio/ogg';
            const ab = await resp.arrayBuffer();
            const blob = new Blob([ab], { type: ct });
            player.src = URL.createObjectURL(blob);
            player.play().catch(() => { });
            stopBtn.disabled = false;
            startBtn.disabled = true;
        }

        function stopStreaming() {
            try { if (controller) controller.abort(); } catch (e) { }
            controller = null;
            if (reader) { try { reader.cancel(); } catch (e) { } reader = null; }
            cleanupMediaSource();
            try { if (player.src && player.src.startsWith('blob:')) URL.revokeObjectURL(player.src); } catch (e) { }
            player.pause();
            stopBtn.disabled = true;
            startBtn.disabled = false;
            log('已停止');
        }
    </script>
</body>

</html>

测试效果

image.png

测试文本

各位听众,早上好!今天,我们将一起踏上一段奇妙的声音之旅——让文字在声波中绽放。 请闭上眼睛,想象自己正站在春日田野,微风拂过耳畔,带来淡淡花香; 远处传来布谷鸟的呼唤,“布——谷——布——谷”。此刻,阳光穿过云层,洒在湖面,泛起粼粼金光; 而你的心跳,也与这自然的节奏渐渐同频。愿这段声音,为你点亮一整天的好心情。

155 字的情况下:

语音长度 37s

简单测试,仅供参考,测试显卡 NVIDIA GeForce RTX 4090 48G 显存

6. 存在问题

6.1. 必须得上传参考音频

按照说明,理论上来说可以不填写参考音频和文本(可以打对勾无参考音频模式),使用微调后的模型来直接推理,但是试了 v1、v2、v2pro、v2proplus,v4 都必须得上传音频

其中 v4 是无法开启无参考音频模式

其他版本可以开启无参考音频模式,但是报错提示需要上传参考音频


分享到:

上一篇
FunASR热词配置
下一篇
LangChain 极速入门