Refactor and add merge
This commit is contained in:
13
Merge.bat
Normal file
13
Merge.bat
Normal 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
|
||||||
@@ -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
275
app.py
@@ -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],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
20
common/constants.py
Normal 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
|
||||||
219
common/tts_model.py
Normal file
219
common/tts_model.py
Normal 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
|
||||||
|
)
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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の改良・調整(一括事前処理ボタン等)
|
||||||
|
|||||||
14
infer.py
14
infer.py
@@ -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:]
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
74
server_test.ipynb
Normal file
File diff suppressed because one or more lines are too long
2
slice.py
2
slice.py
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
2
utils.py
2
utils.py
@@ -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
|
||||||
|
|
||||||
|
|||||||
4
webui.py
4
webui.py
@@ -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)
|
||||||
|
|||||||
@@ -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
389
webui_merge.py
Normal 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)
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user