Refactor and add merge

This commit is contained in:
litagin02
2023-12-31 14:55:04 +09:00
parent 62d360e777
commit ef4e82defc
34 changed files with 898 additions and 370 deletions

13
Merge.bat Normal file
View File

@@ -0,0 +1,13 @@
chcp 65001 > NUL
@echo off
pushd %~dp0
echo Running webui_merge.py...
venv\Scripts\python webui_merge.py
if %errorlevel% neq 0 ( pause & popd & exit /b %errorlevel% )
popd
pause

View File

@@ -44,8 +44,6 @@ Windowsを前提としています。
#### GitやPython使える人 #### GitやPython使える人
Windowsの通常環境のPython 3.10で動作確認していますが、他の環境でも動くと思います。
```bash ```bash
git clone https://github.com/litagin02/Style-Bert-VITS2.git git clone https://github.com/litagin02/Style-Bert-VITS2.git
cd Style-Bert-VITS2 cd Style-Bert-VITS2

275
app.py
View File

@@ -2,225 +2,30 @@ import argparse
import datetime import datetime
import os import os
import sys import sys
import warnings
import enum
import gradio as gr import gradio as gr
import numpy as np import numpy as np
import torch import torch
from gradio.processing_utils import convert_to_16_bit_wav from gradio.processing_utils import convert_to_16_bit_wav
from typing import Dict, List
import utils from common.constants import (
DEFAULT_ASSIST_TEXT_WEIGHT,
DEFAULT_LENGTH,
DEFAULT_LINE_SPLIT,
DEFAULT_NOISE,
DEFAULT_NOISEW,
DEFAULT_SDP_RATIO,
DEFAULT_SPLIT_INTERVAL,
DEFAULT_STYLE,
DEFAULT_STYLE_WEIGHT,
Languages,
)
from common.log import logger
from common.tts_model import ModelHolder
from config import config from config import config
from infer import get_net_g, infer
from tools.log import logger
class Languages(str, enum.Enum):
JP = "JP"
EN = "EN"
ZH = "ZH"
languages = [l.value for l in Languages] languages = [l.value for l in Languages]
DEFAULT_SDP_RATIO: float = 0.2
DEFAULT_NOISE: float = 0.6
DEFAULT_NOISEW: float = 0.8
DEFAULT_LENGTH: float = 1
DEFAULT_LINE_SPLIT: bool = True
DEFAULT_SPLIT_INTERVAL: float = 0.5
DEFAULT_STYLE_WEIGHT: float = 0.7
DEFAULT_EMOTION_WEIGHT: float = 1.0
class Model:
def __init__(self, model_path, config_path, style_vec_path, device):
self.model_path = model_path
self.config_path = config_path
self.device = device
self.style_vec_path = style_vec_path
self.hps = utils.get_hparams_from_file(self.config_path)
self.spk2id: Dict[str, int] = self.hps.data.spk2id
self.id2spk: Dict[int, str] = {v: k for k, v in self.spk2id.items()}
self.num_styles = self.hps.data.num_styles
if hasattr(self.hps.data, "style2id"):
self.style2id = self.hps.data.style2id
else:
self.style2id = {str(i): i for i in range(self.num_styles)}
self.style_vectors = np.load(self.style_vec_path)
self.net_g = None
def load_net_g(self):
self.net_g = get_net_g(
model_path=self.model_path,
version=self.hps.version,
device=self.device,
hps=self.hps,
)
def get_style_vector(self, style_id, weight=1.0):
mean = self.style_vectors[0]
style_vec = self.style_vectors[style_id]
style_vec = mean + (style_vec - mean) * weight
return style_vec
def get_style_vector_from_audio(self, audio_path, weight=1.0):
from style_gen import extract_style_vector
xvec = extract_style_vector(audio_path)
mean = self.style_vectors[0]
xvec = mean + (xvec - mean) * weight
return xvec
def infer(
self,
text,
language="JP",
sid=0,
reference_audio_path=None,
sdp_ratio=DEFAULT_SDP_RATIO,
noise=DEFAULT_NOISE,
noisew=DEFAULT_NOISEW,
length=DEFAULT_LENGTH,
line_split=DEFAULT_LINE_SPLIT,
split_interval=DEFAULT_SPLIT_INTERVAL,
style_text="",
style_weight=DEFAULT_STYLE_WEIGHT,
use_style_text=False,
style="0",
emotion_weight=DEFAULT_EMOTION_WEIGHT,
):
if reference_audio_path == "":
reference_audio_path = None
if style_text == "" or not use_style_text:
style_text = None
if self.net_g is None:
self.load_net_g()
if reference_audio_path is None:
style_id = self.style2id[style]
style_vector = self.get_style_vector(style_id, emotion_weight)
else:
style_vector = self.get_style_vector_from_audio(
reference_audio_path, emotion_weight
)
if not line_split:
with torch.no_grad():
audio = infer(
text=text,
sdp_ratio=sdp_ratio,
noise_scale=noise,
noise_scale_w=noisew,
length_scale=length,
sid=sid,
language=language,
hps=self.hps,
net_g=self.net_g,
device=self.device,
style_text=style_text,
style_weight=style_weight,
style_vec=style_vector,
)
else:
texts = text.split("\n")
texts = [t for t in texts if t != ""]
audios = []
with torch.no_grad():
for i, t in enumerate(texts):
audios.append(
infer(
text=t,
sdp_ratio=sdp_ratio,
noise_scale=noise,
noise_scale_w=noisew,
length_scale=length,
sid=sid,
language=language,
hps=self.hps,
net_g=self.net_g,
device=self.device,
style_text=style_text,
style_weight=style_weight,
style_vec=style_vector,
)
)
if i != len(texts) - 1:
audios.append(np.zeros(int(44100 * split_interval)))
audio = np.concatenate(audios)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
audio = convert_to_16_bit_wav(audio)
return (self.hps.data.sampling_rate, audio)
class ModelHolder:
def __init__(self, root_dir, device):
self.root_dir = root_dir
self.device = device
self.model_files_dict = {}
self.current_model = None
self.model_names = []
self.models = []
self.refresh()
def refresh(self):
self.model_files_dict: Dict[str, List[str]] = {}
self.model_names: List[str] = []
self.current_model = None
model_dirs = [
d
for d in os.listdir(self.root_dir)
if os.path.isdir(os.path.join(self.root_dir, d))
]
for model_name in model_dirs:
model_dir = os.path.join(self.root_dir, model_name)
model_files = [
os.path.join(model_dir, f)
for f in os.listdir(model_dir)
if f.endswith(".pth") or f.endswith(".pt") or f.endswith(".safetensors")
]
if len(model_files) == 0:
logger.info(
f"No model files found in {self.root_dir}/{model_name}, so skip it"
)
continue
self.model_files_dict[model_name] = model_files
self.model_names.append(model_name)
def load_model(self, model_name, model_path):
if model_name not in self.model_files_dict:
raise Exception(f"モデル名{model_name}は存在しません")
if model_path not in self.model_files_dict[model_name]:
raise Exception(f"pthファイル{model_path}は存在しません")
self.current_model = Model(
model_path=model_path,
config_path=os.path.join(self.root_dir, model_name, "config.json"),
style_vec_path=os.path.join(self.root_dir, model_name, "style_vectors.npy"),
device=self.device,
)
styles = list(self.current_model.style2id.keys())
return (
gr.Dropdown(choices=styles, value=styles[0]),
gr.update(interactive=True, value="音声合成"),
)
def update_model_files_dropdown(self, model_name):
model_files = self.model_files_dict[model_name]
return gr.Dropdown(choices=model_files, value=model_files[0])
def update_model_names_dropdown(self):
self.refresh()
initial_model_name = self.model_names[0]
initial_model_files = self.model_files_dict[initial_model_name]
return (
gr.Dropdown(choices=self.model_names, value=initial_model_name),
gr.Dropdown(choices=initial_model_files, value=initial_model_files[0]),
gr.update(interactive=False), # For tts_button
)
def tts_fn( def tts_fn(
text, text,
@@ -232,11 +37,11 @@ def tts_fn(
length_scale, length_scale,
line_split, line_split,
split_interval, split_interval,
style_text, assist_text,
assist_text_weight,
use_assist_text,
style,
style_weight, style_weight,
use_style_text,
emotion,
emotion_weight,
): ):
assert model_holder.current_model is not None assert model_holder.current_model is not None
@@ -252,11 +57,11 @@ def tts_fn(
length=length_scale, length=length_scale,
line_split=line_split, line_split=line_split,
split_interval=split_interval, split_interval=split_interval,
style_text=style_text, assist_text=assist_text,
assist_text_weight=assist_text_weight,
use_assist_text=use_assist_text,
style=style,
style_weight=style_weight, style_weight=style_weight,
use_style_text=use_style_text,
style=emotion,
emotion_weight=emotion_weight,
) )
end_time = datetime.datetime.now() end_time = datetime.datetime.now()
@@ -349,9 +154,9 @@ model_assets
TODO: 現在のところはspeaker_id = 0に固定しており複数話者の合成には対応していません。 TODO: 現在のところはspeaker_id = 0に固定しており複数話者の合成には対応していません。
""" """
style_md = """ style_md = f"""
- プリセットまたは音声ファイルから読み上げの声音・感情・スタイルのようなものを制御できます。 - プリセットまたは音声ファイルから読み上げの声音・感情・スタイルのようなものを制御できます。
- デフォルトのNeutralでも、十分に読み上げる文に応じた感情で感情豊かに読み上げられます。このスタイル制御は、それを重み付きで上書きするような感じです。 - デフォルトの{DEFAULT_STYLE}でも、十分に読み上げる文に応じた感情で感情豊かに読み上げられます。このスタイル制御は、それを重み付きで上書きするような感じです。
- 強さを大きくしすぎると発音が変になったり声にならなかったりと崩壊することがあります。 - 強さを大きくしすぎると発音が変になったり声にならなかったりと崩壊することがあります。
- どのくらいに強さがいいかはモデルやスタイルによって異なるようです。 - どのくらいに強さがいいかはモデルやスタイルによって異なるようです。
- 音声ファイルを入力する場合は、学習データと似た声音の話者(特に同じ性別)でないとよい効果が出ないかもしれません。 - 音声ファイルを入力する場合は、学習データと似た声音の話者(特に同じ性別)でないとよい効果が出ないかもしれません。
@@ -459,25 +264,25 @@ if __name__ == "__main__":
step=0.1, step=0.1,
label="Length", label="Length",
) )
use_style_text = gr.Checkbox(label="Style textを使う", value=False) use_assist_text = gr.Checkbox(label="Assist textを使う", value=False)
style_text = gr.Textbox( assist_text = gr.Textbox(
label="Style text", label="Assist text",
placeholder="どうして私の意見を無視するの?許せない、ムカつく!死ねばいいのに。", placeholder="どうして私の意見を無視するの?許せない、ムカつく!死ねばいいのに。",
info="このテキストの読み上げと似た声音・感情になりやすくなります。ただ抑揚やテンポ等が犠牲になる傾向があります。", info="このテキストの読み上げと似た声音・感情になりやすくなります。ただ抑揚やテンポ等が犠牲になる傾向があります。",
visible=False, visible=False,
) )
style_text_weight = gr.Slider( assist_text_weight = gr.Slider(
minimum=0, minimum=0,
maximum=1, maximum=1,
value=DEFAULT_STYLE_WEIGHT, value=DEFAULT_ASSIST_TEXT_WEIGHT,
step=0.1, step=0.1,
label="Style textの強さ", label="Style textの強さ",
visible=False, visible=False,
) )
use_style_text.change( use_assist_text.change(
lambda x: (gr.Textbox(visible=x), gr.Slider(visible=x)), lambda x: (gr.Textbox(visible=x), gr.Slider(visible=x)),
inputs=[use_style_text], inputs=[use_assist_text],
outputs=[style_text, style_text_weight], outputs=[assist_text, assist_text_weight],
) )
with gr.Column(): with gr.Column():
with gr.Accordion("スタイルについて詳細", open=False): with gr.Accordion("スタイルについて詳細", open=False):
@@ -488,14 +293,14 @@ if __name__ == "__main__":
value="プリセットから選ぶ", value="プリセットから選ぶ",
) )
style = gr.Dropdown( style = gr.Dropdown(
label="スタイル(Neutralが平均スタイル)", label="スタイル({DEFAULT_STYLE}が平均スタイル)",
choices=["モデルをロードしてください"], choices=["モデルをロードしてください"],
value="モデルをロードしてください", value="モデルをロードしてください",
) )
style_weight = gr.Slider( style_weight = gr.Slider(
minimum=0, minimum=0,
maximum=50, maximum=50,
value=DEFAULT_EMOTION_WEIGHT, value=DEFAULT_STYLE_WEIGHT,
step=0.1, step=0.1,
label="スタイルの強さ", label="スタイルの強さ",
) )
@@ -520,9 +325,9 @@ if __name__ == "__main__":
length_scale, length_scale,
line_split, line_split,
split_interval, split_interval,
style_text, assist_text,
style_text_weight, assist_text_weight,
use_style_text, use_assist_text,
style, style,
style_weight, style_weight,
], ],
@@ -530,7 +335,7 @@ if __name__ == "__main__":
) )
model_name.change( model_name.change(
model_holder.update_model_files_dropdown, model_holder.update_model_files_gr,
inputs=[model_name], inputs=[model_name],
outputs=[model_path], outputs=[model_path],
) )
@@ -538,12 +343,12 @@ if __name__ == "__main__":
model_path.change(make_non_interactive, outputs=[tts_button]) model_path.change(make_non_interactive, outputs=[tts_button])
refresh_button.click( refresh_button.click(
model_holder.update_model_names_dropdown, model_holder.update_model_names_gr,
outputs=[model_name, model_path, tts_button], outputs=[model_name, model_path, tts_button],
) )
load_button.click( load_button.click(
model_holder.load_model, model_holder.load_model_gr,
inputs=[model_name, model_path], inputs=[model_name, model_path],
outputs=[style, tts_button], outputs=[style, tts_button],
) )

