From 8976a3ed2fc0045fef877157bab1d2033777efa3 Mon Sep 17 00:00:00 2001 From: litagin02 Date: Sat, 25 May 2024 10:25:22 +0900 Subject: [PATCH] Refactor: Use PathConfig and pathlib instead of paths.yml loading --- app.py | 10 +- bert_gen.py | 6 +- colab.ipynb | 2 +- config.py | 171 ++++++++++++++++++---------------- configs/config.json | 2 +- configs/config_jp_extra.json | 2 +- default_style.py | 1 - gradio_tabs/merge.py | 128 +++++++++++++------------ gradio_tabs/style_vectors.py | 50 +++++----- gradio_tabs/train.py | 78 ++++++++++------ preprocess_text.py | 4 +- requirements.txt | 1 + resample.py | 3 +- server_editor.py | 14 +-- server_fastapi.py | 6 +- slice.py | 8 +- speech_mos.py | 13 +-- style_bert_vits2/constants.py | 2 +- style_gen.py | 4 +- train_ms.py | 4 +- train_ms_jp_extra.py | 4 +- transcribe.py | 6 +- 22 files changed, 266 insertions(+), 253 deletions(-) diff --git a/app.py b/app.py index f556a23..2df9386 100644 --- a/app.py +++ b/app.py @@ -3,7 +3,6 @@ from pathlib import Path import gradio as gr import torch -import yaml from gradio_tabs.dataset import create_dataset_app from gradio_tabs.inference import create_inference_app @@ -14,6 +13,7 @@ from style_bert_vits2.constants import GRADIO_THEME, VERSION from style_bert_vits2.nlp.japanese import pyopenjtalk_worker from style_bert_vits2.nlp.japanese.user_dict import update_dict from style_bert_vits2.tts_model import TTSModelHolder +from config import get_path_config # このプロセスからはワーカーを起動して辞書を使いたいので、ここで初期化 @@ -22,11 +22,6 @@ pyopenjtalk_worker.initialize_worker() # dict_data/ 以下の辞書データを pyopenjtalk に適用 update_dict() -# Get path settings -with Path("configs/paths.yml").open("r", encoding="utf-8") as f: - path_config: dict[str, str] = yaml.safe_load(f.read()) - # dataset_root = path_config["dataset_root"] - assets_root = path_config["assets_root"] parser = argparse.ArgumentParser() parser.add_argument("--device", type=str, default="cuda") @@ -40,7 +35,8 @@ device = args.device if device == "cuda" and not torch.cuda.is_available(): device = "cpu" -model_holder = TTSModelHolder(Path(assets_root), device) +path_config = get_path_config() +model_holder = TTSModelHolder(Path(path_config.assets_root), device) with gr.Blocks(theme=GRADIO_THEME) as app: gr.Markdown(f"# Style-Bert-VITS2 WebUI (version {VERSION})") diff --git a/bert_gen.py b/bert_gen.py index 1dcab9e..af50b60 100644 --- a/bert_gen.py +++ b/bert_gen.py @@ -5,13 +5,12 @@ import torch import torch.multiprocessing as mp from tqdm import tqdm -from config import config +from config import get_config from style_bert_vits2.constants import Languages from style_bert_vits2.logging import logger from style_bert_vits2.models import commons from style_bert_vits2.models.hyper_parameters import HyperParameters from style_bert_vits2.nlp import ( - bert_models, cleaned_text_to_sequence, extract_bert_feature, ) @@ -20,6 +19,7 @@ from style_bert_vits2.nlp.japanese.user_dict import update_dict from style_bert_vits2.utils.stdout_wrapper import SAFE_STDOUT +config = get_config() # このプロセスからはワーカーを起動して辞書を使いたいので、ここで初期化 pyopenjtalk_worker.initialize_worker() @@ -61,7 +61,7 @@ def process_line(x: tuple[str, bool]): bert = torch.load(bert_path) assert bert.shape[-1] == len(phone) except Exception: - bert = extract_bert_feature(text, word2ph, language_str, device) + bert = extract_bert_feature(text, word2ph, Languages(language_str), device) assert bert.shape[-1] == len(phone) torch.save(bert, bert_path) diff --git a/colab.ipynb b/colab.ipynb index 4f21124..a9aaa93 100644 --- a/colab.ipynb +++ b/colab.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Style-Bert-VITS2 (ver 2.4.1) のGoogle Colabでの学習\n", + "# Style-Bert-VITS2 (ver 2.5.0) のGoogle Colabでの学習\n", "\n", "Google Colab上でStyle-Bert-VITS2の学習を行うことができます。\n", "\n", diff --git a/config.py b/config.py index 2229e61..51e05d4 100644 --- a/config.py +++ b/config.py @@ -2,9 +2,9 @@ @Desc: 全局配置文件读取 """ -import os import shutil -from typing import Dict, List +from pathlib import Path +from typing import Any import torch import yaml @@ -12,6 +12,12 @@ import yaml from style_bert_vits2.logging import logger +class PathConfig: + def __init__(self, dataset_root: str, assets_root: str): + self.dataset_root = Path(dataset_root) + self.assets_root = Path(assets_root) + + # If not cuda available, set possible devices to cpu cuda_available = torch.cuda.is_available() @@ -20,17 +26,17 @@ class Resample_config: """重采样配置""" def __init__(self, in_dir: str, out_dir: str, sampling_rate: int = 44100): - self.sampling_rate: int = sampling_rate # 目标采样率 - self.in_dir: str = in_dir # 待处理音频目录路径 - self.out_dir: str = out_dir # 重采样输出路径 + self.sampling_rate = sampling_rate # 目标采样率 + self.in_dir = Path(in_dir) # 待处理音频目录路径 + self.out_dir = Path(out_dir) # 重采样输出路径 @classmethod - def from_dict(cls, dataset_path: str, data: Dict[str, any]): + def from_dict(cls, dataset_path: Path, data: dict[str, Any]): """从字典中生成实例""" # 不检查路径是否有效,此逻辑在resample.py中处理 - data["in_dir"] = os.path.join(dataset_path, data["in_dir"]) - data["out_dir"] = os.path.join(dataset_path, data["out_dir"]) + data["in_dir"] = dataset_path / data["in_dir"] + data["out_dir"] = dataset_path / data["out_dir"] return cls(**data) @@ -49,39 +55,27 @@ class Preprocess_text_config: max_val_total: int = 10000, clean: bool = True, ): - self.transcription_path: str = ( - transcription_path # 原始文本文件路径,文本格式应为{wav_path}|{speaker_name}|{language}|{text}。 - ) - self.cleaned_path: str = ( - cleaned_path # 数据清洗后文本路径,可以不填。不填则将在原始文本目录生成 - ) - self.train_path: str = ( - train_path # 训练集路径,可以不填。不填则将在原始文本目录生成 - ) - self.val_path: str = ( - val_path # 验证集路径,可以不填。不填则将在原始文本目录生成 - ) - self.config_path: str = config_path # 配置文件路径 - self.val_per_lang: int = val_per_lang # 每个speaker的验证集条数 - self.max_val_total: int = ( - max_val_total # 验证集最大条数,多于的会被截断并放到训练集中 - ) - self.clean: bool = clean # 是否进行数据清洗 + self.transcription_path = Path(transcription_path) + self.cleaned_path = Path(cleaned_path) + self.train_path = Path(train_path) + self.val_path = Path(val_path) + self.config_path = Path(config_path) + self.val_per_lang = val_per_lang + self.max_val_total = max_val_total + self.clean = clean @classmethod - def from_dict(cls, dataset_path: str, data: Dict[str, any]): + def from_dict(cls, dataset_path: Path, data: dict[str, Any]): """从字典中生成实例""" - data["transcription_path"] = os.path.join( - dataset_path, data["transcription_path"] - ) + data["transcription_path"] = dataset_path / data["transcription_path"] if data["cleaned_path"] == "" or data["cleaned_path"] is None: data["cleaned_path"] = None else: - data["cleaned_path"] = os.path.join(dataset_path, data["cleaned_path"]) - data["train_path"] = os.path.join(dataset_path, data["train_path"]) - data["val_path"] = os.path.join(dataset_path, data["val_path"]) - data["config_path"] = os.path.join(dataset_path, data["config_path"]) + data["cleaned_path"] = dataset_path / data["cleaned_path"] + data["train_path"] = dataset_path / data["train_path"] + data["val_path"] = dataset_path / data["val_path"] + data["config_path"] = dataset_path / data["config_path"] return cls(**data) @@ -96,7 +90,7 @@ class Bert_gen_config: device: str = "cuda", use_multi_device: bool = False, ): - self.config_path = config_path + self.config_path = Path(config_path) self.num_processes = num_processes if not cuda_available: device = "cpu" @@ -104,8 +98,8 @@ class Bert_gen_config: self.use_multi_device = use_multi_device @classmethod - def from_dict(cls, dataset_path: str, data: Dict[str, any]): - data["config_path"] = os.path.join(dataset_path, data["config_path"]) + def from_dict(cls, dataset_path: Path, data: dict[str, Any]): + data["config_path"] = dataset_path / data["config_path"] return cls(**data) @@ -119,15 +113,15 @@ class Style_gen_config: num_processes: int = 4, device: str = "cuda", ): - self.config_path = config_path + self.config_path = Path(config_path) self.num_processes = num_processes if not cuda_available: device = "cpu" self.device = device @classmethod - def from_dict(cls, dataset_path: str, data: Dict[str, any]): - data["config_path"] = os.path.join(dataset_path, data["config_path"]) + def from_dict(cls, dataset_path: Path, data: dict[str, Any]): + data["config_path"] = dataset_path / data["config_path"] return cls(**data) @@ -138,7 +132,7 @@ class Train_ms_config: def __init__( self, config_path: str, - env: Dict[str, any], + env: dict[str, Any], # base: Dict[str, any], model_dir: str, num_workers: int, @@ -147,16 +141,18 @@ class Train_ms_config: ): self.env = env # 需要加载的环境变量 # self.base = base # 底模配置 - self.model_dir = model_dir # 训练模型存储目录,该路径为相对于dataset_path的路径,而非项目根目录 - self.config_path = config_path # 配置文件路径 + self.model_dir = Path( + model_dir + ) # 训练模型存储目录,该路径为相对于dataset_path的路径,而非项目根目录 + self.config_path = Path(config_path) # 配置文件路径 self.num_workers = num_workers # worker数量 self.spec_cache = spec_cache # 是否启用spec缓存 self.keep_ckpts = keep_ckpts # ckpt数量 @classmethod - def from_dict(cls, dataset_path: str, data: Dict[str, any]): + def from_dict(cls, dataset_path: Path, data: dict[str, Any]): # data["model"] = os.path.join(dataset_path, data["model"]) - data["config_path"] = os.path.join(dataset_path, data["config_path"]) + data["config_path"] = dataset_path / data["config_path"] return cls(**data) @@ -176,20 +172,18 @@ class Webui_config: ): if not cuda_available: device = "cpu" - self.device: str = device - self.model: str = model # 端口号 - self.config_path: str = config_path # 是否公开部署,对外网开放 - self.port: int = port # 是否开启debug模式 - self.share: bool = share # 模型路径 - self.debug: bool = debug # 配置文件路径 - self.language_identification_library: str = ( - language_identification_library # 语种识别库 - ) + self.device = device + self.model = Path(model) + self.config_path = Path(config_path) + self.port: int = port + self.share: bool = share + self.debug: bool = debug + self.language_identification_library: str = language_identification_library @classmethod - def from_dict(cls, dataset_path: str, data: Dict[str, any]): - data["config_path"] = os.path.join(dataset_path, data["config_path"]) - data["model"] = os.path.join(dataset_path, data["model"]) + def from_dict(cls, dataset_path: Path, data: dict[str, Any]): + data["config_path"] = dataset_path / data["config_path"] + data["model"] = dataset_path / data["model"] return cls(**data) @@ -200,7 +194,7 @@ class Server_config: device: str = "cuda", limit: int = 100, language: str = "JP", - origins: List[str] = None, + origins: list[str] = ["*"], ): self.port: int = port if not cuda_available: @@ -208,10 +202,10 @@ class Server_config: self.device: str = device self.language: str = language self.limit: int = limit - self.origins: List[str] = origins + self.origins: list[str] = origins @classmethod - def from_dict(cls, data: Dict[str, any]): + def from_dict(cls, data: dict[str, Any]): return cls(**data) @@ -223,32 +217,32 @@ class Translate_config: self.secret_key = secret_key @classmethod - def from_dict(cls, data: Dict[str, any]): + def from_dict(cls, data: dict[str, Any]): return cls(**data) class Config: - def __init__(self, config_path: str, path_config: dict[str, str]): - if not os.path.isfile(config_path) and os.path.isfile("default_config.yml"): + def __init__(self, config_path: str, path_config: PathConfig): + if not Path(config_path).exists(): shutil.copy(src="default_config.yml", dst=config_path) logger.info( f"A configuration file {config_path} has been generated based on the default configuration file default_config.yml." ) logger.info( - "If you have no special needs, please do not modify default_config.yml." + "Please do not modify default_config.yml. Instead, modify config.yml." ) # sys.exit(0) with open(config_path, "r", encoding="utf-8") as file: - yaml_config: Dict[str, any] = yaml.safe_load(file.read()) + yaml_config: dict[str, Any] = yaml.safe_load(file.read()) model_name: str = yaml_config["model_name"] self.model_name: str = model_name if "dataset_path" in yaml_config: - dataset_path = yaml_config["dataset_path"] + dataset_path = Path(yaml_config["dataset_path"]) else: - dataset_path = os.path.join(path_config["dataset_root"], model_name) - self.dataset_path: str = dataset_path - self.assets_root: str = path_config["assets_root"] - self.out_dir = os.path.join(self.assets_root, model_name) + dataset_path = path_config.dataset_root / model_name + self.dataset_path = dataset_path + self.assets_root = path_config.assets_root + self.out_dir = self.assets_root / model_name self.resample_config: Resample_config = Resample_config.from_dict( dataset_path, yaml_config["resample"] ) @@ -277,16 +271,31 @@ class Config: # ) -with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f: - path_config: dict[str, str] = yaml.safe_load(f.read()) - # Should contain the following keys: - # - dataset_root: the root directory of the dataset, default to "Data" - # - assets_root: the root directory of the assets, default to "model_assets" +# Load and initialize the configuration -try: - config = Config("config.yml", path_config) -except (TypeError, KeyError): - logger.warning("Old config.yml found. Replace it with default_config.yml.") - shutil.copy(src="default_config.yml", dst="config.yml") - config = Config("config.yml", path_config) +def get_path_config() -> PathConfig: + path_config_path = Path("configs/paths.yml") + if not path_config_path.exists(): + shutil.copy(src="configs/default_paths.yml", dst=path_config_path) + logger.info( + f"A configuration file {path_config_path} has been generated based on the default configuration file default_paths.yml." + ) + logger.info( + "Please do not modify configs/default_paths.yml. Instead, modify configs/paths.yml." + ) + with open(path_config_path, "r", encoding="utf-8") as file: + path_config_dict: dict[str, str] = yaml.safe_load(file.read()) + return PathConfig(**path_config_dict) + + +def get_config() -> Config: + path_config = get_path_config() + try: + config = Config("config.yml", path_config) + except (TypeError, KeyError): + logger.warning("Old config.yml found. Replace it with default_config.yml.") + shutil.copy(src="default_config.yml", dst="config.yml") + config = Config("config.yml", path_config) + + return config diff --git a/configs/config.json b/configs/config.json index 2f3988b..75629e9 100644 --- a/configs/config.json +++ b/configs/config.json @@ -69,5 +69,5 @@ "use_spectral_norm": false, "gin_channels": 256 }, - "version": "2.4.1" + "version": "2.5.0" } diff --git a/configs/config_jp_extra.json b/configs/config_jp_extra.json index d754809..d7e135a 100644 --- a/configs/config_jp_extra.json +++ b/configs/config_jp_extra.json @@ -76,5 +76,5 @@ "initial_channel": 64 } }, - "version": "2.4.1-JP-Extra" + "version": "2.5.0-JP-Extra" } diff --git a/default_style.py b/default_style.py index 49881eb..3897cb0 100644 --- a/default_style.py +++ b/default_style.py @@ -1,5 +1,4 @@ import json -import os from pathlib import Path from typing import Union diff --git a/gradio_tabs/merge.py b/gradio_tabs/merge.py index 661f233..c6348dd 100644 --- a/gradio_tabs/merge.py +++ b/gradio_tabs/merge.py @@ -1,14 +1,13 @@ import json -import os from pathlib import Path import gradio as gr import numpy as np import torch -import yaml from safetensors import safe_open from safetensors.torch import save_file +from config import get_path_config from style_bert_vits2.constants import DEFAULT_STYLE, GRADIO_THEME from style_bert_vits2.logging import logger from style_bert_vits2.tts_model import TTSModel, TTSModelHolder @@ -20,15 +19,17 @@ speech_style_keys = ["enc_p"] tempo_keys = ["sdp", "dp"] device = "cuda" if torch.cuda.is_available() else "cpu" - -# Get path settings -with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f: - path_config: dict[str, str] = yaml.safe_load(f.read()) - # dataset_root = path_config["dataset_root"] - assets_root = path_config["assets_root"] +path_config = get_path_config() +assets_root = path_config.assets_root -def merge_style(model_name_a, model_name_b, weight, output_name, style_triple_list): +def merge_style( + model_name_a: str, + model_name_b: str, + weight: float, + output_name: str, + style_triple_list: list[tuple[str, str, str]], +) -> tuple[Path, list[str]]: """ style_triple_list: list[(model_aでのスタイル名, model_bでのスタイル名, 出力するスタイル名)] """ @@ -41,18 +42,14 @@ def merge_style(model_name_a, model_name_b, weight, output_name, style_triple_li raise ValueError(f"No element with {DEFAULT_STYLE} output style name found.") style_vectors_a = np.load( - os.path.join(assets_root, model_name_a, "style_vectors.npy") + assets_root / model_name_a / "style_vectors.npy" ) # (style_num_a, 256) style_vectors_b = np.load( - os.path.join(assets_root, model_name_b, "style_vectors.npy") + assets_root / model_name_b / "style_vectors.npy" ) # (style_num_b, 256) - with open( - os.path.join(assets_root, model_name_a, "config.json"), "r", encoding="utf-8" - ) as f: + with open(assets_root / model_name_a / "config.json", "r", encoding="utf-8") as f: config_a = json.load(f) - with open( - os.path.join(assets_root, model_name_b, "config.json"), "r", encoding="utf-8" - ) as f: + with open(assets_root / model_name_b / "config.json", "r", encoding="utf-8") as f: config_b = json.load(f) style2id_a = config_a["data"]["style2id"] style2id_b = config_b["data"]["style2id"] @@ -73,21 +70,19 @@ def merge_style(model_name_a, model_name_b, weight, output_name, style_triple_li new_style2id[style_out] = len(new_style_vecs) - 1 new_style_vecs = np.array(new_style_vecs) - output_style_path = os.path.join(assets_root, output_name, "style_vectors.npy") + output_style_path = assets_root / output_name / "style_vectors.npy" np.save(output_style_path, new_style_vecs) new_config = config_a.copy() new_config["data"]["num_styles"] = len(new_style2id) new_config["data"]["style2id"] = new_style2id new_config["model_name"] = output_name - with open( - os.path.join(assets_root, output_name, "config.json"), "w", encoding="utf-8" - ) as f: + with open(assets_root / output_name / "config.json", "w", encoding="utf-8") as f: json.dump(new_config, f, indent=2, ensure_ascii=False) # recipe.jsonを読み込んで、style_triple_listを追記 - info_path = os.path.join(assets_root, output_name, "recipe.json") - if os.path.exists(info_path): + info_path = assets_root / output_name / "recipe.json" + if info_path.exists(): with open(info_path, "r", encoding="utf-8") as f: info = json.load(f) else: @@ -99,11 +94,13 @@ def merge_style(model_name_a, model_name_b, weight, output_name, style_triple_li return output_style_path, list(new_style2id.keys()) -def lerp_tensors(t, v0, v1): +def lerp_tensors(t: float, v0: torch.Tensor, v1: torch.Tensor): return v0 * (1 - t) + v1 * t -def slerp_tensors(t, v0, v1, dot_thres=0.998): +def slerp_tensors( + t: float, v0: torch.Tensor, v1: torch.Tensor, dot_thres: float = 0.998 +): device = v0.device v0c = v0.cpu().numpy() v1c = v1.cpu().numpy() @@ -123,23 +120,23 @@ def slerp_tensors(t, v0, v1, dot_thres=0.998): def merge_models( - model_path_a, - model_path_b, - voice_weight, - voice_pitch_weight, - speech_style_weight, - tempo_weight, - output_name, - use_slerp_instead_of_lerp, + model_path_a: str, + model_path_b: str, + voice_weight: float, + voice_pitch_weight: float, + speech_style_weight: float, + tempo_weight: float, + output_name: str, + use_slerp_instead_of_lerp: bool, ): """model Aを起点に、model Bの各要素を重み付けしてマージする。 safetensors形式を前提とする。""" - model_a_weight = {} + model_a_weight: dict[str, torch.Tensor] = {} with safe_open(model_path_a, framework="pt", device="cpu") as f: for k in f.keys(): model_a_weight[k] = f.get_tensor(k) - model_b_weight = {} + model_b_weight: dict[str, torch.Tensor] = {} with safe_open(model_path_b, framework="pt", device="cpu") as f: for k in f.keys(): model_b_weight[k] = f.get_tensor(k) @@ -161,10 +158,8 @@ def merge_models( slerp_tensors if use_slerp_instead_of_lerp else lerp_tensors )(weight, model_a_weight[key], model_b_weight[key]) - merged_model_path = os.path.join( - assets_root, output_name, f"{output_name}.safetensors" - ) - os.makedirs(os.path.dirname(merged_model_path), exist_ok=True) + merged_model_path = assets_root / output_name / f"{output_name}.safetensors" + merged_model_path.parent.mkdir(parents=True, exist_ok=True) save_file(merged_model_weight, merged_model_path) info = { @@ -175,24 +170,22 @@ def merge_models( "speech_style_weight": speech_style_weight, "tempo_weight": tempo_weight, } - with open( - os.path.join(assets_root, output_name, "recipe.json"), "w", encoding="utf-8" - ) as f: + with open(assets_root / output_name / "recipe.json", "w", encoding="utf-8") as f: json.dump(info, f, indent=2, ensure_ascii=False) return merged_model_path def merge_models_gr( - model_name_a, - model_path_a, - model_name_b, - model_path_b, - output_name, - voice_weight, - voice_pitch_weight, - speech_style_weight, - tempo_weight, - use_slerp_instead_of_lerp, + model_name_a: str, + model_path_a: str, + model_name_b: str, + model_path_b: str, + output_name: str, + voice_weight: float, + voice_pitch_weight: float, + speech_style_weight: float, + tempo_weight: float, + use_slerp_instead_of_lerp: bool, ): if output_name == "": return "Error: 新しいモデル名を入力してください。" @@ -210,10 +203,10 @@ def merge_models_gr( def merge_style_gr( - model_name_a, - model_name_b, - weight, - output_name, + model_name_a: str, + model_name_b: str, + weight: float, + output_name: str, style_triple_list_str: str, ): if output_name == "": @@ -245,12 +238,14 @@ def merge_style_gr( ) -def simple_tts(model_name, text, style=DEFAULT_STYLE, style_weight=1.0): - model_path = os.path.join(assets_root, model_name, f"{model_name}.safetensors") - config_path = os.path.join(assets_root, model_name, "config.json") - style_vec_path = os.path.join(assets_root, model_name, "style_vectors.npy") +def simple_tts( + model_name: str, text: str, style: str = DEFAULT_STYLE, style_weight: float = 1.0 +): + model_path = assets_root / model_name / f"{model_name}.safetensors" + config_path = assets_root / model_name / "config.json" + style_vec_path = assets_root / model_name / "style_vectors.npy" - model = TTSModel(Path(model_path), Path(config_path), Path(style_vec_path), device) + model = TTSModel(model_path, config_path, style_vec_path, device) return model.infer(text, style=style, style_weight=style_weight) @@ -259,13 +254,13 @@ def update_two_model_names_dropdown(model_holder: TTSModelHolder): return new_names, new_files, new_names, new_files -def load_styles_gr(model_name_a, model_name_b): - config_path_a = os.path.join(assets_root, model_name_a, "config.json") +def load_styles_gr(model_name_a: str, model_name_b: str): + config_path_a = assets_root / model_name_a / "config.json" with open(config_path_a, "r", encoding="utf-8") as f: config_a = json.load(f) styles_a = list(config_a["data"]["style2id"].keys()) - config_path_b = os.path.join(assets_root, model_name_b, "config.json") + config_path_b = assets_root / model_name_b / "config.json" with open(config_path_b, "r", encoding="utf-8") as f: config_b = json.load(f) styles_b = list(config_b["data"]["style2id"].keys()) @@ -336,7 +331,10 @@ def create_merge_app(model_holder: TTSModelHolder) -> gr.Blocks: ) return app initial_id = 0 - initial_model_files = model_holder.model_files_dict[model_names[initial_id]] + # initial_model_files = model_holder.model_files_dict[model_names[initial_id]] + initial_model_files = [ + str(f) for f in model_holder.model_files_dict[model_names[initial_id]] + ] with gr.Blocks(theme=GRADIO_THEME) as app: gr.Markdown( diff --git a/gradio_tabs/style_vectors.py b/gradio_tabs/style_vectors.py index 1800c85..90cb04d 100644 --- a/gradio_tabs/style_vectors.py +++ b/gradio_tabs/style_vectors.py @@ -1,27 +1,23 @@ import json -import os import shutil from pathlib import Path import gradio as gr import matplotlib.pyplot as plt import numpy as np -import yaml from scipy.spatial.distance import pdist, squareform from sklearn.cluster import DBSCAN, AgglomerativeClustering, KMeans from sklearn.manifold import TSNE from umap import UMAP -from config import config +from config import get_path_config from style_bert_vits2.constants import DEFAULT_STYLE, GRADIO_THEME from style_bert_vits2.logging import logger -# Get path settings -with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f: - path_config: dict[str, str] = yaml.safe_load(f.read()) - dataset_root = Path(path_config["dataset_root"]) - # assets_root = path_config["assets_root"] +path_config = get_path_config() +dataset_root = path_config.dataset_root +assets_root = path_config.assets_root MAX_CLUSTER_NUM = 10 MAX_AUDIO_NUM = 10 @@ -39,11 +35,7 @@ centroids = [] def load(model_name: str, reduction_method: str): global wav_files, x, x_reduced, mean - # wavs_dir = os.path.join(dataset_root, model_name, "wavs") wavs_dir = dataset_root / model_name / "wavs" - # style_vector_files = [ - # os.path.join(wavs_dir, f) for f in os.listdir(wavs_dir) if f.endswith(".npy") - # ] style_vector_files = [f for f in wavs_dir.rglob("*.npy") if f.is_file()] # foo.wav.npy -> foo.wav wav_files = [f.with_suffix("") for f in style_vector_files] @@ -196,21 +188,21 @@ def do_clustering_gradio(n_clusters=4, method="KMeans"): ] * MAX_AUDIO_NUM -def save_style_vectors_from_clustering(model_name, style_names_str: str): +def save_style_vectors_from_clustering(model_name: str, style_names_str: str): """centerとcentroidsを保存する""" - result_dir = os.path.join(config.assets_root, model_name) - os.makedirs(result_dir, exist_ok=True) + result_dir = assets_root / model_name + result_dir.mkdir(parents=True, exist_ok=True) style_vectors = np.stack([mean] + centroids) - style_vector_path = os.path.join(result_dir, "style_vectors.npy") - if os.path.exists(style_vector_path): + style_vector_path = result_dir / "style_vectors.npy" + if style_vector_path.exists(): logger.info(f"Backup {style_vector_path} to {style_vector_path}.bak") shutil.copy(style_vector_path, f"{style_vector_path}.bak") np.save(style_vector_path, style_vectors) logger.success(f"Saved style vectors to {style_vector_path}") # config.jsonの更新 - config_path = os.path.join(result_dir, "config.json") - if not os.path.exists(config_path): + config_path = result_dir / "config.json" + if not config_path.exists(): return f"{config_path}が存在しません。" style_names = [name.strip() for name in style_names_str.split(",")] style_name_list = [DEFAULT_STYLE] + style_names @@ -233,7 +225,7 @@ def save_style_vectors_from_clustering(model_name, style_names_str: str): def save_style_vectors_from_files( - model_name, audio_files_str: str, style_names_str: str + model_name: str, audio_files_str: str, style_names_str: str ): """音声ファイルからスタイルベクトルを作成して保存する""" global mean @@ -241,8 +233,8 @@ def save_style_vectors_from_files( return "Error: スタイルベクトルを読み込んでください。" mean = np.mean(x, axis=0) - result_dir = os.path.join(config.assets_root, model_name) - os.makedirs(result_dir, exist_ok=True) + result_dir = assets_root / model_name + result_dir.mkdir(parents=True, exist_ok=True) audio_files = [name.strip() for name in audio_files_str.split(",")] style_names = [name.strip() for name in style_names_str.split(",")] if len(audio_files) != len(style_names): @@ -252,23 +244,23 @@ def save_style_vectors_from_files( return "スタイル名が重複しています。" style_vectors = [mean] - wavs_dir = os.path.join(dataset_root, model_name, "wavs") + wavs_dir = dataset_root / model_name / "wavs" for audio_file in audio_files: - path = os.path.join(wavs_dir, audio_file) - if not os.path.exists(path): + path = wavs_dir / audio_file + if not path.exists(): return f"{path}が存在しません。" style_vectors.append(np.load(f"{path}.npy")) style_vectors = np.stack(style_vectors) assert len(style_name_list) == len(style_vectors) - style_vector_path = os.path.join(result_dir, "style_vectors.npy") - if os.path.exists(style_vector_path): + style_vector_path = result_dir / "style_vectors.npy" + if style_vector_path.exists(): logger.info(f"Backup {style_vector_path} to {style_vector_path}.bak") shutil.copy(style_vector_path, f"{style_vector_path}.bak") np.save(style_vector_path, style_vectors) # config.jsonの更新 - config_path = os.path.join(result_dir, "config.json") - if not os.path.exists(config_path): + config_path = result_dir / "config.json" + if not config_path.exists(): return f"{config_path}が存在しません。" logger.info(f"Backup {config_path} to {config_path}.bak") shutil.copy(config_path, f"{config_path}.bak") diff --git a/gradio_tabs/train.py b/gradio_tabs/train.py index 03efdde..e3b130e 100644 --- a/gradio_tabs/train.py +++ b/gradio_tabs/train.py @@ -1,5 +1,4 @@ import json -import os import shutil import socket import subprocess @@ -12,7 +11,8 @@ from pathlib import Path import gradio as gr import yaml - +from dataclasses import dataclass +from config import get_path_config from style_bert_vits2.logging import logger from style_bert_vits2.utils.stdout_wrapper import SAFE_STDOUT from style_bert_vits2.utils.subprocess import run_script_with_log, second_elem_of @@ -21,20 +21,27 @@ from style_bert_vits2.utils.subprocess import run_script_with_log, second_elem_o logger_handler = None tensorboard_executed = False -# Get path settings -with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f: - path_config: dict[str, str] = yaml.safe_load(f.read()) - dataset_root = Path(path_config["dataset_root"]) +path_config = get_path_config() +dataset_root = path_config.dataset_root -def get_path(model_name: str) -> tuple[Path, Path, Path, Path, Path]: +@dataclass +class PathsForPreprocess: + dataset_path: Path + esd_path: Path + train_path: Path + val_path: Path + config_path: Path + + +def get_path(model_name: str) -> PathsForPreprocess: assert model_name != "", "モデル名は空にできません" dataset_path = dataset_root / model_name - lbl_path = dataset_path / "esd.list" + esd_path = dataset_path / "esd.list" train_path = dataset_path / "train.list" val_path = dataset_path / "val.list" config_path = dataset_path / "config.json" - return dataset_path, lbl_path, train_path, val_path, config_path + return PathsForPreprocess(dataset_path, esd_path, train_path, val_path, config_path) def initialize( @@ -51,14 +58,14 @@ def initialize( log_interval: int, ): global logger_handler - dataset_path, _, train_path, val_path, config_path = get_path(model_name) + paths = get_path(model_name) # 前処理のログをファイルに保存する timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") file_name = f"preprocess_{timestamp}.log" if logger_handler is not None: logger.remove(logger_handler) - logger_handler = logger.add(os.path.join(dataset_path, file_name)) + logger_handler = logger.add(paths.dataset_path / file_name) logger.info( f"Step 1: start initialization...\nmodel_name: {model_name}, batch_size: {batch_size}, epochs: {epochs}, save_every_steps: {save_every_steps}, freeze_ZH_bert: {freeze_ZH_bert}, freeze_JP_bert: {freeze_JP_bert}, freeze_EN_bert: {freeze_EN_bert}, freeze_style: {freeze_style}, freeze_decoder: {freeze_decoder}, use_jp_extra: {use_jp_extra}" @@ -71,8 +78,8 @@ def initialize( with open(default_config_path, "r", encoding="utf-8") as f: config = json.load(f) config["model_name"] = model_name - config["data"]["training_files"] = str(train_path) - config["data"]["validation_files"] = str(val_path) + config["data"]["training_files"] = str(paths.train_path) + config["data"]["validation_files"] = str(paths.val_path) config["train"]["batch_size"] = batch_size config["train"]["epochs"] = epochs config["train"]["eval_interval"] = save_every_steps @@ -89,14 +96,14 @@ def initialize( # 今はデフォルトであるが、以前は非JP-Extra版になくバグの原因になるので念のため config["data"]["use_jp_extra"] = use_jp_extra - model_path = dataset_path / "models" + model_path = paths.dataset_path / "models" if model_path.exists(): logger.warning( f"Step 1: {model_path} already exists, so copy it to backup to {model_path}_backup" ) shutil.copytree( src=model_path, - dst=dataset_path / "models_backup", + dst=paths.dataset_path / "models_backup", dirs_exist_ok=True, ) shutil.rmtree(model_path) @@ -110,14 +117,14 @@ def initialize( logger.error(f"Step 1: {pretrained_dir} folder not found.") return False, f"Step 1, Error: {pretrained_dir}フォルダが見つかりません。" - with open(config_path, "w", encoding="utf-8") as f: + with open(paths.config_path, "w", encoding="utf-8") as f: json.dump(config, f, indent=2, ensure_ascii=False) if not Path("config.yml").exists(): shutil.copy(src="default_config.yml", dst="config.yml") with open("config.yml", "r", encoding="utf-8") as f: yml_data = yaml.safe_load(f) yml_data["model_name"] = model_name - yml_data["dataset_path"] = str(dataset_path) + yml_data["dataset_path"] = str(paths.dataset_path) with open("config.yml", "w", encoding="utf-8") as f: yaml.dump(yml_data, f, allow_unicode=True) logger.success("Step 1: initialization finished.") @@ -126,7 +133,7 @@ def initialize( def resample(model_name: str, normalize: bool, trim: bool, num_processes: int): logger.info("Step 2: start resampling...") - dataset_path, _, _, _, _ = get_path(model_name) + dataset_path = get_path(model_name).dataset_path input_dir = dataset_path / "raw" output_dir = dataset_path / "wavs" cmd = [ @@ -159,21 +166,24 @@ def preprocess_text( model_name: str, use_jp_extra: bool, val_per_lang: int, yomi_error: str ): logger.info("Step 3: start preprocessing text...") - _, lbl_path, train_path, val_path, config_path = get_path(model_name) - if not lbl_path.exists(): - logger.error(f"Step 3: {lbl_path} not found.") - return False, f"Step 3, Error: 書き起こしファイル {lbl_path} が見つかりません。" + paths = get_path(model_name) + if not paths.esd_path.exists(): + logger.error(f"Step 3: {paths.esd_path} not found.") + return ( + False, + f"Step 3, Error: 書き起こしファイル {paths.esd_path} が見つかりません。", + ) cmd = [ "preprocess_text.py", "--config-path", - str(config_path), + str(paths.config_path), "--transcription-path", - str(lbl_path), + str(paths.esd_path), "--train-path", - str(train_path), + str(paths.train_path), "--val-path", - str(val_path), + str(paths.val_path), "--val-per-lang", str(val_per_lang), "--yomi_error", @@ -201,7 +211,7 @@ def preprocess_text( def bert_gen(model_name: str): logger.info("Step 4: start bert_gen...") - _, _, _, _, config_path = get_path(model_name) + config_path = get_path(model_name).config_path success, message = run_script_with_log( ["bert_gen.py", "--config", str(config_path)] ) @@ -220,7 +230,7 @@ def bert_gen(model_name: str): def style_gen(model_name: str, num_processes: int): logger.info("Step 5: start style_gen...") - _, _, _, _, config_path = get_path(model_name) + config_path = get_path(model_name).config_path success, message = run_script_with_log( [ "style_gen.py", @@ -319,17 +329,23 @@ def train( use_jp_extra: bool = True, speedup: bool = False, ): - dataset_path, _, _, _, config_path = get_path(model_name) + paths = get_path(model_name) # 学習再開の場合を考えて念のためconfig.ymlの名前等を更新 with open("config.yml", "r", encoding="utf-8") as f: yml_data = yaml.safe_load(f) yml_data["model_name"] = model_name - yml_data["dataset_path"] = str(dataset_path) + yml_data["dataset_path"] = str(paths.dataset_path) with open("config.yml", "w", encoding="utf-8") as f: yaml.dump(yml_data, f, allow_unicode=True) train_py = "train_ms.py" if not use_jp_extra else "train_ms_jp_extra.py" - cmd = [train_py, "--config", str(config_path), "--model", str(dataset_path)] + cmd = [ + train_py, + "--config", + str(paths.config_path), + "--model", + str(paths.dataset_path), + ] if skip_style: cmd.append("--skip_default_style") if speedup: diff --git a/preprocess_text.py b/preprocess_text.py index a65a1d6..99badd4 100644 --- a/preprocess_text.py +++ b/preprocess_text.py @@ -7,7 +7,7 @@ from typing import Optional from tqdm import tqdm -from config import Preprocess_text_config, config +from config import get_config from style_bert_vits2.logging import logger from style_bert_vits2.nlp import clean_text from style_bert_vits2.nlp.japanese import pyopenjtalk_worker @@ -22,7 +22,7 @@ pyopenjtalk_worker.initialize_worker() update_dict() -preprocess_text_config: Preprocess_text_config = config.preprocess_text_config +preprocess_text_config = get_config().preprocess_text_config # Count lines for tqdm diff --git a/requirements.txt b/requirements.txt index d111e69..68a4fee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ matplotlib num2words numba numpy +protobuf==4.25 psutil pyannote.audio>=3.1.0 pydantic>=2.0 diff --git a/resample.py b/resample.py index 55f0f7f..050ce9a 100644 --- a/resample.py +++ b/resample.py @@ -10,7 +10,7 @@ import soundfile from numpy.typing import NDArray from tqdm import tqdm -from config import config +from config import get_config from style_bert_vits2.logging import logger from style_bert_vits2.utils.stdout_wrapper import SAFE_STDOUT @@ -70,6 +70,7 @@ def resample( if __name__ == "__main__": + config = get_config() parser = argparse.ArgumentParser() parser.add_argument( "--sr", diff --git a/server_editor.py b/server_editor.py index c6f856e..cf70feb 100644 --- a/server_editor.py +++ b/server_editor.py @@ -22,7 +22,6 @@ import numpy as np import requests import torch import uvicorn -import yaml from fastapi import APIRouter, FastAPI, HTTPException, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse, Response @@ -30,6 +29,7 @@ from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from scipy.io import wavfile +from config import get_path_config from style_bert_vits2.constants import ( DEFAULT_ASSIST_TEXT_WEIGHT, DEFAULT_NOISE, @@ -174,22 +174,14 @@ origins = [ "http://127.0.0.1:8000", ] -# Get path settings -with open(Path("configs/paths.yml"), "r", encoding="utf-8") as f: - path_config: dict[str, str] = yaml.safe_load(f.read()) - # dataset_root = path_config["dataset_root"] - assets_root = path_config["assets_root"] - +path_config = get_path_config() parser = argparse.ArgumentParser() -parser.add_argument("--model_dir", type=str, default="model_assets/") +parser.add_argument("--model_dir", type=str, default=path_config.assets_root) parser.add_argument("--device", type=str, default="cuda") parser.add_argument("--port", type=int, default=8000) parser.add_argument("--inbrowser", action="store_true") parser.add_argument("--line_length", type=int, default=None) parser.add_argument("--line_count", type=int, default=None) -parser.add_argument( - "--dir", "-d", type=str, help="Model directory", default=assets_root -) args = parser.parse_args() device = args.device diff --git a/server_fastapi.py b/server_fastapi.py index 3c9d31e..dbae8c7 100644 --- a/server_fastapi.py +++ b/server_fastapi.py @@ -20,7 +20,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, Response from scipy.io import wavfile -from config import config +from config import get_config from style_bert_vits2.constants import ( DEFAULT_ASSIST_TEXT_WEIGHT, DEFAULT_LENGTH, @@ -40,6 +40,7 @@ from style_bert_vits2.nlp.japanese.user_dict import update_dict from style_bert_vits2.tts_model import TTSModel, TTSModelHolder +config = get_config() ln = config.server_config.language @@ -113,6 +114,9 @@ if __name__ == "__main__": load_models(model_holder) limit = config.server_config.limit + logger.info( + f"The maximum length of the text is {limit}. If you want to change it, modify config.yml" + ) app = FastAPI() allow_origins = config.server_config.origins if allow_origins: diff --git a/slice.py b/slice.py index c4b2292..bf28bf6 100644 --- a/slice.py +++ b/slice.py @@ -10,6 +10,7 @@ import torch import yaml from tqdm import tqdm +from config import get_path_config from style_bert_vits2.logging import logger from style_bert_vits2.utils.stdout_wrapper import SAFE_STDOUT @@ -150,13 +151,12 @@ if __name__ == "__main__": ) args = parser.parse_args() - with open(Path("configs/paths.yml"), "r", encoding="utf-8") as f: - path_config: dict[str, str] = yaml.safe_load(f.read()) - dataset_root = path_config["dataset_root"] + path_config = get_path_config() + dataset_root = path_config.dataset_root model_name = str(args.model_name) input_dir = Path(args.input_dir) - output_dir = Path(dataset_root) / model_name / "raw" + output_dir = dataset_root / model_name / "raw" min_sec: float = args.min_sec max_sec: float = args.max_sec min_silence_dur_ms: int = args.min_silence_dur_ms diff --git a/speech_mos.py b/speech_mos.py index 453b7d3..c7a6a25 100644 --- a/speech_mos.py +++ b/speech_mos.py @@ -10,7 +10,7 @@ import pandas as pd import torch from tqdm import tqdm -from config import config +from config import get_path_config from style_bert_vits2.logging import logger from style_bert_vits2.tts_model import TTSModel @@ -35,6 +35,8 @@ test_texts = [ "この分野の最新の研究成果を使うと、より自然で表現豊かな音声の生成が可能である。深層学習の応用により、感情やアクセントを含む声質の微妙な変化も再現することが出来る。", ] +path_config = get_path_config() + predictor = torch.hub.load( "tarepan/SpeechMOS:v1.2.0", "utmos22_strong", trust_repo=True ) @@ -48,17 +50,16 @@ args = parser.parse_args() model_name: str = args.model_name device: str = args.device -model_path = Path(config.assets_root) / model_name - +model_path = path_config.assets_root / model_name # .safetensorsファイルを検索 safetensors_files = model_path.glob("*.safetensors") def get_model(model_file: Path): return TTSModel( - model_path=str(model_file), - config_path=str(model_file.parent / "config.json"), - style_vec_path=str(model_file.parent / "style_vectors.npy"), + model_path=model_file, + config_path=model_file.parent / "config.json", + style_vec_path=model_file.parent / "style_vectors.npy", device=device, ) diff --git a/style_bert_vits2/constants.py b/style_bert_vits2/constants.py index 58b6441..b1987d8 100644 --- a/style_bert_vits2/constants.py +++ b/style_bert_vits2/constants.py @@ -4,7 +4,7 @@ from style_bert_vits2.utils.strenum import StrEnum # Style-Bert-VITS2 のバージョン -VERSION = "2.4.1" +VERSION = "2.5.0" # Style-Bert-VITS2 のベースディレクトリ BASE_DIR = Path(__file__).parent.parent diff --git a/style_gen.py b/style_gen.py index a21ab2b..2293e22 100644 --- a/style_gen.py +++ b/style_gen.py @@ -8,12 +8,14 @@ from numpy.typing import NDArray from pyannote.audio import Inference, Model from tqdm import tqdm -from config import config +from config import get_config from style_bert_vits2.logging import logger from style_bert_vits2.models.hyper_parameters import HyperParameters from style_bert_vits2.utils.stdout_wrapper import SAFE_STDOUT +config = get_config() + model = Model.from_pretrained("pyannote/wespeaker-voxceleb-resnet34-LM") inference = Inference(model, window="whole") device = torch.device(config.style_gen_config.device) diff --git a/train_ms.py b/train_ms.py index 067a6a5..152f8ed 100644 --- a/train_ms.py +++ b/train_ms.py @@ -16,7 +16,7 @@ from tqdm import tqdm # logging.getLogger("numba").setLevel(logging.WARNING) import default_style -from config import config +from config import get_config from data_utils import ( DistributedBucketSampler, TextAudioSpeakerCollate, @@ -48,7 +48,7 @@ torch.backends.cuda.enable_mem_efficient_sdp( ) # Not available if torch version is lower than 2.0 torch.backends.cuda.enable_math_sdp(True) - +config = get_config() global_step = 0 api = HfApi() diff --git a/train_ms_jp_extra.py b/train_ms_jp_extra.py index ad3d3bd..9346128 100644 --- a/train_ms_jp_extra.py +++ b/train_ms_jp_extra.py @@ -16,7 +16,7 @@ from tqdm import tqdm # logging.getLogger("numba").setLevel(logging.WARNING) import default_style -from config import config +from config import get_config from data_utils import ( DistributedBucketSampler, TextAudioSpeakerCollate, @@ -48,6 +48,8 @@ torch.backends.cuda.enable_flash_sdp(True) torch.backends.cuda.enable_mem_efficient_sdp( True ) # Not available if torch version is lower than 2.0 + +config = get_config() global_step = 0 api = HfApi() diff --git a/transcribe.py b/transcribe.py index 898fdc9..c7aa493 100644 --- a/transcribe.py +++ b/transcribe.py @@ -8,6 +8,7 @@ import yaml from torch.utils.data import Dataset from tqdm import tqdm +from config import get_path_config from style_bert_vits2.constants import Languages from style_bert_vits2.logging import logger from style_bert_vits2.utils.stdout_wrapper import SAFE_STDOUT @@ -124,9 +125,8 @@ if __name__ == "__main__": parser.add_argument("--no_repeat_ngram_size", type=int, default=10) args = parser.parse_args() - with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f: - path_config: dict[str, str] = yaml.safe_load(f.read()) - dataset_root = Path(path_config["dataset_root"]) + path_config = get_path_config() + dataset_root = path_config.dataset_root model_name = str(args.model_name)