mirror of
https://github.com/RVC-Boss/GPT-SoVITS.git
synced 2026-04-29 21:00:42 +08:00
425 lines
16 KiB
Python
425 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
GPT-SoVITS 持久化工具类
|
||
包含:模型配置、参考音频、推理参数 的持久化读写与管理
|
||
抽离自主文件,减少主文件臃肿,方便后续维护
|
||
"""
|
||
import json
|
||
import yaml
|
||
import hashlib
|
||
import os
|
||
import shutil
|
||
import random
|
||
from pathlib import Path
|
||
|
||
# ===================== 全局配置(统一管理所有持久化文件路径) =====================
|
||
# 模型持久化配置文件
|
||
LAST_SELECTED_MODELS_JSON = Path("./last_selected_models.json")
|
||
# 参考预设最后选中配置文件
|
||
LAST_SELECTED_PRESET_JSON = Path("./last_selected_preset.json")
|
||
# 参考音频持久化目录
|
||
REF_AUDIO_DIR = Path("GPT_SoVITS/ref_audios")
|
||
# 参考预设配置文件
|
||
REF_PRESETS_YAML = Path("GPT_SoVITS/configs/ref_audios_presets.yaml")
|
||
# 推理参数配置文件
|
||
INFER_SETTINGS_JSON = Path("GPT_SoVITS/configs/infer_settings.json")
|
||
|
||
# 参考音频配置常量
|
||
MAX_FILENAME_LENGTH = 40
|
||
INVALID_FILE_CHARS = set(r'\/:*?"<>|')
|
||
|
||
# 默认推理参数
|
||
DEFAULT_INFER_SETTINGS = {
|
||
"batch_size": 20,
|
||
"sample_steps": 32,
|
||
"fragment_interval": 0.2,
|
||
"speed_factor": 1.0,
|
||
"top_k": 5,
|
||
"top_p": 1.0,
|
||
"temperature": 1.0,
|
||
"repetition_penalty": 1.35,
|
||
"how_to_cut": "凑四句一切",
|
||
"super_sampling": False,
|
||
"parallel_infer": True,
|
||
"split_bucket": True,
|
||
"seed": -1,
|
||
"keep_random": True
|
||
}
|
||
|
||
# ===================== 通用工具函数(抽离重复逻辑) =====================
|
||
def sanitize_filename(name):
|
||
"""清理文件名中的非法字符,替换为下划线"""
|
||
if not name:
|
||
return "unnamed_preset"
|
||
return ''.join(c if c not in INVALID_FILE_CHARS else '_' for c in name)
|
||
|
||
def get_audio_md5(file_path, chunk_size=4096):
|
||
"""计算音频文件的MD5值(取前8位),用于区分不同音频内容"""
|
||
if not os.path.exists(file_path):
|
||
return "invalid_file"
|
||
try:
|
||
md5 = hashlib.md5()
|
||
with open(file_path, 'rb') as f:
|
||
while chunk := f.read(chunk_size):
|
||
md5.update(chunk)
|
||
return md5.hexdigest()[:8]
|
||
except Exception as e:
|
||
print(f"计算音频MD5失败:{e}")
|
||
return f"err_{random.randint(10000000, 99999999)}"
|
||
|
||
def ensure_dir_exists(dir_path):
|
||
"""确保目录存在,不存在则创建"""
|
||
if dir_path and not dir_path.exists():
|
||
dir_path.mkdir(exist_ok=True, parents=True)
|
||
|
||
# ===================== 1. 模型配置持久化(last_selected_models.json) =====================
|
||
def init_last_selected_models(gpt_default, sovits_default, current_version):
|
||
"""初始化模型配置文件,写入默认模型路径"""
|
||
ensure_dir_exists(LAST_SELECTED_MODELS_JSON.parent)
|
||
init_data = {
|
||
"gpt_model_path": gpt_default,
|
||
"sovits_model_path": sovits_default,
|
||
"version": current_version
|
||
}
|
||
with open(LAST_SELECTED_MODELS_JSON, "w", encoding="utf-8") as f:
|
||
json.dump(init_data, f, ensure_ascii=False, indent=4)
|
||
print(f"首次生成模型配置文件:{LAST_SELECTED_MODELS_JSON}")
|
||
return init_data
|
||
|
||
def read_last_selected_models():
|
||
"""读取模型配置文件中的路径"""
|
||
if not LAST_SELECTED_MODELS_JSON.exists():
|
||
return None
|
||
try:
|
||
with open(LAST_SELECTED_MODELS_JSON, "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
# 校验必要字段
|
||
required_fields = ["gpt_model_path", "sovits_model_path", "version"]
|
||
for field in required_fields:
|
||
if field not in data:
|
||
return None
|
||
return data
|
||
except Exception as e:
|
||
print(f"读取模型配置失败:{e}")
|
||
return None
|
||
|
||
def write_last_selected_models(gpt_path_new, sovits_path_new, current_version):
|
||
"""写入新的模型路径到配置文件"""
|
||
ensure_dir_exists(LAST_SELECTED_MODELS_JSON.parent)
|
||
try:
|
||
data = read_last_selected_models() or {}
|
||
data["gpt_model_path"] = gpt_path_new
|
||
data["sovits_model_path"] = sovits_path_new
|
||
data["version"] = current_version
|
||
with open(LAST_SELECTED_MODELS_JSON, "w", encoding="utf-8") as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||
except Exception as e:
|
||
print(f"写入模型配置失败:{e}")
|
||
|
||
# ===================== 2. 参考音频预设持久化(last_selected_preset.json + ref_audios_presets.yaml) =====================
|
||
# 2.1 最后选中预设的读写清
|
||
def read_last_selected_preset():
|
||
"""读取最后一次选中的预设名称"""
|
||
if not LAST_SELECTED_PRESET_JSON.exists():
|
||
return None
|
||
try:
|
||
with open(LAST_SELECTED_PRESET_JSON, "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
return data.get("last_selected_preset")
|
||
except Exception as e:
|
||
print(f"读取最后选中预设失败:{e}")
|
||
return None
|
||
|
||
def write_last_selected_preset(preset_name):
|
||
"""写入最后一次选中的预设名称"""
|
||
ensure_dir_exists(LAST_SELECTED_PRESET_JSON.parent)
|
||
try:
|
||
data = {"last_selected_preset": preset_name.strip()}
|
||
with open(LAST_SELECTED_PRESET_JSON, "w", encoding="utf-8") as f:
|
||
json.dump(data, f, ensure_ascii=False, indent=4)
|
||
print(f"已记录最后选中的预设:{preset_name.strip()}")
|
||
except Exception as e:
|
||
print(f"写入最后选中预设失败:{e}")
|
||
|
||
def clear_last_selected_preset():
|
||
"""清空最后选中的预设记录"""
|
||
if not LAST_SELECTED_PRESET_JSON.exists():
|
||
return
|
||
try:
|
||
with open(LAST_SELECTED_PRESET_JSON, "w", encoding="utf-8") as f:
|
||
json.dump({"last_selected_preset": ""}, f, ensure_ascii=False, indent=4)
|
||
except Exception as e:
|
||
print(f"清空最后选中预设失败:{e}")
|
||
|
||
# 2.2 参考预设配置的加载/保存/删除
|
||
def load_ref_presets():
|
||
"""加载多组参考预设配置"""
|
||
ensure_dir_exists(REF_PRESETS_YAML.parent)
|
||
|
||
# 新增:配置文件不存在时,自动创建空文件
|
||
if not REF_PRESETS_YAML.exists():
|
||
with open(REF_PRESETS_YAML, "w", encoding="utf-8") as f:
|
||
yaml.dump([], f, indent=4, allow_unicode=True)
|
||
print(f"暂未检测到参考预设配置文件,已自动创建空文件:{REF_PRESETS_YAML}")
|
||
return []
|
||
|
||
try:
|
||
with open(REF_PRESETS_YAML, "r", encoding="utf-8") as f:
|
||
presets = yaml.load(f, Loader=yaml.FullLoader) or []
|
||
|
||
# 兼容旧格式转换
|
||
if isinstance(presets, dict):
|
||
presets = [{"name": "旧配置转换", "ref_audio_path": presets.get("ref_audio_path"),
|
||
"prompt_text": presets.get("prompt_text", ""), "prompt_language": presets.get("prompt_language", "中文")}]
|
||
|
||
# 补充缺失字段 + 校验音频路径
|
||
default_template = {"name": "", "ref_audio_path": None, "prompt_text": "", "prompt_language": "中文"}
|
||
for preset in presets:
|
||
for key, value in default_template.items():
|
||
preset.setdefault(key, value)
|
||
# 校验音频路径有效性
|
||
audio_path = preset["ref_audio_path"]
|
||
if audio_path and not os.path.exists(str(audio_path)):
|
||
preset["ref_audio_path"] = None
|
||
|
||
# 清理冗余音频
|
||
clean_unreferenced_audios(presets)
|
||
print(f"参考预设加载成功,共 {len(presets)} 组")
|
||
return presets
|
||
except Exception as e:
|
||
print(f"参考预设加载失败:{e}")
|
||
return []
|
||
|
||
def get_preset_by_name(preset_name, presets=None):
|
||
"""根据配置名称查询对应的配置详情"""
|
||
# 核心修复:先判断 preset_name 是否为 None,避免 AttributeError
|
||
if preset_name is None:
|
||
return {"name": "", "ref_audio_path": None, "prompt_text": "", "prompt_language": "中文"}
|
||
|
||
if not presets:
|
||
presets = load_ref_presets()
|
||
|
||
# 现在再调用 strip(),确保 preset_name 不是 None
|
||
preset_name_str = preset_name.strip()
|
||
for preset in presets:
|
||
if preset["name"].strip() == preset_name_str:
|
||
return preset
|
||
|
||
# 无匹配预设时,返回空的合法预设字典
|
||
return {"name": "", "ref_audio_path": None, "prompt_text": "", "prompt_language": "中文"}
|
||
|
||
def save_ref_preset_core(preset_name, ref_audio_path, prompt_text, prompt_language, confirm_override=False):
|
||
"""保存/覆盖参考预设核心逻辑(返回:提示信息、是否成功、预设列表)"""
|
||
ensure_dir_exists(REF_AUDIO_DIR)
|
||
presets = load_ref_presets()
|
||
preset_name = preset_name.strip()
|
||
|
||
# 前置校验
|
||
if not ref_audio_path or not os.path.exists(str(ref_audio_path)):
|
||
return "保存失败!请先上传有效的主参考音频文件。", False, [p["name"] for p in presets]
|
||
if not preset_name:
|
||
return "保存失败!配置名称不能为空。", False, [p["name"] for p in presets]
|
||
|
||
# 音频持久化处理
|
||
persistent_audio_path = get_persistent_audio_path(ref_audio_path, preset_name)
|
||
if not persistent_audio_path:
|
||
return "保存失败!音频文件持久化存储失败。", False, [p["name"] for p in presets]
|
||
|
||
# 同名检测
|
||
preset_index = -1
|
||
for idx, p in enumerate(presets):
|
||
if p["name"].strip() == preset_name:
|
||
preset_index = idx
|
||
break
|
||
|
||
if preset_index >= 0 and not confirm_override:
|
||
return f"配置「{preset_name}」已存在,如需替换请确认覆盖!", False, [p["name"] for p in presets]
|
||
|
||
# 构造新配置
|
||
new_preset = {
|
||
"name": preset_name,
|
||
"ref_audio_path": persistent_audio_path,
|
||
"prompt_text": prompt_text,
|
||
"prompt_language": prompt_language
|
||
}
|
||
|
||
# 更新配置列表
|
||
is_new_preset = preset_index < 0
|
||
if preset_index >= 0:
|
||
presets[preset_index] = new_preset
|
||
tip = "同名配置已覆盖!"
|
||
else:
|
||
presets.append(new_preset)
|
||
tip = "新配置已新增!"
|
||
|
||
# 写入配置文件
|
||
try:
|
||
with open(REF_PRESETS_YAML, "w", encoding="utf-8") as f:
|
||
yaml.dump(presets, f, indent=4, allow_unicode=True)
|
||
|
||
# 新增预设自动记录为最后选中
|
||
if is_new_preset:
|
||
write_last_selected_preset(preset_name)
|
||
|
||
preset_names = [p["name"] for p in presets]
|
||
return f"配置保存成功!{tip}", True, preset_names
|
||
except Exception as e:
|
||
return f"保存失败:{str(e)}", False, [p["name"] for p in presets]
|
||
|
||
def delete_ref_preset_core(preset_name):
|
||
"""删除参考预设核心逻辑(返回:提示信息、预设列表、默认选中预设)"""
|
||
presets = load_ref_presets()
|
||
preset_name = preset_name.strip()
|
||
|
||
if not presets:
|
||
return "暂无配置可删除!", [], None
|
||
|
||
# 获取待删除音频路径
|
||
target_audio_path = None
|
||
for p in presets:
|
||
if p["name"].strip() == preset_name:
|
||
target_audio_path = p.get("ref_audio_path")
|
||
break
|
||
|
||
# 过滤删除
|
||
presets = [p for p in presets if p["name"].strip() != preset_name]
|
||
|
||
# 写入配置文件
|
||
try:
|
||
with open(REF_PRESETS_YAML, "w", encoding="utf-8") as f:
|
||
yaml.dump(presets, f, indent=4, allow_unicode=True)
|
||
|
||
# 删除对应音频
|
||
if target_audio_path and os.path.exists(target_audio_path):
|
||
os.unlink(target_audio_path)
|
||
print(f"同步删除配置对应音频:{target_audio_path}")
|
||
|
||
# 清空最后选中记录(若删除的是最后选中的预设)
|
||
last_selected = read_last_selected_preset()
|
||
if last_selected and last_selected == preset_name:
|
||
clear_last_selected_preset()
|
||
|
||
preset_names = [p["name"] for p in presets]
|
||
new_selected = preset_names[0] if preset_names else None
|
||
tip = "配置删除成功!已同步清理对应音频文件" if preset_names else "配置删除成功!已同步清理对应音频文件,当前无剩余配置"
|
||
return tip, preset_names, new_selected
|
||
except Exception as e:
|
||
return f"删除失败:{str(e)}", [p["name"] for p in presets], preset_name
|
||
|
||
# 2.3 参考音频文件管理
|
||
def get_persistent_audio_path(src_audio_path, preset_name):
|
||
"""获取音频持久化路径,清理同配置名旧音频"""
|
||
if not src_audio_path or not os.path.exists(src_audio_path):
|
||
return None
|
||
|
||
# 清理文件名
|
||
safe_preset_name = sanitize_filename(preset_name)
|
||
safe_preset_name = safe_preset_name[:MAX_FILENAME_LENGTH]
|
||
|
||
# 提取后缀
|
||
src_suffix = Path(src_audio_path).suffix.lower()
|
||
if not src_suffix or src_suffix not in [".wav", ".mp3", ".flac", ".ogg", ".m4a"]:
|
||
src_suffix = ".wav"
|
||
|
||
# 计算MD5
|
||
audio_md5 = get_audio_md5(src_audio_path)
|
||
dst_filename = f"{safe_preset_name}_{audio_md5}{src_suffix}"
|
||
dst_path = REF_AUDIO_DIR / dst_filename
|
||
|
||
# 清理同配置名旧音频
|
||
for old_audio in REF_AUDIO_DIR.glob(f"{safe_preset_name}_*"):
|
||
if old_audio.suffix.lower() in [".wav", ".mp3", ".flac", ".ogg", ".m4a"]:
|
||
try:
|
||
old_audio.unlink()
|
||
except Exception as e:
|
||
print(f"清理旧音频失败:{e}")
|
||
|
||
# 复制新音频
|
||
try:
|
||
shutil.copy2(src_audio_path, dst_path)
|
||
return str(dst_path)
|
||
except Exception as e:
|
||
print(f"音频持久化复制失败:{e}")
|
||
return None
|
||
|
||
def clean_unreferenced_audios(presets):
|
||
"""清理未被任何预设引用的冗余音频"""
|
||
if not REF_AUDIO_DIR.exists():
|
||
return
|
||
|
||
# 收集已引用音频
|
||
referenced = set()
|
||
for preset in presets:
|
||
audio_path = preset.get("ref_audio_path")
|
||
if audio_path and os.path.exists(audio_path):
|
||
referenced.add(Path(audio_path).absolute())
|
||
|
||
# 删除未引用音频
|
||
deleted_count = 0
|
||
for audio_file in REF_AUDIO_DIR.glob("*"):
|
||
if audio_file.is_file() and audio_file.suffix.lower() in [".wav", ".mp3", ".flac", ".ogg", ".m4a"]:
|
||
if audio_file.absolute() not in referenced:
|
||
try:
|
||
audio_file.unlink()
|
||
deleted_count += 1
|
||
except Exception as e:
|
||
print(f"清理冗余音频失败:{e}")
|
||
|
||
if deleted_count > 0:
|
||
print(f"清理冗余未引用音频 {deleted_count} 个")
|
||
|
||
# ===================== 3. 推理参数持久化(infer_settings.json) =====================
|
||
def load_infer_settings():
|
||
"""加载推理参数配置"""
|
||
ensure_dir_exists(INFER_SETTINGS_JSON.parent)
|
||
if not INFER_SETTINGS_JSON.exists():
|
||
return DEFAULT_INFER_SETTINGS
|
||
try:
|
||
with open(INFER_SETTINGS_JSON, "r", encoding="utf-8") as f:
|
||
saved = json.load(f)
|
||
return {**DEFAULT_INFER_SETTINGS, **saved}
|
||
except Exception as e:
|
||
print(f"加载推理参数失败,使用默认值:{e}")
|
||
return DEFAULT_INFER_SETTINGS
|
||
|
||
def save_infer_settings_core(settings):
|
||
"""保存推理参数核心逻辑(返回:提示信息)"""
|
||
ensure_dir_exists(INFER_SETTINGS_JSON.parent)
|
||
try:
|
||
with open(INFER_SETTINGS_JSON, "w", encoding="utf-8") as f:
|
||
json.dump(settings, f, indent=4, ensure_ascii=False)
|
||
|
||
# 精简日志输出
|
||
print(f"✅ 推理配置保存成功:{INFER_SETTINGS_JSON.absolute()}")
|
||
return "推理设置保存成功!已覆盖原有配置文件。"
|
||
except Exception as e:
|
||
print(f"❌ 推理配置保存失败:{e}")
|
||
return f"推理设置保存失败:{str(e)}"
|
||
|
||
def restore_default_infer_settings_core():
|
||
"""恢复推理参数默认值核心逻辑(返回:默认参数列表)"""
|
||
ensure_dir_exists(INFER_SETTINGS_JSON.parent)
|
||
try:
|
||
with open(INFER_SETTINGS_JSON, "w", encoding="utf-8") as f:
|
||
json.dump(DEFAULT_INFER_SETTINGS, f, indent=4, ensure_ascii=False)
|
||
print(f"✅ 推理配置已恢复默认值:{INFER_SETTINGS_JSON.absolute()}")
|
||
except Exception as e:
|
||
print(f"❌ 推理配置恢复默认失败:{e}")
|
||
|
||
# 返回默认参数(按顺序对应UI组件)
|
||
return [
|
||
DEFAULT_INFER_SETTINGS["batch_size"],
|
||
DEFAULT_INFER_SETTINGS["sample_steps"],
|
||
DEFAULT_INFER_SETTINGS["fragment_interval"],
|
||
DEFAULT_INFER_SETTINGS["speed_factor"],
|
||
DEFAULT_INFER_SETTINGS["top_k"],
|
||
DEFAULT_INFER_SETTINGS["top_p"],
|
||
DEFAULT_INFER_SETTINGS["temperature"],
|
||
DEFAULT_INFER_SETTINGS["repetition_penalty"],
|
||
DEFAULT_INFER_SETTINGS["how_to_cut"],
|
||
DEFAULT_INFER_SETTINGS["super_sampling"],
|
||
DEFAULT_INFER_SETTINGS["parallel_infer"],
|
||
DEFAULT_INFER_SETTINGS["split_bucket"],
|
||
DEFAULT_INFER_SETTINGS["seed"],
|
||
DEFAULT_INFER_SETTINGS["keep_random"]
|
||
] |