View File

@@ -4,7 +4,7 @@ from torch import nn
from torch.nn import functional as F from torch.nn import functional as F
import commons import commons
from tools.log import logger as logging from common.log import logger as logging
class LayerNorm(nn.Module): class LayerNorm(nn.Module):

View File

@@ -9,7 +9,7 @@ import commons
import utils import utils
from config import config from config import config
from text import cleaned_text_to_sequence, get_bert from text import cleaned_text_to_sequence, get_bert
from tools.stdout_wrapper import SAFE_STDOUT from common.stdout_wrapper import SAFE_STDOUT
def process_line(x): def process_line(x):

20
common/constants.py Normal file
View File

@@ -0,0 +1,20 @@
import enum
DEFAULT_STYLE: str = "Neutral"
DEFAULT_STYLE_WEIGHT: float = 5.0
class Languages(str, enum.Enum):
JP = "JP"
EN = "EN"
ZH = "ZH"
DEFAULT_SDP_RATIO: float = 0.2
DEFAULT_NOISE: float = 0.6
DEFAULT_NOISEW: float = 0.8
DEFAULT_LENGTH: float = 1.0
DEFAULT_LINE_SPLIT: bool = True
DEFAULT_SPLIT_INTERVAL: float = 0.5
DEFAULT_ASSIST_TEXT_WEIGHT: float = 0.7
DEFAULT_ASSIST_TEXT_WEIGHT: float = 1.0

View File

