1. 训练完不保存,等于白炼:sovits 模型保存到底在存什么
很多人第一次跑完 sovits 训练,看到终端刷出一堆 loss 数字就以为大功告成,结果换了个数据集想再炼一炉,回头发现上一个模型找不到了,或者找到了却加载不起来。问题不在训练本身,而在于保存环节没做对。sovits 的模型保存不是简单地把一个文件丢进文件夹,它涉及 checkpoint 权重、config.json 配置、logs 目录结构三者的配合,缺一个都可能导致模型无法复现。
这篇文章聚焦训练后的保存环节,面向已经跑通 sovits 训练、准备做模型归档和迁移的语音克隆开发者。我会给出可复制的保存目录结构、config.json 关键字段的配置说明、checkpoint 命名规范,以及加载验证的具体命令和预期输出。目标只有一个:让你炼完的模型能稳定保存、随时加载、换机器也能跑起来。
如果你同时管理多个模型或多次实验,调用凭证和 API 通道的混乱也会拖慢节奏。我习惯用 TaoToken 统一管理多模型调用的 Key,官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,后面会讲怎么把它接进你的工作流。先把保存这件事做扎实。
2. 保存前的准备:TaoToken 统一 Key 与目录规划
在动手保存之前,有两件事值得先理清楚:一是你的模型调用凭证怎么管,二是保存目录怎么规划。很多人炼了五六个模型之后,Key 散落在各种配置文件里,换个环境就要重新翻一遍,非常痛苦。
TaoToken 的作用是把多个模型的调用凭证收敛到一个通道里。你可以在控制台创建 API Key,然后在需要调用模型的地方统一走这个 Key。对于 sovits 这种本地训练为主、但偶尔需要调用外部模型做辅助推理的场景,统一 Key 能省掉不少切换成本。
具体操作上,先到控制台创建 Key:
# 访问控制台创建 API Key(浏览器打开) # https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite创建完成后,把 Key 写进环境变量,避免硬编码到脚本里:
# Linux / macOS export TAOTOKEN_API_KEY="sk-你的key" # Windows PowerShell $env:TAOTOKEN_API_KEY="sk-你的key"API 端点统一用 https://taotoken.net/api ,注意这个地址不带 UTM 参数,直接作为 base_url 使用即可。如果你用的是 OpenAI 兼容的客户端,配置大概长这样:
# config_api.py import os API_KEY = os.environ.get("TAOTOKEN_API_KEY") BASE_URL = "https://taotoken.net/api" # 后续调用时传入 # client = OpenAI(api_key=API_KEY, base_url=BASE_URL)目录规划方面,建议在项目根目录下建一个saved_models文件夹,每个模型一个子目录,用模型名加日期命名。这样做的原因是 sovits 训练过程中会在logs目录下生成大量中间文件,如果不做隔离,换数据集时很容易把上一个模型的权重覆盖掉。
# 推荐的保存目录结构 saved_models/ ├── myvoice_20240115/ │ ├── G_44000.pth # 生成器权重 │ ├── D_44000.pth # 判别器权重 │ ├── config.json # 模型配置 │ └── train_data_backup/ # 训练数据备份 └── another_20240120/ ├── G_44000.pth ├── D_44000.pth ├── config.json └── train_data_backup/这个结构的好处是每个模型自包含,迁移时整个文件夹拷走就行,不会丢配置也不会串数据。
3. 可复制的保存配置:checkpoint 命名与 config.json 字段
sovits 训练结束后,logs目录下通常会有多个 checkpoint 文件,命名格式类似G_44000.pth和D_44000.pth。这里的数字代表训练步数,G 是生成器,D 是判别器。保存时不要只留最后一个,建议保留几个关键步数的版本,因为不同步数的模型在音色稳定性和自然度上会有差异。
命名规范上,我建议在原始文件名基础上加模型标识,避免多个模型的 checkpoint 混在一起时分不清:
# 原始文件 logs/44k/G_44000.pth logs/44k/D_44000.pth logs/44k/config.json # 归档后重命名 saved_models/myvoice_20240115/G_myvoice_44000.pth saved_models/myvoice_20240115/D_myvoice_44000.pth saved_models/myvoice_20240115/config_myvoice.jsonconfig.json 是保存环节最容易出问题的地方。这个文件记录了模型的采样率、音高提取方式、说话人数量等关键参数,如果配置和权重不匹配,加载时会直接报错。下面是一个典型的 config.json 关键字段说明:
{ "train": { "log_interval": 200, "seed": 1234, "epochs": 100, "batch_size": 6, "learning_rate": 0.0001, "betas": [0.8, 0.99], "lr_decay": 0.999875, "save_every_epoch": 10, "if_cache_data_in_gpu": false }, "data": { "training_files": "filelists/train.txt", "validation_files": "filelists/val.txt", "sampling_rate": 44100, "filter_length": 2048, "hop_length": 512, "win_length": 2048, "n_speakers": 1 }, "model": { "inter_channels": 192, "hidden_channels": 192, "filter_channels": 768, "n_heads": 2, "n_layers": 6, "kernel_size": 3, "p_dropout": 0.1, "resblock": "1", "resblock_kernel_sizes": [3, 7, 11], "resblock_dilation_sizes": [[1, 3, 5], [1, 3, 5], [1, 3, 5]], "upsample_rates": [8, 8, 2, 2, 2], "upsample_initial_channel": 512, "upsample_kernel_sizes": [16, 16, 4, 4, 4], "n_layers_q": 3, "use_spectral_norm": false, "gin_channels": 256, "ssl_dim": 256, "n_speakers": 1 }, "spk": { "myvoice": 0 } }几个必须核对的字段:sampling_rate要和训练数据一致,44100 和 48000 不能混;n_speakers要和实际说话人数量匹配,单说话人就是 1;spk里的映射关系决定了推理时怎么指定音色。这些字段如果保存时写错,加载模型时会出现维度不匹配或者音色错乱。
保存操作本身很简单,把logs/44k下的 G、D 权重和 config.json 复制到归档目录,然后按上面的命名规范重命名:
# 创建归档目录 mkdir -p saved_models/myvoice_20240115 # 复制并重命名 cp logs/44k/G_44000.pth saved_models/myvoice_20240115/G_myvoice_44000.pth cp logs/44k/D_44000.pth saved_models/myvoice_20240115/D_myvoice_44000.pth cp logs/44k/config.json saved_models/myvoice_20240115/config_myvoice.json # 备份训练数据(如果 dataset_raw 还有用) cp -r dataset_raw/myvoice saved_models/myvoice_20240115/train_data_backup/注意dataset_raw在预处理后会被加载到dataset文件夹,训练时实际用的是dataset。如果你之后还要用这份数据,务必在换数据集之前备份,因为dataset_raw里同时放两份数据会导致预处理时把它们混在一起,炼出来的模型音色会出问题。
4. 加载验证:用命令确认模型能跑起来
保存完不代表结束,必须验证模型能正常加载。最直接的方式是用推理脚本加载 checkpoint 和 config,看是否能输出音频。
先确认你的推理入口脚本,通常是inference_main.py或 webui。用命令行方式验证:
# 加载模型进行推理测试 python inference_main.py \ -m saved_models/myvoice_20240115/G_myvoice_44000.pth \ -c saved_models/myvoice_20240115/config_myvoice.json \ -n test_input.wav \ -t 0 \ -s myvoice \ -f 0 \ -o output_test.wav参数说明:-m指定生成器权重,-c指定配置文件,-n是输入音频,-t是变调参数,-s是说话人名称(要和 config.json 里spk的 key 一致),-f是特征提取相关参数,-o是输出文件。
预期输出应该类似这样:
INFO:root:Loading model from saved_models/myvoice_20240115/G_myvoice_44000.pth INFO:root:Config loaded: sampling_rate=44100, n_speakers=1 INFO:root:Speaker myvoice mapped to id 0 INFO:root:Processing test_input.wav INFO:root:Inference done, saved to output_test.wav如果看到Config loaded和Speaker mapped这两行,说明配置和权重匹配成功。如果报错KeyError: 'myvoice',说明 config.json 里的spk字段没有这个说话人;如果报size mismatch,说明权重和配置的维度对不上,通常是n_speakers或gin_channels写错了。
另一种验证方式是在 Python 里直接加载:
import torch import json # 加载配置 with open("saved_models/myvoice_20240115/config_myvoice.json", "r") as f: config = json.load(f) print("sampling_rate:", config["data"]["sampling_rate"]) print("n_speakers:", config["data"]["n_speakers"]) print("speakers:", list(config["spk"].keys())) # 加载权重 checkpoint = torch.load("saved_models/myvoice_20240115/G_myvoice_44000.pth", map_location="cpu") print("checkpoint keys:", list(checkpoint.keys())[:5]) print("model state dict loaded:", "model" in checkpoint or "state_dict" in checkpoint)预期输出会打印出采样率、说话人数量和权重里的关键 key。如果checkpoint keys里能看到model或state_dict,说明权重文件完整。
如果你在验证过程中需要调用外部模型做对比或者辅助处理,可以用 TaoToken 的 API 通道,模型对话入口在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite ,统一 Key 直接调用,不用再单独配一套凭证。
5. 本篇常见错排查:保存后加载失败的几种情况
保存环节的坑主要集中在配置不匹配和文件缺失上。下面是我实际遇到过的几种典型报错和排查方法。
报错一:FileNotFoundError: config.json
原因通常是复制时路径写错,或者 config.json 没有跟着权重一起归档。排查方法是确认归档目录下三个文件都在:
ls -la saved_models/myvoice_20240115/ # 应该看到 G_*.pth、D_*.pth、config_*.json 三个文件报错二:RuntimeError: Error(s) in loading state_dict for SynthesizerTrn
这是权重和配置维度不匹配。最常见的是n_speakers不一致,比如训练时是 2 个说话人,保存的 config 里写成了 1。排查方法是打开 config.json 核对data.n_speakers和model.n_speakers是否一致,以及spk里的说话人数量是否匹配。
报错三:推理出来的音频是噪声或者音色不对
这种情况通常是采样率不匹配。训练数据是 44100,但推理时用了 48000 的配置,或者反过来。检查 config.json 里的data.sampling_rate,确保和训练时一致。另外hop_length和win_length也要核对,这两个参数影响音频帧的切分方式。
报错四:换数据集后上一个模型加载不了
这是因为logs/44k目录被新训练的模型覆盖了。sovits 训练时默认往logs/44k写 checkpoint,如果你没有在训练前把旧模型归档,新训练会直接覆盖。解决办法就是本文强调的:训练完立刻归档到saved_models下的独立目录,不要依赖logs目录长期保存。
报错五:KeyError: 'speaker_name'
config.json 里的spk字段没有包含你指定的说话人名称。检查spk的 key 是否和推理命令里-s参数一致。比如 config 里是"myvoice": 0,推理时就要用-s myvoice,不能写成-s MyVoice,大小写敏感。
排查时建议按这个顺序:先确认文件齐全,再核对 config 关键字段,最后检查推理参数。大部分问题在前两步就能定位。
6. 多模型管理与长期编码:把保存流程固化下来
单次保存做好之后,下一步是把流程固化,让每次训练完都能自动归档。我自己的做法是写一个保存脚本,训练结束后手动跑一次,把 checkpoint、config 和训练数据备份一次性搞定。
#!/bin/bash # save_model.sh MODEL_NAME=$1 DATE=$(date +%Y%m%d) SAVE_DIR="saved_models/${MODEL_NAME}_${DATE}" mkdir -p "$SAVE_DIR" # 复制权重和配置 cp logs/44k/G_*.pth "$SAVE_DIR/G_${MODEL_NAME}.pth" cp logs/44k/D_*.pth "$SAVE_DIR/D_${MODEL_NAME}.pth" cp logs/44k/config.json "$SAVE_DIR/config_${MODEL_NAME}.json" # 备份训练数据 if [ -d "dataset_raw/${MODEL_NAME}" ]; then cp -r "dataset_raw/${MODEL_NAME}" "$SAVE_DIR/train_data_backup/" fi echo "Model saved to $SAVE_DIR" ls -la "$SAVE_DIR"用法就是bash save_model.sh myvoice,脚本会自动按日期建目录并完成归档。这样每次训练完跑一次,不会漏文件也不会覆盖旧模型。
如果你长期做语音克隆开发,模型数量会越来越多,调用凭证的管理也会变复杂。TaoToken 的 Coding Plan 适合这种长期编码场景,把多个模型的调用统一到一个通道里,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有具体的配置示例。
API Key 管理页面在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,可以随时创建和吊销 Key。如果你用 Claude Code 做开发辅助,Anthropic 兼容入口在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude-code-anthropic&utm_campaign=rewrite ,配置方式和前面讲的 base_url 替换类似。
回到保存本身,最后再强调一个细节:每次训练前先确认logs/44k目录是干净的,或者至少把旧模型归档后再开始新训练。我踩过的坑就是直接开炼第二个模型,结果第一个模型的 checkpoint 被覆盖,只能重新训练。现在我的习惯是训练脚本跑完立刻执行保存脚本,归档完成后再动数据集。这样即使后面要回头炼第一个模型,把归档目录里的权重复制回logs/44k,删掉除 diffusion 文件夹外的旧文件,再运行 webui 就能恢复。整个流程跑顺之后,模型保存就不再是负担,而是标准动作。