@@ -1,34 +1,34 @@
import subprocess import subprocess
import sys import sys
from .log import logger from .log import logger
from .stdout_wrapper import SAFE_STDOUT from .stdout_wrapper import SAFE_STDOUT
python = sys.executable python = sys.executable
def run_script_with_log(cmd: list[str]) -> tuple[bool, str]: def run_script_with_log(cmd: list[str]) -> tuple[bool, str]:
logger.info(f"Running: {' '.join(cmd)}") logger.info(f"Running: {' '.join(cmd)}")
result = subprocess.run( result = subprocess.run(
[python] + cmd, [python] + cmd,
stdout=SAFE_STDOUT, # type: ignore stdout=SAFE_STDOUT, # type: ignore
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, text=True,
) )
if result.returncode != 0: if result.returncode != 0:
logger.error(f"Error: {' '.join(cmd)}") logger.error(f"Error: {' '.join(cmd)}")
print(result.stderr) print(result.stderr)
return False, result.stderr return False, result.stderr
elif result.stderr: elif result.stderr:
logger.warning(f"Warning: {' '.join(cmd)}") logger.warning(f"Warning: {' '.join(cmd)}")
print(result.stderr) print(result.stderr)
return True, result.stderr return True, result.stderr
logger.success(f"Success: {' '.join(cmd)}") logger.success(f"Success: {' '.join(cmd)}")
return True, "" return True, ""
def second_elem_of(original_function): def second_elem_of(original_function):
def inner_function(*args, **kwargs): def inner_function(*args, **kwargs):
return original_function(*args, **kwargs)[1] return original_function(*args, **kwargs)[1]
return inner_function return inner_function

219
common/tts_model.py Normal file
View File

@@ -0,0 +1,219 @@
import numpy as np
import gradio as gr
import torch
import os
import warnings
from gradio.processing_utils import convert_to_16_bit_wav
from typing import Dict, List, Optional
import utils
from infer import get_net_g, infer
from models import SynthesizerTrn
from .log import logger
from .constants import (
DEFAULT_ASSIST_TEXT_WEIGHT,
DEFAULT_LENGTH,
DEFAULT_LINE_SPLIT,
DEFAULT_NOISE,
DEFAULT_NOISEW,
DEFAULT_SDP_RATIO,
DEFAULT_SPLIT_INTERVAL,
DEFAULT_STYLE,
DEFAULT_STYLE_WEIGHT,
)
class Model:
def __init__(
self, model_path: str, config_path: str, style_vec_path: str, device: str
):
self.model_path: str = model_path
self.config_path: str = config_path
self.device: str = device
self.style_vec_path: str = style_vec_path
self.hps: utils.HParams = utils.get_hparams_from_file(self.config_path)
self.spk2id: Dict[str, int] = self.hps.data.spk2id
self.id2spk: Dict[int, str] = {v: k for k, v in self.spk2id.items()}
self.num_styles: int = self.hps.data.num_styles
if hasattr(self.hps.data, "style2id"):
self.style2id: Dict[str, int] = self.hps.data.style2id
else:
self.style2id: Dict[str, int] = {str(i): i for i in range(self.num_styles)}
self.style_vectors: np.ndarray = np.load(self.style_vec_path)
self.net_g: Optional[SynthesizerTrn] = None
def load_net_g(self):
self.net_g = get_net_g(
model_path=self.model_path,
version=self.hps.version,
device=self.device,
hps=self.hps,
)
def get_style_vector(self, style_id: int, weight: float = 1.0) -> np.ndarray:
mean = self.style_vectors[0]
style_vec = self.style_vectors[style_id]
style_vec = mean + (style_vec - mean) * weight
return style_vec
def get_style_vector_from_audio(
self, audio_path: str, weight: float = 1.0
) -> np.ndarray:
from style_gen import extract_style_vector
xvec = extract_style_vector(audio_path)
mean = self.style_vectors[0]
xvec = mean + (xvec - mean) * weight
return xvec
def infer(
self,
text: str,
language: str = "JP",
sid: int = 0,
reference_audio_path: Optional[str] = None,
sdp_ratio: float = DEFAULT_SDP_RATIO,
noise: float = DEFAULT_NOISE,
noisew: float = DEFAULT_NOISEW,
length: float = DEFAULT_LENGTH,
line_split: bool = DEFAULT_LINE_SPLIT,
split_interval: float = DEFAULT_SPLIT_INTERVAL,
assist_text: Optional[str] = None,
assist_text_weight: float = DEFAULT_ASSIST_TEXT_WEIGHT,
use_assist_text: bool = False,
style: str = DEFAULT_STYLE,
style_weight: float = DEFAULT_STYLE_WEIGHT,
) -> tuple[int, np.ndarray]:
logger.info(f"Start generating audio data from text:\n{text}")
if reference_audio_path == "":
reference_audio_path = None
if assist_text == "" or not use_assist_text:
assist_text = None
if self.net_g is None:
self.load_net_g()
if reference_audio_path is None:
style_id = self.style2id[style]
style_vector = self.get_style_vector(style_id, style_weight)
else:
style_vector = self.get_style_vector_from_audio(
reference_audio_path, style_weight
)
if not line_split:
with torch.no_grad():
audio = infer(
text=text,
sdp_ratio=sdp_ratio,
noise_scale=noise,
noise_scale_w=noisew,
length_scale=length,
sid=sid,
language=language,
hps=self.hps,
net_g=self.net_g,
device=self.device,
assist_text=assist_text,
assist_text_weight=assist_text_weight,
style_vec=style_vector,
)
else:
texts = text.split("\n")
texts = [t for t in texts if t != ""]
audios = []
with torch.no_grad():
for i, t in enumerate(texts):
audios.append(
infer(
text=t,
sdp_ratio=sdp_ratio,
noise_scale=noise,
noise_scale_w=noisew,
length_scale=length,
sid=sid,
language=language,
hps=self.hps,
net_g=self.net_g,
device=self.device,
assist_text=assist_text,
assist_text_weight=assist_text_weight,
style_vec=style_vector,
)
)
if i != len(texts) - 1:
audios.append(np.zeros(int(44100 * split_interval)))
audio = np.concatenate(audios)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
audio = convert_to_16_bit_wav(audio)
logger.info("Audio data generated successfully")
return (self.hps.data.sampling_rate, audio)
class ModelHolder:
def __init__(self, root_dir: str, device: str):
self.root_dir: str = root_dir
self.device: str = device
self.model_files_dict: Dict[str, List[str]] = {}
self.current_model: Optional[Model] = None
self.model_names: List[str] = []
self.models: List[Model] = []
self.refresh()
def refresh(self):
self.model_files_dict = {}
self.model_names = []
self.current_model = None
model_dirs = [
d
for d in os.listdir(self.root_dir)
if os.path.isdir(os.path.join(self.root_dir, d))
]
for model_name in model_dirs:
model_dir = os.path.join(self.root_dir, model_name)
model_files = [
os.path.join(model_dir, f)
for f in os.listdir(model_dir)
if f.endswith(".pth") or f.endswith(".pt") or f.endswith(".safetensors")
]
if len(model_files) == 0:
logger.info(
f"No model files found in {self.root_dir}/{model_name}, so skip it"
)
continue
self.model_files_dict[model_name] = model_files
self.model_names.append(model_name)
def load_model_gr(
self, model_name: str, model_path: str
) -> tuple[gr.Dropdown, gr.Button]:
if model_name not in self.model_files_dict:
raise Exception(f"モデル名{model_name}は存在しません")
if model_path not in self.model_files_dict[model_name]:
raise Exception(f"pthファイル{model_path}は存在しません")
self.current_model = Model(
model_path=model_path,
config_path=os.path.join(self.root_dir, model_name, "config.json"),
style_vec_path=os.path.join(self.root_dir, model_name, "style_vectors.npy"),
device=self.device,
)
styles = list(self.current_model.style2id.keys())
return (
gr.Dropdown(choices=styles, value=styles[0]),
gr.Button(interactive=True, value="音声合成"),
)
def update_model_files_gr(self, model_name: str) -> gr.Dropdown:
model_files = self.model_files_dict[model_name]
return gr.Dropdown(choices=model_files, value=model_files[0])
def update_model_names_gr(self) -> tuple[gr.Dropdown, gr.Dropdown, gr.Button]:
self.refresh()
initial_model_name = self.model_names[0]
initial_model_files = self.model_files_dict[initial_model_name]
return (
gr.Dropdown(choices=self.model_names, value=initial_model_name),
gr.Dropdown(choices=initial_model_files, value=initial_model_files[0]),
gr.Button(interactive=False), # For tts_button
)

View File

@@ -8,7 +8,7 @@ from typing import Dict, List
import yaml import yaml
from tools.log import logger from common.log import logger
class Resample_config: class Resample_config:

View File

@@ -11,7 +11,7 @@ import commons
from config import config from config import config
from mel_processing import mel_spectrogram_torch, spectrogram_torch from mel_processing import mel_spectrogram_torch, spectrogram_torch
from text import cleaned_text_to_sequence from text import cleaned_text_to_sequence
from tools.log import logger from common.log import logger
from utils import load_filepaths_and_text, load_wav_to_torch from utils import load_filepaths_and_text, load_wav_to_torch
"""Multi speaker version""" """Multi speaker version"""

View File

@@ -1,5 +1,6 @@
import os import os
from tools.log import logger from common.log import logger
from common.constants import DEFAULT_STYLE
import numpy as np import numpy as np
import json import json
@@ -9,10 +10,10 @@ def set_style_config(json_path, output_path):
with open(json_path, "r") as f: with open(json_path, "r") as f:
json_dict = json.load(f) json_dict = json.load(f)
json_dict["data"]["num_styles"] = 1 json_dict["data"]["num_styles"] = 1
json_dict["data"]["style2id"] = {"Neutral": 0} json_dict["data"]["style2id"] = {DEFAULT_STYLE: 0}
with open(output_path, "w") as f: with open(output_path, "w") as f:
json.dump(json_dict, f, indent=2) json.dump(json_dict, f, indent=2)
logger.info(f"Update style config (only Neutral style) to {output_path}") logger.info(f"Save style config (only {DEFAULT_STYLE}) to {output_path}")
def save_mean_vector(wav_dir, output_path): def save_mean_vector(wav_dir, output_path):

View File

@@ -1,11 +1,13 @@
# Changelog # Changelog
## v1.2 (2023-12-30) ## v1.2 (2023-12-31)
- グラボがないCPUユーザーで音声合成機能は使えるように - グラボがないユーザーで音声合成をサポート、`Install-Style-Bert-VITS2-CPU.bat`でインストール。
- Google colabでの学習をサポート、ートブックを追加 - Google Colabでの学習をサポート、[ノートブック](../colab.ipynb)を追加
- 前処理のリサンプリング時に音声ファイルの開始・終了部分の無音を削除するオプションを追加(デフォルトでオン) - 前処理のリサンプリング時に音声ファイルの開始・終了部分の無音を削除するオプションを追加(デフォルトでオン)
- 学習時に自動的にデフォルトスタイル Neutral を生成するように。これで学習したらそのまま音声合成を試せます。 - 学習時に自動的にデフォルトスタイル Neutral を生成するように。特にスタイル指定が必要のない方は、学習したらそのまま音声合成を試せます。これまで通りスタイルを自分で作ることもできます。
- マージ機能の新規追加: `Merge.bat`, `webui_merge.py`
- 音声合成のAPIサーバーを追加、`python server_fastapi.py`で起動します。API仕様は起動後に`/docs`にて確認ください。( @darai0512 様によるPRです、ありがとうございます
## v1.1 (2023-12-29) ## v1.1 (2023-12-29)
- TrainとDatasetのWebUIの改良・調整一括事前処理ボタン等 - TrainとDatasetのWebUIの改良・調整一括事前処理ボタン等

View File

@@ -23,13 +23,13 @@ def get_net_g(model_path: str, version: str, device: str, hps):
if model_path.endswith(".pth") or model_path.endswith(".pt"): if model_path.endswith(".pth") or model_path.endswith(".pt"):
_ = utils.load_checkpoint(model_path, net_g, None, skip_optimizer=True) _ = utils.load_checkpoint(model_path, net_g, None, skip_optimizer=True)
elif model_path.endswith(".safetensors"): elif model_path.endswith(".safetensors"):
_ = utils.load_safetensors(model_path, net_g, device) _ = utils.load_safetensors(model_path, net_g, True)
else: else:
raise ValueError(f"Unknown model format: {model_path}") raise ValueError(f"Unknown model format: {model_path}")
return net_g return net_g
def get_text(text, language_str, hps, device, style_text=None, style_weight=0.7): def get_text(text, language_str, hps, device, assist_text=None, assist_text_weight=0.7):
# 在此处实现当前版本的get_text # 在此处实现当前版本的get_text
norm_text, phone, tone, word2ph = clean_text(text, language_str) norm_text, phone, tone, word2ph = clean_text(text, language_str)
phone, tone, language = cleaned_text_to_sequence(phone, tone, language_str) phone, tone, language = cleaned_text_to_sequence(phone, tone, language_str)
@@ -42,7 +42,7 @@ def get_text(text, language_str, hps, device, style_text=None, style_weight=0.7)
word2ph[i] = word2ph[i] * 2 word2ph[i] = word2ph[i] * 2
word2ph[0] += 1 word2ph[0] += 1
bert_ori = get_bert( bert_ori = get_bert(
norm_text, word2ph, language_str, device, style_text, style_weight norm_text, word2ph, language_str, device, assist_text, assist_text_weight
) )
del word2ph del word2ph
assert bert_ori.shape[-1] == len(phone), phone assert bert_ori.shape[-1] == len(phone), phone
@@ -86,16 +86,16 @@ def infer(
device, device,
skip_start=False, skip_start=False,
skip_end=False, skip_end=False,
style_text=None, assist_text=None,
style_weight=0.7, assist_text_weight=0.7,
): ):
bert, ja_bert, en_bert, phones, tones, lang_ids = get_text( bert, ja_bert, en_bert, phones, tones, lang_ids = get_text(
text, text,
language, language,
hps, hps,
device, device,
style_text=style_text, assist_text=assist_text,
style_weight=style_weight, assist_text_weight=assist_text_weight,
) )
if skip_start: if skip_start:
phones = phones[3:] phones = phones[3:]

View File

@@ -3,7 +3,7 @@ from pathlib import Path
from huggingface_hub import hf_hub_download from huggingface_hub import hf_hub_download
from tools.log import logger from common.log import logger
def download_bert_models(): def download_bert_models():

View File

@@ -9,7 +9,7 @@ from tqdm import tqdm
from config import config from config import config
from text.cleaner import clean_text from text.cleaner import clean_text
from tools.stdout_wrapper import SAFE_STDOUT from common.stdout_wrapper import SAFE_STDOUT
preprocess_text_config = config.preprocess_text_config preprocess_text_config = config.preprocess_text_config

View File

@@ -8,8 +8,8 @@ import soundfile
from tqdm import tqdm from tqdm import tqdm
from config import config from config import config
from tools.log import logger from common.log import logger
from tools.stdout_wrapper import SAFE_STDOUT from common.stdout_wrapper import SAFE_STDOUT
def normalize_audio(data, sr): def normalize_audio(data, sr):

View File

@@ -1,40 +1,43 @@
""" """
api服务 多版本多模型 fastapi实现 API server for TTS
""" """
import argparse import argparse
from fastapi import FastAPI, Query, Request, status, HTTPException import os
from fastapi.responses import Response, FileResponse import sys
from fastapi.middleware.cors import CORSMiddleware
from io import BytesIO from io import BytesIO
from scipy.io import wavfile from typing import Dict, Optional, Union
import uvicorn
import torch
import psutil
import GPUtil
from typing import Dict, Optional, List, Union
import os, sys
from tools.log import logger
from urllib.parse import unquote from urllib.parse import unquote
from config import config
from app import ( import GPUtil
Model, import psutil
ModelHolder, import torch
Languages, import uvicorn
DEFAULT_SDP_RATIO, from fastapi import FastAPI, HTTPException, Query, Request, status
DEFAULT_NOISE, from fastapi.middleware.cors import CORSMiddleware
DEFAULT_NOISEW, from fastapi.responses import FileResponse, Response
from scipy.io import wavfile
from common.constants import (
DEFAULT_ASSIST_TEXT_WEIGHT,
DEFAULT_LENGTH, DEFAULT_LENGTH,
DEFAULT_LINE_SPLIT, DEFAULT_LINE_SPLIT,
DEFAULT_NOISE,
DEFAULT_NOISEW,
DEFAULT_SDP_RATIO,
DEFAULT_SPLIT_INTERVAL, DEFAULT_SPLIT_INTERVAL,
DEFAULT_STYLE,
DEFAULT_STYLE_WEIGHT, DEFAULT_STYLE_WEIGHT,
DEFAULT_EMOTION_WEIGHT, Languages,
) )
from webui_style_vectors import DEFAULT_EMOTION from common.log import logger
from common.tts_model import Model, ModelHolder
from config import config
ln = config.server_config.language ln = config.server_config.language
def raise_validation_error(msg: str, param: str): def raise_validation_error(msg: str, param: str):
logger.warning(f"Validation error: {msg}")
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=[dict(type="invalid_params", msg=msg, loc=["query", param])], detail=[dict(type="invalid_params", msg=msg, loc=["query", param])],
@@ -125,15 +128,15 @@ if __name__ == "__main__":
split_interval: float = Query( split_interval: float = Query(
DEFAULT_SPLIT_INTERVAL, description="分けた場合に挟む無音の長さ(秒)" DEFAULT_SPLIT_INTERVAL, description="分けた場合に挟む無音の長さ(秒)"
), ),
style_text: Optional[str] = Query( assist_text: Optional[str] = Query(
None, description="このテキストの読み上げと似た声音・感情になりやすくなる。ただし抑揚やテンポ等が犠牲になる傾向がある" None, description="このテキストの読み上げと似た声音・感情になりやすくなる。ただし抑揚やテンポ等が犠牲になる傾向がある"
), ),
style_weight: float = Query(DEFAULT_STYLE_WEIGHT, description="style_textの強さ"), assist_text_weight: float = Query(
emotion: Optional[Union[int, str]] = Query(DEFAULT_EMOTION, description="スタイル"), DEFAULT_ASSIST_TEXT_WEIGHT, description="assist_textの強さ"
emotion_weight: float = Query(DEFAULT_EMOTION_WEIGHT, description="emotionの強さ"),
reference_audio_path: Optional[str] = Query(
None, description="emotionを音声ファイルで行う"
), ),
style: Optional[Union[int, str]] = Query(DEFAULT_STYLE, description="スタイル"),
style_weight: float = Query(DEFAULT_STYLE_WEIGHT, description="スタイルの強さ"),
reference_audio_path: Optional[str] = Query(None, description="スタイルを音声ファイルで行う"),
): ):
"""Infer text to speech(テキストから感情付き音声を生成する)""" """Infer text to speech(テキストから感情付き音声を生成する)"""
logger.info( logger.info(
@@ -154,8 +157,8 @@ if __name__ == "__main__":
f"speaker_name={speaker_name} not found", "speaker_name" f"speaker_name={speaker_name} not found", "speaker_name"
) )
speaker_id = model.spk2id[speaker_name] speaker_id = model.spk2id[speaker_name]
if emotion not in model.style2id.keys(): if style not in model.style2id.keys():
raise_validation_error(f"emotion={emotion} not found", "emotion") raise_validation_error(f"style={style} not found", "style")
if encoding is not None: if encoding is not None:
text = unquote(text, encoding=encoding) text = unquote(text, encoding=encoding)
sr, audio = model.infer( sr, audio = model.infer(
@@ -169,12 +172,13 @@ if __name__ == "__main__":
length=length, length=length,
line_split=auto_split, line_split=auto_split,
split_interval=split_interval, split_interval=split_interval,
style_text=style_text, assist_text=assist_text,
assist_text_weight=assist_text_weight,
use_assist_text=bool(assist_text),
style=style,
style_weight=style_weight, style_weight=style_weight,
use_style_text=bool(style_text),
style=emotion,
emotion_weight=emotion_weight,
) )
logger.success("Audio data generated and sent successfully")
with BytesIO() as wavContent: with BytesIO() as wavContent:
wavfile.write(wavContent, sr, audio) wavfile.write(wavContent, sr, audio)
return Response(content=wavContent.getvalue(), media_type="audio/wav") return Response(content=wavContent.getvalue(), media_type="audio/wav")

74
server_test.ipynb Normal file

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@ import soundfile as sf
import torch import torch
from tqdm import tqdm from tqdm import tqdm
from tools.stdout_wrapper import SAFE_STDOUT from common.stdout_wrapper import SAFE_STDOUT
vad_model, utils = torch.hub.load( vad_model, utils = torch.hub.load(
repo_or_dir="snakers4/silero-vad", repo_or_dir="snakers4/silero-vad",

View File

@@ -8,7 +8,7 @@ from tqdm import tqdm
import utils import utils
from config import config from config import config
from tools.stdout_wrapper import SAFE_STDOUT from common.stdout_wrapper import SAFE_STDOUT
warnings.filterwarnings("ignore", category=UserWarning) warnings.filterwarnings("ignore", category=UserWarning)
from pyannote.audio import Inference, Model from pyannote.audio import Inference, Model

View File

@@ -18,13 +18,15 @@ def cleaned_text_to_sequence(cleaned_text, tones, language):
return phones, tones, lang_ids return phones, tones, lang_ids
def get_bert(norm_text, word2ph, language, device, style_text=None, style_weight=0.7): def get_bert(
norm_text, word2ph, language, device, assist_text=None, assist_text_weight=0.7
):
from .chinese_bert import get_bert_feature as zh_bert from .chinese_bert import get_bert_feature as zh_bert
from .english_bert_mock import get_bert_feature as en_bert from .english_bert_mock import get_bert_feature as en_bert
from .japanese_bert import get_bert_feature as jp_bert from .japanese_bert import get_bert_feature as jp_bert
lang_bert_func_map = {"ZH": zh_bert, "EN": en_bert, "JP": jp_bert} lang_bert_func_map = {"ZH": zh_bert, "EN": en_bert, "JP": jp_bert}
bert = lang_bert_func_map[language]( bert = lang_bert_func_map[language](
norm_text, word2ph, device, style_text, style_weight norm_text, word2ph, device, assist_text, assist_text_weight
) )
return bert return bert

View File

@@ -16,8 +16,8 @@ def get_bert_feature(
text, text,
word2ph, word2ph,
device=config.bert_gen_config.device, device=config.bert_gen_config.device,
style_text=None, assist_text=None,
style_weight=0.7, assist_text_weight=0.7,
): ):
if ( if (
sys.platform == "darwin" sys.platform == "darwin"
@@ -37,8 +37,8 @@ def get_bert_feature(
inputs[i] = inputs[i].to(device) inputs[i] = inputs[i].to(device)
res = models[device](**inputs, output_hidden_states=True) res = models[device](**inputs, output_hidden_states=True)
res = torch.cat(res["hidden_states"][-3:-2], -1)[0].cpu() res = torch.cat(res["hidden_states"][-3:-2], -1)[0].cpu()
if style_text: if assist_text:
style_inputs = tokenizer(style_text, return_tensors="pt") style_inputs = tokenizer(assist_text, return_tensors="pt")
for i in style_inputs: for i in style_inputs:
style_inputs[i] = style_inputs[i].to(device) style_inputs[i] = style_inputs[i].to(device)
style_res = models[device](**style_inputs, output_hidden_states=True) style_res = models[device](**style_inputs, output_hidden_states=True)
@@ -48,10 +48,10 @@ def get_bert_feature(
word2phone = word2ph word2phone = word2ph
phone_level_feature = [] phone_level_feature = []
for i in range(len(word2phone)): for i in range(len(word2phone)):
if style_text: if assist_text:
repeat_feature = ( repeat_feature = (
res[i].repeat(word2phone[i], 1) * (1 - style_weight) res[i].repeat(word2phone[i], 1) * (1 - assist_text_weight)
+ style_res_mean.repeat(word2phone[i], 1) * style_weight + style_res_mean.repeat(word2phone[i], 1) * assist_text_weight
) )
else: else:
repeat_feature = res[i].repeat(word2phone[i], 1) repeat_feature = res[i].repeat(word2phone[i], 1)

View File

@@ -17,8 +17,8 @@ def get_bert_feature(
text, text,
word2ph, word2ph,
device=config.bert_gen_config.device, device=config.bert_gen_config.device,
style_text=None, assist_text=None,
style_weight=0.7, assist_text_weight=0.7,
): ):
if ( if (
sys.platform == "darwin" sys.platform == "darwin"
@@ -38,8 +38,8 @@ def get_bert_feature(
inputs[i] = inputs[i].to(device) inputs[i] = inputs[i].to(device)
res = models[device](**inputs, output_hidden_states=True) res = models[device](**inputs, output_hidden_states=True)
res = torch.cat(res["hidden_states"][-3:-2], -1)[0].cpu() res = torch.cat(res["hidden_states"][-3:-2], -1)[0].cpu()
if style_text: if assist_text:
style_inputs = tokenizer(style_text, return_tensors="pt") style_inputs = tokenizer(assist_text, return_tensors="pt")
for i in style_inputs: for i in style_inputs:
style_inputs[i] = style_inputs[i].to(device) style_inputs[i] = style_inputs[i].to(device)
style_res = models[device](**style_inputs, output_hidden_states=True) style_res = models[device](**style_inputs, output_hidden_states=True)
@@ -49,10 +49,10 @@ def get_bert_feature(
word2phone = word2ph word2phone = word2ph
phone_level_feature = [] phone_level_feature = []
for i in range(len(word2phone)): for i in range(len(word2phone)):
if style_text: if assist_text:
repeat_feature = ( repeat_feature = (
res[i].repeat(word2phone[i], 1) * (1 - style_weight) res[i].repeat(word2phone[i], 1) * (1 - assist_text_weight)
+ style_res_mean.repeat(word2phone[i], 1) * style_weight + style_res_mean.repeat(word2phone[i], 1) * assist_text_weight
) )
else: else:
repeat_feature = res[i].repeat(word2phone[i], 1) repeat_feature = res[i].repeat(word2phone[i], 1)

View File

@@ -17,12 +17,12 @@ def get_bert_feature(
text, text,
word2ph, word2ph,
device=config.bert_gen_config.device, device=config.bert_gen_config.device,
style_text=None, assist_text=None,
style_weight=0.7, assist_text_weight=0.7,
): ):
text = "".join(text2sep_kata(text)[0]) text = "".join(text2sep_kata(text)[0])
if style_text: if assist_text:
style_text = "".join(text2sep_kata(style_text)[0]) assist_text = "".join(text2sep_kata(assist_text)[0])
if ( if (
sys.platform == "darwin" sys.platform == "darwin"
and torch.backends.mps.is_available() and torch.backends.mps.is_available()
@@ -41,8 +41,8 @@ def get_bert_feature(
inputs[i] = inputs[i].to(device) inputs[i] = inputs[i].to(device)
res = models[device](**inputs, output_hidden_states=True) res = models[device](**inputs, output_hidden_states=True)
res = torch.cat(res["hidden_states"][-3:-2], -1)[0].cpu() res = torch.cat(res["hidden_states"][-3:-2], -1)[0].cpu()
if style_text: if assist_text:
style_inputs = tokenizer(style_text, return_tensors="pt") style_inputs = tokenizer(assist_text, return_tensors="pt")
for i in style_inputs: for i in style_inputs:
style_inputs[i] = style_inputs[i].to(device) style_inputs[i] = style_inputs[i].to(device)
style_res = models[device](**style_inputs, output_hidden_states=True) style_res = models[device](**style_inputs, output_hidden_states=True)
@@ -53,10 +53,10 @@ def get_bert_feature(
word2phone = word2ph word2phone = word2ph
phone_level_feature = [] phone_level_feature = []
for i in range(len(word2phone)): for i in range(len(word2phone)):
if style_text: if assist_text:
repeat_feature = ( repeat_feature = (
res[i].repeat(word2phone[i], 1) * (1 - style_weight) res[i].repeat(word2phone[i], 1) * (1 - assist_text_weight)
+ style_res_mean.repeat(word2phone[i], 1) * style_weight + style_res_mean.repeat(word2phone[i], 1) * assist_text_weight
) )
else: else:
repeat_feature = res[i].repeat(word2phone[i], 1) repeat_feature = res[i].repeat(word2phone[i], 1)

View File

@@ -29,8 +29,8 @@ from losses import discriminator_loss, feature_loss, generator_loss, kl_loss
from mel_processing import mel_spectrogram_torch, spec_to_mel_torch from mel_processing import mel_spectrogram_torch, spec_to_mel_torch
from models import DurationDiscriminator, MultiPeriodDiscriminator, SynthesizerTrn from models import DurationDiscriminator, MultiPeriodDiscriminator, SynthesizerTrn
from text.symbols import symbols from text.symbols import symbols
from tools.log import logger from common.log import logger
from tools.stdout_wrapper import SAFE_STDOUT from common.stdout_wrapper import SAFE_STDOUT
torch.backends.cuda.matmul.allow_tf32 = True torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = ( torch.backends.cudnn.allow_tf32 = (
@@ -157,6 +157,7 @@ def run():
if rank == 0: if rank == 0:
# logger = utils.get_logger(hps.model_dir) # logger = utils.get_logger(hps.model_dir)
# logger.info(hps) # logger.info(hps)
logger.add(os.path.join(hps.model_dir, "train.log"))
utils.check_git_hash(hps.model_dir) utils.check_git_hash(hps.model_dir)
writer = SummaryWriter(log_dir=hps.model_dir) writer = SummaryWriter(log_dir=hps.model_dir)
writer_eval = SummaryWriter(log_dir=os.path.join(hps.model_dir, "eval")) writer_eval = SummaryWriter(log_dir=os.path.join(hps.model_dir, "eval"))

View File

@@ -5,7 +5,7 @@ import sys
from faster_whisper import WhisperModel from faster_whisper import WhisperModel
from tqdm import tqdm from tqdm import tqdm
from tools.stdout_wrapper import SAFE_STDOUT from common.stdout_wrapper import SAFE_STDOUT
def transcribe(wav_path, initial_prompt=None): def transcribe(wav_path, initial_prompt=None):

View File

@@ -13,7 +13,7 @@ from safetensors import safe_open
from safetensors.torch import save_file from safetensors.torch import save_file
from scipy.io.wavfile import read from scipy.io.wavfile import read
from tools.log import logger from common.log import logger
MATPLOTLIB_FLAG = False MATPLOTLIB_FLAG = False

View File

@@ -71,8 +71,8 @@ def generate_audio(
device=device, device=device,
skip_start=skip_start, skip_start=skip_start,
skip_end=skip_end, skip_end=skip_end,
style_text=style_text, assist_text=style_text,
style_weight=style_weight, assist_text_weight=style_weight,
) )
audio16bit = gr.processing_utils.convert_to_16_bit_wav(audio) audio16bit = gr.processing_utils.convert_to_16_bit_wav(audio)
audio_list.append(audio16bit) audio_list.append(audio16bit)

View File

@@ -2,8 +2,8 @@ import os
import gradio as gr import gradio as gr
from tools.log import logger from common.log import logger
from tools.subprocess_utils import run_script_with_log, second_elem_of from common.subprocess_utils import run_script_with_log, second_elem_of
def do_slice(model_name, normalize): def do_slice(model_name, normalize):

389
webui_merge.py Normal file
View File

@@ -0,0 +1,389 @@
import json
import os
import sys
import gradio as gr
import numpy as np
import torch
from safetensors import safe_open
from safetensors.torch import save_file
from config import config
from common.tts_model import Model, ModelHolder
from common.log import logger
from common.constants import DEFAULT_STYLE
voice_keys = ["dec", "flow"]
speech_style_keys = ["enc_p"]
tempo_keys = ["sdp", "dp"]
device = "cuda" if torch.cuda.is_available() else "cpu"
model_dir = config.out_dir
model_holder = ModelHolder(model_dir, device)
def merge_style(model_name_a, model_name_b, weight, output_name, style_triple_list):
"""
style_triple_list: list[(model_aでのスタイル名, model_bでのスタイル名, 出力するスタイル名)]
"""
# 新スタイル名リストにNeutralが含まれているか確認し、Neutralを先頭に持ってくる
if any(triple[2] == DEFAULT_STYLE for triple in style_triple_list):
# 存在する場合、リストをソート
sorted_list = sorted(style_triple_list, key=lambda x: x[2] != DEFAULT_STYLE)
else:
# 存在しない場合、エラーを発生
raise ValueError("No element with {DEFAULT_STYLE} output style name found.")
style_vectors_a = np.load(
os.path.join(model_dir, model_name_a, "style_vectors.npy")
) # (style_num_a, 256)
style_vectors_b = np.load(
os.path.join(model_dir, model_name_b, "style_vectors.npy")
) # (style_num_b, 256)
with open(os.path.join(model_dir, model_name_a, "config.json")) as f:
config_a = json.load(f)
with open(os.path.join(model_dir, model_name_b, "config.json")) as f:
config_b = json.load(f)
style2id_a = config_a["data"]["style2id"]
style2id_b = config_b["data"]["style2id"]
new_style_vecs = []
new_style2id = {}
for style_a, style_b, style_out in sorted_list:
if style_a not in style2id_a:
logger.error(f"{style_a} is not in {model_name_a}.")
raise ValueError(f"{style_a}{model_name_a} にありません。")
if style_b not in style2id_b:
logger.error(f"{style_b} is not in {model_name_b}.")
raise ValueError(f"{style_b}{model_name_b} にありません。")
new_style = (
style_vectors_a[style2id_a[style_a]] * (1 - weight)
+ style_vectors_b[style2id_b[style_b]] * weight
)
new_style_vecs.append(new_style)
new_style2id[style_out] = len(new_style_vecs) - 1
new_style_vecs = np.array(new_style_vecs)
output_style_path = os.path.join(model_dir, 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(model_dir, output_name, "config.json"), "w") as f:
json.dump(new_config, f, indent=2)
return output_style_path, list(new_style2id.keys())
def merge_models(
model_path_a,
model_path_b,
voice_weight,
speech_style_weight,
tempo_weight,
output_name,
):
"""model Aを起点に、model Bの各要素を重み付けしてマージする。
safetensors形式を前提とする。"""
model_a_weight = {}
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 = {}
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)
merged_model_weight = model_a_weight.copy()
for key in model_a_weight.keys():
if any([key.startswith(prefix) for prefix in voice_keys]):
weight = voice_weight
elif any([key.startswith(prefix) for prefix in speech_style_keys]):
weight = speech_style_weight
elif any([key.startswith(prefix) for prefix in tempo_keys]):
weight = tempo_weight
else:
continue
merged_model_weight[key] = (
model_a_weight[key] * (1 - weight) + model_b_weight[key] * weight
)
merged_model_path = os.path.join(
model_dir, output_name, f"{output_name}.safetensors"
)
os.makedirs(os.path.dirname(merged_model_path), exist_ok=True)
save_file(merged_model_weight, merged_model_path)
return merged_model_path
def merge_models_gr(
model_name_a,
model_path_a,
model_name_b,
model_path_b,
output_name,
voice_weight,
speech_style_weight,
tempo_weight,
):
merged_model_path = merge_models(
model_path_a,
model_path_b,
voice_weight,
speech_style_weight,
tempo_weight,
output_name,
)
return f"Success: モデルを{merged_model_path}に保存しました。"
def merge_style_gr(
model_name_a,
model_name_b,
weight,
output_name,
style_triple_list_str: str,
):
style_triple_list = []
for line in style_triple_list_str.split("\n"):
if not line:
continue
style_triple = line.split(",")
if len(style_triple) != 3:
logger.error(f"Invalid style triple: {line}")
return f"Error: スタイルを3つのカンマ区切りで入力してください:\n{line}", None
style_a, style_b, style_out = style_triple
style_a = style_a.strip()
style_b = style_b.strip()
style_out = style_out.strip()
style_triple_list.append((style_a, style_b, style_out))
try:
new_style_path, new_styles = merge_style(
model_name_a, model_name_b, weight, output_name, style_triple_list
)
except ValueError as e:
return f"Error: {e}"
return f"Success: スタイルを{new_style_path}に保存しました。", gr.Dropdown(
choices=new_styles, value=new_styles[0]
)
def simple_tts(model_name, text, style=DEFAULT_STYLE, emotion_weight=1.0):
model_path = os.path.join(model_dir, model_name, f"{model_name}.safetensors")
config_path = os.path.join(model_dir, model_name, "config.json")
style_vec_path = os.path.join(model_dir, model_name, "style_vectors.npy")
model = Model(model_path, config_path, style_vec_path, device)
return model.infer(text, style=style, style_weight=emotion_weight)
def update_two_model_names_dropdown():
new_names, new_files, _ = model_holder.update_model_names_gr()
return new_names, new_files, new_names, new_files
def get_styles_gr(model_name_a, model_name_b):
config_path_a = os.path.join(model_dir, model_name_a, "config.json")
with open(config_path_a) as f:
config_a = json.load(f)
styles_a = list(config_a["data"]["style2id"].keys())
config_path_b = os.path.join(model_dir, model_name_b, "config.json")
with open(config_path_b) as f:
config_b = json.load(f)
styles_b = list(config_b["data"]["style2id"].keys())
return gr.Textbox(value=", ".join(styles_a)), gr.Textbox(value=", ".join(styles_b))
initial_md = """
# Style-Bert-VITS2 モデルマージツール
2つのStyle-Bert-VITS2モデルから、声質・話し方・話す速さを取り替えたり混ぜたりできます。
## 使い方
1. マージしたい2つのモデルを選択してください`model_assets`フォルダの中から選ばれます)。
2. マージ後のモデルの名前を入力してください。
3. マージ後のモデルの声質・話し方・話す速さを調整してください。
4. 「モデルファイルのマージ」ボタンを押してくださいsafetensorsファイルがマージされる
5. スタイルベクトルファイルも生成する必要があるので、指示に従ってマージ方法を入力後、「スタイルのマージ」ボタンを押してください。
以上でマージは完了で、`model_assets/マージ後のモデル名`にマージ後のモデルが保存され、音声合成のときに使えます。
一番下にマージしたモデルによる簡易的な音声合成機能もつけています。
"""
style_merge_md = f"""
## スタイルベクトルのマージ
1行に「モデルAのスタイル名, モデルBのスタイル名, 左の2つを混ぜて出力するスタイル名」
という形式で入力してください。例えば、
```
{DEFAULT_STYLE}, {DEFAULT_STYLE}, {DEFAULT_STYLE}
Happy, Surprise, HappySurprise
```
と入力すると、マージ後のスタイルベクトルは、
- `{DEFAULT_STYLE}`: モデルAの`{DEFAULT_STYLE}`とモデルBの`{DEFAULT_STYLE}`を混ぜたもの
- `HappySurprise`: モデルAの`Happy`とモデルBの`Surprise`を混ぜたもの
の2つになります。
### 注意
- 必ず「{DEFAULT_STYLE}」という名前のスタイルを作ってください。これは、マージ後のモデルの平均スタイルになります。
- 構造上の相性の関係で、スタイルベクトルを混ぜる重みは、上の「話し方」と同じ比率で混ぜられます。例えば「話し方」が0のときはモデルAのみしか使われません。
"""
model_names = model_holder.model_names
if len(model_names) == 0:
logger.error(f"モデルが見つかりませんでした。{model_dir}にモデルを置いてください。")
sys.exit(1)
initial_id = 0
initial_model_files = model_holder.model_files_dict[model_names[initial_id]]
with gr.Blocks(theme="NoCrypt/miku") as app:
gr.Markdown(initial_md)
with gr.Accordion(label="使い方", open=False):
gr.Markdown(initial_md)
with gr.Row():
with gr.Column(scale=3):
model_name_a = gr.Dropdown(
label="モデルA",
choices=model_names,
value=model_names[initial_id],
)
model_path_a = gr.Dropdown(
label="モデルファイル",
choices=initial_model_files,
value=initial_model_files[0],
)
with gr.Column(scale=3):
model_name_b = gr.Dropdown(
label="モデルB",
choices=model_names,
value=model_names[initial_id],
)
model_path_b = gr.Dropdown(
label="モデルファイル",
choices=initial_model_files,
value=initial_model_files[0],
)
refresh_button = gr.Button("更新", scale=1, visible=True)
with gr.Column(variant="panel"):
new_name = gr.Textbox(label="新しいモデル名", placeholder="new_model")
with gr.Row():
voice_slider = gr.Slider(
label="声質",
value=0,
minimum=0,
maximum=1,
step=0.1,
)
speech_style_slider = gr.Slider(
label="話し方(抑揚・感情表現等)",
value=0,
minimum=0,
maximum=1,
step=0.1,
)
tempo_slider = gr.Slider(
label="話す速さ・リズム・テンポ",
value=0,
minimum=0,
maximum=1,
step=0.1,
)
with gr.Column(variant="panel"):
gr.Markdown("## モデルファイルsafetensorsのマージ")
model_merge_button = gr.Button("モデルファイルのマージ", variant="primary")
info_model_merge = gr.Textbox(label="情報")
with gr.Column(variant="panel"):
gr.Markdown(style_merge_md)
with gr.Row():
load_button = gr.Button("スタイル一覧をロード", scale=1)
styles_a = gr.Textbox(label="モデルAのスタイル一覧")
styles_b = gr.Textbox(label="モデルBのスタイル一覧")
style_triple_list = gr.TextArea(
label="スタイルのマージリスト",
placeholder=f"{DEFAULT_STYLE}, {DEFAULT_STYLE},{DEFAULT_STYLE}\nAngry, Angry, Angry",
value=f"{DEFAULT_STYLE}, {DEFAULT_STYLE}, {DEFAULT_STYLE}",
)
style_merge_button = gr.Button("スタイルのマージ", variant="primary")
info_style_merge = gr.Textbox(label="情報")
text_input = gr.TextArea(label="テキスト", value="これはテストです。聞こえていますか?")
style = gr.Dropdown(
label="スタイル",
choices=["スタイルをマージしてください"],
value="スタイルをマージしてください",
)
emotion_weight = gr.Slider(
minimum=0,
maximum=50,
value=1,
step=0.1,
label="スタイルの強さ",
)
tts_button = gr.Button("音声合成", variant="primary")
audio_output = gr.Audio(label="結果")
model_name_a.change(
model_holder.update_model_files_gr,
inputs=[model_name_a],
outputs=[model_path_a],
)
model_name_b.change(
model_holder.update_model_files_gr,
inputs=[model_name_b],
outputs=[model_path_b],
)
refresh_button.click(
update_two_model_names_dropdown,
outputs=[model_name_a, model_path_a, model_name_b, model_path_b],
)
load_button.click(
get_styles_gr,
inputs=[model_name_a, model_name_b],
outputs=[styles_a, styles_b],
)
model_merge_button.click(
merge_models_gr,
inputs=[
model_name_a,
model_path_a,
model_name_b,
model_path_b,
new_name,
voice_slider,
speech_style_slider,
tempo_slider,
],
outputs=[info_model_merge],
)
style_merge_button.click(
merge_style_gr,
inputs=[
model_name_a,
model_name_b,
speech_style_slider,
new_name,
style_triple_list,
],
outputs=[info_style_merge, style],
)
tts_button.click(
simple_tts,
inputs=[new_name, text_input, style, emotion_weight],
outputs=[audio_output],
)
app.launch(inbrowser=True)

View File

@@ -7,9 +7,9 @@ import numpy as np
from sklearn.cluster import AgglomerativeClustering, KMeans from sklearn.cluster import AgglomerativeClustering, KMeans
from sklearn.manifold import TSNE from sklearn.manifold import TSNE
from config import config from config import config
from common.constants import DEFAULT_STYLE
MAX_CLUSTER_NUM = 10 MAX_CLUSTER_NUM = 10
DEFAULT_EMOTION: str = "Neutral"
tsne = TSNE(n_components=2, random_state=42, metric="cosine") tsne = TSNE(n_components=2, random_state=42, metric="cosine")
@@ -125,7 +125,7 @@ def save_style_vectors(model_name, style_names: str):
config_path = os.path.join(result_dir, "config.json") config_path = os.path.join(result_dir, "config.json")
if not os.path.exists(config_path): if not os.path.exists(config_path):
return f"{config_path}が存在しません。" return f"{config_path}が存在しません。"
style_name_list = [DEFAULT_EMOTION] style_name_list = [DEFAULT_STYLE]
style_name_list = style_name_list + style_names.split(",") style_name_list = style_name_list + style_names.split(",")
if len(style_name_list) != len(centroids) + 1: if len(style_name_list) != len(centroids) + 1:
return f"スタイルの数が合いません。`,`で正しく{len(centroids)}個に区切られているか確認してください: {style_names}" return f"スタイルの数が合いません。`,`で正しく{len(centroids)}個に区切られているか確認してください: {style_names}"
@@ -174,7 +174,7 @@ def save_style_vectors_from_files(model_name, audio_files_text, style_names_text
config_path = os.path.join(result_dir, "config.json") config_path = os.path.join(result_dir, "config.json")
if not os.path.exists(config_path): if not os.path.exists(config_path):
return f"{config_path}が存在しません。" return f"{config_path}が存在しません。"
style_name_list = [DEFAULT_EMOTION] style_name_list = [DEFAULT_STYLE]
style_name_list = style_name_list + style_names style_name_list = style_name_list + style_names
assert len(style_name_list) == len(style_vectors) assert len(style_name_list) == len(style_vectors)
@@ -194,7 +194,7 @@ initial_md = f"""
Style-Bert-VITS2でこまかくスタイルを指定して音声合成するには、モデルごとにスタイルベクトルのファイル`style_vectors.npy`を手動で作成する必要があります。 Style-Bert-VITS2でこまかくスタイルを指定して音声合成するには、モデルごとにスタイルベクトルのファイル`style_vectors.npy`を手動で作成する必要があります。
ただし、学習の過程で自動的に平均スタイル「Neutral」のみは作成されるので、それをそのまま使うこともできますその場合はこのWebUIは使いません ただし、学習の過程で自動的に平均スタイル「{DEFAULT_STYLE}」のみは作成されるので、それをそのまま使うこともできますその場合はこのWebUIは使いません
このプロセスは学習とは全く関係がないので、何回でも独立して繰り返して試せます。また学習中にもたぶん軽いので動くはずです。 このプロセスは学習とは全く関係がないので、何回でも独立して繰り返して試せます。また学習中にもたぶん軽いので動くはずです。
@@ -275,7 +275,7 @@ with gr.Blocks(theme="NoCrypt/miku") as app:
style_names = gr.Textbox( style_names = gr.Textbox(
"Angry, Sad, Happy", "Angry, Sad, Happy",
label="スタイルの名前", label="スタイルの名前",
info=f"スタイルの名前を`,`で区切って入力してください(日本語可)。例: `Angry, Sad, Happy`や`怒り, 悲しみ, 喜び`など。平均音声は{DEFAULT_EMOTION}として自動的に保存されます。", info=f"スタイルの名前を`,`で区切って入力してください(日本語可)。例: `Angry, Sad, Happy`や`怒り, 悲しみ, 喜び`など。平均音声は{DEFAULT_STYLE}として自動的に保存されます。",
) )
with gr.Row(): with gr.Row():
save_button = gr.Button("スタイルベクトルを保存", variant="primary") save_button = gr.Button("スタイルベクトルを保存", variant="primary")
@@ -288,7 +288,7 @@ with gr.Blocks(theme="NoCrypt/miku") as app:
gr.Markdown("下のテキスト欄に、各スタイルの代表音声のファイル名を`,`区切りで、その横に対応するスタイル名を`,`区切りで入力してください。") gr.Markdown("下のテキスト欄に、各スタイルの代表音声のファイル名を`,`区切りで、その横に対応するスタイル名を`,`区切りで入力してください。")
gr.Markdown("例: `angry.wav, sad.wav, happy.wav`と`Angry, Sad, Happy`") gr.Markdown("例: `angry.wav, sad.wav, happy.wav`と`Angry, Sad, Happy`")
gr.Markdown( gr.Markdown(
f"注意: {DEFAULT_EMOTION}スタイルは自動的に保存されます、手動では{DEFAULT_EMOTION}という名前のスタイルは指定しないでください。" f"注意: {DEFAULT_STYLE}スタイルは自動的に保存されます、手動では{DEFAULT_STYLE}という名前のスタイルは指定しないでください。"
) )
with gr.Row(): with gr.Row():
audio_files_text = gr.Textbox( audio_files_text = gr.Textbox(

View File

@@ -7,8 +7,8 @@ from multiprocessing import cpu_count
import gradio as gr import gradio as gr
import yaml import yaml
from tools.log import logger from common.log import logger
from tools.subprocess_utils import run_script_with_log, second_elem_of from common.subprocess_utils import run_script_with_log, second_elem_of
try: try:
import google.colab import google.colab