Merge branch 'dev' of github.com:litagin02/Style-Bert-VITS2

This commit is contained in:
tsukumi
2024-03-08 05:49:52 +00:00
19 changed files with 1411 additions and 1070 deletions

509
app.py
View File

@@ -1,504 +1,51 @@
import argparse
import datetime
import json
import os
import sys
from pathlib import Path
from typing import Optional
import gradio as gr
import torch
import yaml
from style_bert_vits2.constants import GRADIO_THEME, VERSION
from common.tts_model import ModelHolder
from style_bert_vits2.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,
GRADIO_THEME,
VERSION,
Languages,
from webui import (
create_dataset_app,
create_inference_app,
create_merge_app,
create_style_vectors_app,
create_train_app,
)
from style_bert_vits2.logging import logger
from style_bert_vits2.models.infer import InvalidToneError
from style_bert_vits2.text_processing.japanese import normalize_text
from style_bert_vits2.text_processing.japanese.g2p_utils import g2kata_tone, kata_tone2phone_tone
# Get path settings
with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f:
with Path("configs/paths.yml").open("r", encoding="utf-8") as f:
path_config: dict[str, str] = yaml.safe_load(f.read())
# dataset_root = path_config["dataset_root"]
assets_root = path_config["assets_root"]
languages = [l.value for l in Languages]
def tts_fn(
model_name,
model_path,
text,
language,
reference_audio_path,
sdp_ratio,
noise_scale,
noise_scale_w,
length_scale,
line_split,
split_interval,
assist_text,
assist_text_weight,
use_assist_text,
style,
style_weight,
kata_tone_json_str,
use_tone,
speaker,
pitch_scale,
intonation_scale,
):
model_holder.load_model_gr(model_name, model_path)
wrong_tone_message = ""
kata_tone: Optional[list[tuple[str, int]]] = None
if use_tone and kata_tone_json_str != "":
if language != "JP":
logger.warning("Only Japanese is supported for tone generation.")
wrong_tone_message = "アクセント指定は現在日本語のみ対応しています。"
if line_split:
logger.warning("Tone generation is not supported for line split.")
wrong_tone_message = (
"アクセント指定は改行で分けて生成を使わない場合のみ対応しています。"
)
try:
kata_tone = []
json_data = json.loads(kata_tone_json_str)
# tupleを使うように変換
for kana, tone in json_data:
assert isinstance(kana, str) and tone in (0, 1), f"{kana}, {tone}"
kata_tone.append((kana, tone))
except Exception as e:
logger.warning(f"Error occurred when parsing kana_tone_json: {e}")
wrong_tone_message = f"アクセント指定が不正です: {e}"
kata_tone = None
# toneは実際に音声合成に代入される際のみnot Noneになる
tone: Optional[list[int]] = None
if kata_tone is not None:
phone_tone = kata_tone2phone_tone(kata_tone)
tone = [t for _, t in phone_tone]
speaker_id = model_holder.current_model.spk2id[speaker]
start_time = datetime.datetime.now()
assert model_holder.current_model is not None
try:
sr, audio = model_holder.current_model.infer(
text=text,
language=language,
reference_audio_path=reference_audio_path,
sdp_ratio=sdp_ratio,
noise=noise_scale,
noisew=noise_scale_w,
length=length_scale,
line_split=line_split,
split_interval=split_interval,
assist_text=assist_text,
assist_text_weight=assist_text_weight,
use_assist_text=use_assist_text,
style=style,
style_weight=style_weight,
given_tone=tone,
sid=speaker_id,
pitch_scale=pitch_scale,
intonation_scale=intonation_scale,
)
except InvalidToneError as e:
logger.error(f"Tone error: {e}")
return f"Error: アクセント指定が不正です:\n{e}", None, kata_tone_json_str
except ValueError as e:
logger.error(f"Value error: {e}")
return f"Error: {e}", None, kata_tone_json_str
end_time = datetime.datetime.now()
duration = (end_time - start_time).total_seconds()
if tone is None and language == "JP":
# アクセント指定に使えるようにアクセント情報を返す
norm_text = normalize_text(text)
kata_tone = g2kata_tone(norm_text)
kata_tone_json_str = json.dumps(kata_tone, ensure_ascii=False)
elif tone is None:
kata_tone_json_str = ""
message = f"Success, time: {duration} seconds."
if wrong_tone_message != "":
message = wrong_tone_message + "\n" + message
return message, (sr, audio), kata_tone_json_str
initial_text = "こんにちは、初めまして。あなたの名前はなんていうの?"
examples = [
[initial_text, "JP"],
[
"""あなたがそんなこと言うなんて、私はとっても嬉しい。
あなたがそんなこと言うなんて、私はとっても怒ってる。
あなたがそんなこと言うなんて、私はとっても驚いてる。
あなたがそんなこと言うなんて、私はとっても辛い。""",
"JP",
],
[ # ChatGPTに考えてもらった告白セリフ
"""私、ずっと前からあなたのことを見てきました。あなたの笑顔、優しさ、強さに、心惹かれていたんです。
友達として過ごす中で、あなたのことがだんだんと特別な存在になっていくのがわかりました。
えっと、私、あなたのことが好きです!もしよければ、私と付き合ってくれませんか?""",
"JP",
],
[ # 夏目漱石『吾輩は猫である』
"""吾輩は猫である。名前はまだ無い。
どこで生れたかとんと見当がつかぬ。なんでも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。
吾輩はここで初めて人間というものを見た。しかもあとで聞くと、それは書生という、人間中で一番獰悪な種族であったそうだ。
この書生というのは時々我々を捕まえて煮て食うという話である。""",
"JP",
],
[ # 梶井基次郎『桜の樹の下には』
"""桜の樹の下には屍体が埋まっている!これは信じていいことなんだよ。
何故って、桜の花があんなにも見事に咲くなんて信じられないことじゃないか。俺はあの美しさが信じられないので、このにさんにち不安だった。
しかしいま、やっとわかるときが来た。桜の樹の下には屍体が埋まっている。これは信じていいことだ。""",
"JP",
],
[ # ChatGPTと考えた、感情を表すセリフ
"""やったー!テストで満点取れた!私とっても嬉しいな!
どうして私の意見を無視するの?許せない!ムカつく!あんたなんか死ねばいいのに。
あはははっ!この漫画めっちゃ笑える、見てよこれ、ふふふ、あはは。
あなたがいなくなって、私は一人になっちゃって、泣いちゃいそうなほど悲しい。""",
"JP",
],
[ # 上の丁寧語バージョン
"""やりました!テストで満点取れましたよ!私とっても嬉しいです!
どうして私の意見を無視するんですか?許せません!ムカつきます!あんたなんか死んでください。
あはははっ!この漫画めっちゃ笑えます、見てくださいこれ、ふふふ、あはは。
あなたがいなくなって、私は一人になっちゃって、泣いちゃいそうなほど悲しいです。""",
"JP",
],
[ # ChatGPTに考えてもらった音声合成の説明文章
"""音声合成は、機械学習を活用して、テキストから人の声を再現する技術です。この技術は、言語の構造を解析し、それに基づいて音声を生成します。
この分野の最新の研究成果を使うと、より自然で表現豊かな音声の生成が可能である。深層学習の応用により、感情やアクセントを含む声質の微妙な変化も再現することが出来る。""",
"JP",
],
[
"Speech synthesis is the artificial production of human speech. A computer system used for this purpose is called a speech synthesizer, and can be implemented in software or hardware products.",
"EN",
],
[
"语音合成是人工制造人类语音。用于此目的的计算机系统称为语音合成器,可以通过软件或硬件产品实现。",
"ZH",
],
]
initial_md = f"""
# Style-Bert-VITS2 ver {VERSION} 音声合成
- Ver 2.3で追加されたエディターのほうが実際に読み上げさせるには使いやすいかもしれません。`Editor.bat`か`python server_editor.py`で起動できます。
- 初期からある[jvnvのモデル](https://huggingface.co/litagin/style_bert_vits2_jvnv)は、[JVNVコーパス言語音声と非言語音声を持つ日本語感情音声コーパス](https://sites.google.com/site/shinnosuketakamichi/research-topics/jvnv_corpus)で学習されたモデルです。ライセンスは[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.ja)です。
"""
how_to_md = """
下のように`model_assets`ディレクトリの中にモデルファイルたちを置いてください。
```
model_assets
├── your_model
│ ├── config.json
│ ├── your_model_file1.safetensors
│ ├── your_model_file2.safetensors
│ ├── ...
│ └── style_vectors.npy
└── another_model
├── ...
```
各モデルにはファイルたちが必要です:
- `config.json`:学習時の設定ファイル
- `*.safetensors`学習済みモデルファイル1つ以上が必要、複数可
- `style_vectors.npy`:スタイルベクトルファイル
上2つは`Train.bat`による学習で自動的に正しい位置に保存されます。`style_vectors.npy`は`Style.bat`を実行して指示に従って生成してください。
"""
style_md = f"""
- プリセットまたは音声ファイルから読み上げの声音・感情・スタイルのようなものを制御できます。
- デフォルトの{DEFAULT_STYLE}でも、十分に読み上げる文に応じた感情で感情豊かに読み上げられます。このスタイル制御は、それを重み付きで上書きするような感じです。
- 強さを大きくしすぎると発音が変になったり声にならなかったりと崩壊することがあります。
- どのくらいに強さがいいかはモデルやスタイルによって異なるようです。
- 音声ファイルを入力する場合は、学習データと似た声音の話者(特に同じ性別)でないとよい効果が出ないかもしれません。
"""
def make_interactive():
return gr.update(interactive=True, value="音声合成")
def make_non_interactive():
return gr.update(interactive=False, value="音声合成(モデルをロードしてください)")
def gr_util(item):
if item == "プリセットから選ぶ":
return (gr.update(visible=True), gr.Audio(visible=False, value=None))
else:
return (gr.update(visible=False), gr.update(visible=True))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--cpu", action="store_true", help="Use CPU instead of GPU")
parser.add_argument(
"--dir", "-d", type=str, help="Model directory", default=assets_root
)
parser.add_argument(
"--share", action="store_true", help="Share this app publicly", default=False
)
parser.add_argument(
"--server-name",
type=str,
default=None,
help="Server name for Gradio app",
)
parser.add_argument(
"--no-autolaunch",
action="store_true",
default=False,
help="Do not launch app automatically",
)
parser.add_argument("--device", type=str, default="cuda")
parser.add_argument("--no_autolaunch", action="store_true")
parser.add_argument("--share", action="store_true")
args = parser.parse_args()
model_dir = Path(args.dir)
if args.cpu:
device = args.device
if device == "cuda" and not torch.cuda.is_available():
device = "cpu"
else:
device = "cuda" if torch.cuda.is_available() else "cpu"
model_holder = ModelHolder(model_dir, device)
model_names = model_holder.model_names
if len(model_names) == 0:
logger.error(
f"モデルが見つかりませんでした。{model_dir}にモデルを置いてください。"
)
sys.exit(1)
initial_id = 0
initial_pth_files = model_holder.model_files_dict[model_names[initial_id]]
model_holder = ModelHolder(Path(assets_root), device)
with gr.Blocks(theme=GRADIO_THEME) as app:
gr.Markdown(initial_md)
with gr.Accordion(label="使い方", open=False):
gr.Markdown(how_to_md)
with gr.Row():
with gr.Column():
with gr.Row():
with gr.Column(scale=3):
model_name = gr.Dropdown(
label="モデル一覧",
choices=model_names,
value=model_names[initial_id],
)
model_path = gr.Dropdown(
label="モデルファイル",
choices=initial_pth_files,
value=initial_pth_files[0],
)
refresh_button = gr.Button("更新", scale=1, visible=True)
load_button = gr.Button("ロード", scale=1, variant="primary")
text_input = gr.TextArea(label="テキスト", value=initial_text)
pitch_scale = gr.Slider(
minimum=0.8,
maximum=1.5,
value=1,
step=0.05,
label="音程(1以外では音質劣化)",
visible=False, # pyworldが必要
)
intonation_scale = gr.Slider(
minimum=0,
maximum=2,
value=1,
step=0.1,
label="抑揚(1以外では音質劣化)",
visible=False, # pyworldが必要
)
gr.Markdown(f"# Style-Bert-VITS2 WebUI (version {VERSION})")
with gr.Tabs():
with gr.Tab("音声合成"):
create_inference_app(model_holder=model_holder)
with gr.Tab("データセット作成"):
create_dataset_app()
with gr.Tab("学習"):
create_train_app()
with gr.Tab("スタイル作成"):
create_style_vectors_app()
with gr.Tab("マージ"):
create_merge_app(model_holder=model_holder)
line_split = gr.Checkbox(
label="改行で分けて生成(分けたほうが感情が乗ります)",
value=DEFAULT_LINE_SPLIT,
)
split_interval = gr.Slider(
minimum=0.0,
maximum=2,
value=DEFAULT_SPLIT_INTERVAL,
step=0.1,
label="改行ごとに挟む無音の長さ(秒)",
)
line_split.change(
lambda x: (gr.Slider(visible=x)),
inputs=[line_split],
outputs=[split_interval],
)
tone = gr.Textbox(
label="アクセント調整(数値は 0=低 か1=高 のみ)",
info="改行で分けない場合のみ使えます。万能ではありません。",
)
use_tone = gr.Checkbox(label="アクセント調整を使う", value=False)
use_tone.change(
lambda x: (gr.Checkbox(value=False) if x else gr.Checkbox()),
inputs=[use_tone],
outputs=[line_split],
)
language = gr.Dropdown(choices=languages, value="JP", label="Language")
speaker = gr.Dropdown(label="話者")
with gr.Accordion(label="詳細設定", open=False):
sdp_ratio = gr.Slider(
minimum=0,
maximum=1,
value=DEFAULT_SDP_RATIO,
step=0.1,
label="SDP Ratio",
)
noise_scale = gr.Slider(
minimum=0.1,
maximum=2,
value=DEFAULT_NOISE,
step=0.1,
label="Noise",
)
noise_scale_w = gr.Slider(
minimum=0.1,
maximum=2,
value=DEFAULT_NOISEW,
step=0.1,
label="Noise_W",
)
length_scale = gr.Slider(
minimum=0.1,
maximum=2,
value=DEFAULT_LENGTH,
step=0.1,
label="Length",
)
use_assist_text = gr.Checkbox(
label="Assist textを使う", value=False
)
assist_text = gr.Textbox(
label="Assist text",
placeholder="どうして私の意見を無視するの?許せない、ムカつく!死ねばいいのに。",
info="このテキストの読み上げと似た声音・感情になりやすくなります。ただ抑揚やテンポ等が犠牲になる傾向があります。",
visible=False,
)
assist_text_weight = gr.Slider(
minimum=0,
maximum=1,
value=DEFAULT_ASSIST_TEXT_WEIGHT,
step=0.1,
label="Assist textの強さ",
visible=False,
)
use_assist_text.change(
lambda x: (gr.Textbox(visible=x), gr.Slider(visible=x)),
inputs=[use_assist_text],
outputs=[assist_text, assist_text_weight],
)
with gr.Column():
with gr.Accordion("スタイルについて詳細", open=False):
gr.Markdown(style_md)
style_mode = gr.Radio(
["プリセットから選ぶ", "音声ファイルを入力"],
label="スタイルの指定方法",
value="プリセットから選ぶ",
)
style = gr.Dropdown(
label=f"スタイル({DEFAULT_STYLE}が平均スタイル)",
choices=["モデルをロードしてください"],
value="モデルをロードしてください",
)
style_weight = gr.Slider(
minimum=0,
maximum=50,
value=DEFAULT_STYLE_WEIGHT,
step=0.1,
label="スタイルの強さ",
)
ref_audio_path = gr.Audio(
label="参照音声", type="filepath", visible=False
)
tts_button = gr.Button(
"音声合成(モデルをロードしてください)",
variant="primary",
interactive=False,
)
text_output = gr.Textbox(label="情報")
audio_output = gr.Audio(label="結果")
with gr.Accordion("テキスト例", open=False):
gr.Examples(examples, inputs=[text_input, language])
tts_button.click(
tts_fn,
inputs=[
model_name,
model_path,
text_input,
language,
ref_audio_path,
sdp_ratio,
noise_scale,
noise_scale_w,
length_scale,
line_split,
split_interval,
assist_text,
assist_text_weight,
use_assist_text,
style,
style_weight,
tone,
use_tone,
speaker,
pitch_scale,
intonation_scale,
],
outputs=[text_output, audio_output, tone],
)
model_name.change(
model_holder.update_model_files_gr,
inputs=[model_name],
outputs=[model_path],
)
model_path.change(make_non_interactive, outputs=[tts_button])
refresh_button.click(
model_holder.update_model_names_gr,
outputs=[model_name, model_path, tts_button],
)
load_button.click(
model_holder.load_model_gr,
inputs=[model_name, model_path],
outputs=[style, tts_button, speaker],
)
style_mode.change(
gr_util,
inputs=[style_mode],
outputs=[style, ref_audio_path],
)
app.launch(
inbrowser=not args.no_autolaunch, share=args.share, server_name=args.server_name
)
app.launch(inbrowser=not args.no_autolaunch, share=args.share)

View File

@@ -10,8 +10,11 @@ from config import config
from style_bert_vits2.logging import logger
from style_bert_vits2.models import commons
from style_bert_vits2.text_processing import cleaned_text_to_sequence, extract_bert_feature
from style_bert_vits2.text_processing.japanese import pyopenjtalk_worker as pyopenjtalk
from style_bert_vits2.utils.stdout_wrapper import SAFE_STDOUT
pyopenjtalk.initialize()
def process_line(x):
line, add_blank = x

View File

@@ -219,12 +219,14 @@ class ModelHolder:
self.current_model: Optional[Model] = None
self.model_names: list[str] = []
self.models: list[Model] = []
self.models_info: list[dict[str, Union[str, list[str]]]] = []
self.refresh()
def refresh(self):
self.model_files_dict = {}
self.model_names = []
self.current_model = None
self.models_info = []
model_dirs = [d for d in self.root_dir.iterdir() if d.is_dir()]
for model_dir in model_dirs:
@@ -244,26 +246,19 @@ class ModelHolder:
continue
self.model_files_dict[model_dir.name] = model_files
self.model_names.append(model_dir.name)
def models_info(self):
if hasattr(self, "_models_info"):
return self._models_info
result = []
for name, files in self.model_files_dict.items():
# Get styles
config_path = self.root_dir / name / "config.json"
hps = utils.get_hparams_from_file(config_path)
style2id: dict[str, int] = hps.data.style2id
styles = list(style2id.keys())
result.append(
spk2id: dict[str, int] = hps.data.spk2id
speakers = list(spk2id.keys())
self.models_info.append(
{
"name": name,
"files": [str(f) for f in files],
"name": model_dir.name,
"files": [str(f) for f in model_files],
"styles": styles,
"speakers": speakers,
}
)
self._models_info = result
return result
def load_model(self, model_name: str, model_path_str: str):
model_path = Path(model_path_str)

View File

@@ -12,7 +12,6 @@ import io
import shutil
import sys
import webbrowser
import yaml
import zipfile
from datetime import datetime
from io import BytesIO
@@ -22,6 +21,7 @@ import numpy as np
import requests
import torch
import uvicorn
import yaml
from fastapi import APIRouter, FastAPI, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, Response
@@ -46,10 +46,10 @@ from style_bert_vits2.text_processing.japanese import normalize_text
from style_bert_vits2.text_processing.japanese.g2p_utils import g2kata_tone, kata_tone2phone_tone
from style_bert_vits2.text_processing.japanese.user_dict import (
apply_word,
update_dict,
delete_word,
read_dict,
rewrite_word,
delete_word,
update_dict,
)
@@ -245,7 +245,7 @@ async def normalize(item: TextRequest):
@router.get("/models_info")
def models_info():
return model_holder.models_info()
return model_holder.models_info
class SynthesisRequest(BaseModel):
@@ -265,6 +265,7 @@ class SynthesisRequest(BaseModel):
silenceAfter: float = 0.5
pitchScale: float = 1.0
intonationScale: float = 1.0
speaker: str | None = None
@router.post("/synthesis", response_class=AudioResponse)
@@ -290,6 +291,13 @@ def synthesis(request: SynthesisRequest):
]
phone_tone = kata_tone2phone_tone(kata_tone_list)
tone = [t for _, t in phone_tone]
try:
sid = 0 if request.speaker is None else model.spk2id[request.speaker]
except KeyError:
raise HTTPException(
status_code=400,
detail=f"Speaker {request.speaker} not found in {model.spk2id}",
)
sr, audio = model.infer(
text=text,
language=request.language.value,
@@ -306,6 +314,7 @@ def synthesis(request: SynthesisRequest):
line_split=False,
pitch_scale=request.pitchScale,
intonation_scale=request.intonationScale,
sid=sid,
)
with BytesIO() as wavContent:

View File

@@ -3,7 +3,7 @@ from pathlib import Path
# Style-Bert-VITS2 のバージョン
VERSION = "2.3.1"
VERSION = "2.4"
# Style-Bert-VITS2 のベースディレクトリ
BASE_DIR = Path(__file__).parent.parent

View File

@@ -1,13 +1,15 @@
import pyopenjtalk
import re
from style_bert_vits2.constants import Languages
from style_bert_vits2.logging import logger
from style_bert_vits2.text_processing import bert_models
from style_bert_vits2.text_processing.japanese import pyopenjtalk_worker as pyopenjtalk
from style_bert_vits2.text_processing.japanese.mora_list import MORA_KATA_TO_MORA_PHONEMES
from style_bert_vits2.text_processing.japanese.normalizer import replace_punctuation
from style_bert_vits2.text_processing.symbols import PUNCTUATIONS
pyopenjtalk.initialize()
def g2p(
norm_text: str,

View File

@@ -0,0 +1,126 @@
"""
Run the pyopenjtalk worker in a separate process
to avoid user dictionary access error
"""
from typing import Any, Optional
from style_bert_vits2.logging import logger
from style_bert_vits2.text_processing.japanese.pyopenjtalk_worker.worker_client import WorkerClient
from style_bert_vits2.text_processing.japanese.pyopenjtalk_worker.worker_common import WORKER_PORT
WORKER_CLIENT: Optional[WorkerClient] = None
# pyopenjtalk interface
# g2p(): not used
def run_frontend(text: str) -> list[dict[str, Any]]:
assert WORKER_CLIENT
ret = WORKER_CLIENT.dispatch_pyopenjtalk("run_frontend", text)
assert isinstance(ret, list)
return ret
def make_label(njd_features: Any) -> list[str]:
assert WORKER_CLIENT
ret = WORKER_CLIENT.dispatch_pyopenjtalk("make_label", njd_features)
assert isinstance(ret, list)
return ret
def mecab_dict_index(path: str, out_path: str, dn_mecab: Optional[str] = None):
assert WORKER_CLIENT
WORKER_CLIENT.dispatch_pyopenjtalk("mecab_dict_index", path, out_path, dn_mecab)
def update_global_jtalk_with_user_dict(path: str):
assert WORKER_CLIENT
WORKER_CLIENT.dispatch_pyopenjtalk("update_global_jtalk_with_user_dict", path)
def unset_user_dict():
assert WORKER_CLIENT
WORKER_CLIENT.dispatch_pyopenjtalk("unset_user_dict")
# initialize module when imported
def initialize(port: int = WORKER_PORT) -> None:
import time
import socket
import sys
import atexit
import signal
logger.debug("initialize")
global WORKER_CLIENT
if WORKER_CLIENT:
return
client = None
try:
client = WorkerClient(port)
except (socket.timeout, socket.error):
logger.debug("try starting pyopenjtalk worker server")
import os
import subprocess
worker_pkg_path = os.path.relpath(
os.path.dirname(__file__), os.getcwd()
).replace(os.sep, ".")
args = [sys.executable, "-m", worker_pkg_path, "--port", str(port)]
# new session, new process group
if sys.platform.startswith("win"):
cf = subprocess.CREATE_NEW_CONSOLE | subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore
si = subprocess.STARTUPINFO() # type: ignore
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore
si.wShowWindow = subprocess.SW_HIDE # type: ignore
subprocess.Popen(args, creationflags=cf, startupinfo=si)
else:
# align with Windows behavior
# start_new_session is same as specifying setsid in preexec_fn
subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True) # type: ignore
# wait until server listening
count = 0
while True:
try:
client = WorkerClient(port)
break
except socket.error:
time.sleep(1)
count += 1
# 10: max number of retries
if count == 10:
raise TimeoutError("サーバーに接続できませんでした")
WORKER_CLIENT = client
atexit.register(terminate)
# when the process is killed
def signal_handler(signum: int, frame: Any):
terminate()
signal.signal(signal.SIGTERM, signal_handler)
# top-level declaration
def terminate() -> None:
logger.debug("terminate")
global WORKER_CLIENT
if not WORKER_CLIENT:
return
# repare for unexpected errors
try:
if WORKER_CLIENT.status() == 1:
WORKER_CLIENT.quit_server()
except Exception as e:
logger.error(e)
WORKER_CLIENT.close()
WORKER_CLIENT = None

View File

@@ -0,0 +1,16 @@
import argparse
from style_bert_vits2.text_processing.japanese.pyopenjtalk_worker.worker_common import WORKER_PORT
from style_bert_vits2.text_processing.japanese.pyopenjtalk_worker.worker_server import WorkerServer
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--port", type=int, default=WORKER_PORT)
args = parser.parse_args()
server = WorkerServer()
server.start_server(port=args.port)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,63 @@
import socket
from typing import Any, cast
from style_bert_vits2.logging import logger
from style_bert_vits2.text_processing.japanese.pyopenjtalk_worker.worker_common import RequestType, receive_data, send_data
class WorkerClient:
""" pyopenjtalk worker client """
def __init__(self, port: int) -> None:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 5: timeout
sock.settimeout(5)
sock.connect((socket.gethostname(), port))
self.sock = sock
def __enter__(self) -> "WorkerClient":
return self
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
self.close()
def close(self) -> None:
self.sock.close()
def dispatch_pyopenjtalk(self, func: str, *args: Any, **kwargs: Any) -> Any:
data = {
"request-type": RequestType.PYOPENJTALK,
"func": func,
"args": args,
"kwargs": kwargs,
}
logger.trace(f"client sends request: {data}")
send_data(self.sock, data)
logger.trace("client sent request successfully")
response = receive_data(self.sock)
logger.trace(f"client received response: {response}")
return response.get("return")
def status(self) -> int:
data = {"request-type": RequestType.STATUS}
logger.trace(f"client sends request: {data}")
send_data(self.sock, data)
logger.trace("client sent request successfully")
response = receive_data(self.sock)
logger.trace(f"client received response: {response}")
return cast(int, response.get("client-count"))
def quit_server(self) -> None:
data = {"request-type": RequestType.QUIT_SERVER}
logger.trace(f"client sends request: {data}")
send_data(self.sock, data)
logger.trace("client sent request successfully")
response = receive_data(self.sock)
logger.trace(f"client received response: {response}")

View File

@@ -0,0 +1,45 @@
import json
import socket
from enum import IntEnum, auto
from typing import Any, Final
WORKER_PORT: Final[int] = 7861
HEADER_SIZE: Final[int] = 4
class RequestType(IntEnum):
STATUS = auto()
QUIT_SERVER = auto()
PYOPENJTALK = auto()
class ConnectionClosedException(Exception):
pass
# socket communication
def send_data(sock: socket.socket, data: dict[str, Any]):
json_data = json.dumps(data).encode()
header = len(json_data).to_bytes(HEADER_SIZE, byteorder="big")
sock.sendall(header + json_data)
def __receive_until(sock: socket.socket, size: int):
data = b""
while len(data) < size:
part = sock.recv(size - len(data))
if part == b"":
raise ConnectionClosedException("接続が閉じられました")
data += part
return data
def receive_data(sock: socket.socket) -> dict[str, Any]:
header = __receive_until(sock, HEADER_SIZE)
data_length = int.from_bytes(header, byteorder="big")
body = __receive_until(sock, data_length)
return json.loads(body.decode())

View File

@@ -0,0 +1,126 @@
import select
import socket
import time
from typing import Any, cast
import pyopenjtalk
from style_bert_vits2.logging import logger
from style_bert_vits2.text_processing.japanese.pyopenjtalk_worker.worker_common import (
ConnectionClosedException,
RequestType,
receive_data,
send_data,
)
# To make it as fast as possible
# Probably faster than calling getattr every time
__PYOPENJTALK_FUNC_DICT = {
"run_frontend": pyopenjtalk.run_frontend,
"make_label": pyopenjtalk.make_label,
"mecab_dict_index": pyopenjtalk.mecab_dict_index,
"update_global_jtalk_with_user_dict": pyopenjtalk.update_global_jtalk_with_user_dict,
"unset_user_dict": pyopenjtalk.unset_user_dict,
}
class WorkerServer:
""" pyopenjtalk worker server """
def __init__(self) -> None:
self.client_count: int = 0
self.quit: bool = False
def handle_request(self, request: dict[str, Any]) -> dict[str, Any]:
request_type = None
try:
request_type = RequestType(cast(int, request.get("request-type")))
except Exception:
return {
"success": False,
"reason": "request-type is invalid",
}
response: dict[str, Any] = {}
if request_type:
if request_type == RequestType.STATUS:
response = {
"success": True,
"client-count": self.client_count,
}
elif request_type == RequestType.QUIT_SERVER:
self.quit = True
response = {"success": True}
elif request_type == RequestType.PYOPENJTALK:
func_name = request.get("func")
assert isinstance(func_name, str)
func = __PYOPENJTALK_FUNC_DICT[func_name]
args = request.get("args")
kwargs = request.get("kwargs")
assert isinstance(args, list)
assert isinstance(kwargs, dict)
ret = func(*args, **kwargs)
response = {"success": True, "return": ret}
else:
# NOT REACHED
response = request
return response
def start_server(self, port: int, no_client_timeout: int = 30) -> None:
logger.info("start pyopenjtalk worker server")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.bind((socket.gethostname(), port))
server_socket.listen()
sockets = [server_socket]
no_client_since = time.time()
while True:
if self.client_count == 0:
if no_client_since is None:
no_client_since = time.time()
elif (time.time() - no_client_since) > no_client_timeout:
logger.info("quit because there is no client")
return
else:
no_client_since = None
ready_sockets, _, _ = select.select(sockets, [], [], 0.1)
for sock in ready_sockets:
if sock is server_socket:
logger.info("new client connected")
client_socket, _ = server_socket.accept()
sockets.append(client_socket)
self.client_count += 1
else:
# client
try:
request = receive_data(sock)
except Exception as e:
sock.close()
sockets.remove(sock)
self.client_count -= 1
# unexpected disconnections
if not isinstance(e, ConnectionClosedException):
logger.error(e)
logger.info("close connection")
continue
logger.trace(f"server received request: {request}")
response = self.handle_request(request)
logger.trace(f"server sends response: {response}")
try:
send_data(sock, response)
logger.trace("server sent response successfully")
except Exception:
logger.warning(
"an exception occurred during sending responce"
)
if self.quit:
logger.info("quit pyopenjtalk worker server")
return

View File

@@ -13,15 +13,15 @@ from typing import Dict, List, Optional
from uuid import UUID, uuid4
import numpy as np
import pyopenjtalk
from fastapi import HTTPException
from style_bert_vits2.constants import DEFAULT_USER_DICT_DIR
from style_bert_vits2.text_processing.japanese import pyopenjtalk_worker as pyopenjtalk
from style_bert_vits2.text_processing.japanese.user_dict.word_model import UserDictWord, WordTypes
# from ..utility.mutex_utility import mutex_wrapper
# from ..utility.path_utility import engine_root, get_save_dir
from style_bert_vits2.text_processing.japanese.user_dict.part_of_speech_data import MAX_PRIORITY, MIN_PRIORITY, part_of_speech_data
pyopenjtalk.initialize()
# root_dir = engine_root()
# save_dir = get_save_dir()

16
webui/__init__.py Normal file
View File

@@ -0,0 +1,16 @@
from .dataset import create_dataset_app
from .inference import create_inference_app
from .merge import create_merge_app
from .style_vectors import create_style_vectors_app
from .train import create_train_app
class TrainSettings:
def __init__(self, setting_json):
self.setting_json = setting_json
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
pass

191
webui/dataset.py Normal file
View File

@@ -0,0 +1,191 @@
import gradio as gr
from style_bert_vits2.logging import logger
from style_bert_vits2.utils.subprocess import run_script_with_log
def do_slice(
model_name: str,
min_sec: float,
max_sec: float,
min_silence_dur_ms: int,
input_dir: str,
):
if model_name == "":
return "Error: モデル名を入力してください。"
logger.info("Start slicing...")
cmd = [
"slice.py",
"--model_name",
model_name,
"--min_sec",
str(min_sec),
"--max_sec",
str(max_sec),
"--min_silence_dur_ms",
str(min_silence_dur_ms),
]
if input_dir != "":
cmd += ["--input_dir", input_dir]
# onnxの警告が出るので無視する
success, message = run_script_with_log(cmd, ignore_warning=True)
if not success:
return f"Error: {message}"
return "音声のスライスが完了しました。"
def do_transcribe(
model_name, whisper_model, compute_type, language, initial_prompt, device
):
if model_name == "":
return "Error: モデル名を入力してください。"
success, message = run_script_with_log(
[
"transcribe.py",
"--model_name",
model_name,
"--model",
whisper_model,
"--compute_type",
compute_type,
"--device",
device,
"--language",
language,
"--initial_prompt",
f'"{initial_prompt}"',
]
)
if not success:
return f"Error: {message}. しかし何故かエラーが起きても正常に終了している場合がほとんどなので、書き起こし結果を確認して問題なければ学習に使えます。"
return "音声の文字起こしが完了しました。"
how_to_md = """
Style-Bert-VITS2の学習用データセットを作成するためのツールです。以下の2つからなります。
- 与えられた音声からちょうどいい長さの発話区間を切り取りスライス
- 音声に対して文字起こし
このうち両方を使ってもよいし、スライスする必要がない場合は後者のみを使ってもよいです。
## 必要なもの
学習したい音声が入ったwavファイルいくつか。
合計時間がある程度はあったほうがいいかも、10分とかでも大丈夫だったとの報告あり。単一ファイルでも良いし複数ファイルでもよい。
## スライス使い方
1. `inputs`フォルダにwavファイルをすべて入れる
2. `モデル名`を入力して、設定を必要なら調整して`音声のスライス`ボタンを押す
3. 出来上がった音声ファイルたちは`Data/{モデル名}/raw`に保存される
## 書き起こし使い方
1. 書き起こしたい音声ファイルのあるフォルダを指定(デフォルトは`Data/{モデル名}/raw`なのでスライス後に行う場合は省略してよい)
2. 設定を必要なら調整してボタンを押す
3. 書き起こしファイルは`Data/{モデル名}/esd.list`に保存される
## 注意
- 長すぎる秒数12-15秒くらいより長いのwavファイルは学習に用いられないようです。また短すぎてもあまりよくない可能性もあります。
- 書き起こしの結果をどれだけ修正すればいいかはデータセットに依存しそうです。
- 手動で書き起こしをいろいろ修正したり結果を細かく確認したい場合は、[Aivis Dataset](https://github.com/litagin02/Aivis-Dataset)もおすすめします。書き起こし部分もかなり工夫されています。ですがファイル数が多い場合などは、このツールで簡易的に切り出してデータセットを作るだけでも十分という気もしています。
"""
def create_dataset_app() -> gr.Blocks:
with gr.Blocks() as app:
with gr.Accordion("使い方", open=False):
gr.Markdown(how_to_md)
model_name = gr.Textbox(
label="モデル名を入力してください(話者名としても使われます)。"
)
with gr.Accordion("音声のスライス"):
with gr.Row():
with gr.Column():
input_dir = gr.Textbox(
label="元音声の入っているフォルダパス",
value="inputs",
info="下記フォルダにwavファイルを入れておいてください",
)
min_sec = gr.Slider(
minimum=0,
maximum=10,
value=2,
step=0.5,
label="この秒数未満は切り捨てる",
)
max_sec = gr.Slider(
minimum=0,
maximum=15,
value=12,
step=0.5,
label="この秒数以上は切り捨てる",
)
min_silence_dur_ms = gr.Slider(
minimum=0,
maximum=2000,
value=700,
step=100,
label="無音とみなして区切る最小の無音の長さms",
)
slice_button = gr.Button("スライスを実行")
result1 = gr.Textbox(label="結果")
with gr.Row():
with gr.Column():
whisper_model = gr.Dropdown(
[
"tiny",
"base",
"small",
"medium",
"large",
"large-v2",
"large-v3",
],
label="Whisperモデル",
value="large-v3",
)
compute_type = gr.Dropdown(
[
"int8",
"int8_float32",
"int8_float16",
"int8_bfloat16",
"int16",
"float16",
"bfloat16",
"float32",
],
label="計算精度",
value="bfloat16",
)
device = gr.Radio(["cuda", "cpu"], label="デバイス", value="cuda")
language = gr.Dropdown(["ja", "en", "zh"], value="ja", label="言語")
initial_prompt = gr.Textbox(
label="初期プロンプト",
value="こんにちは。元気、ですかー?ふふっ、私は……ちゃんと元気だよ!",
info="このように書き起こしてほしいという例文(句読点の入れ方・笑い方・固有名詞等)",
)
transcribe_button = gr.Button("音声の文字起こし")
result2 = gr.Textbox(label="結果")
slice_button.click(
do_slice,
inputs=[model_name, min_sec, max_sec, min_silence_dur_ms, input_dir],
outputs=[result1],
)
transcribe_button.click(
do_transcribe,
inputs=[
model_name,
whisper_model,
compute_type,
language,
initial_prompt,
device,
],
outputs=[result2],
)
return app

458
webui/inference.py Normal file
View File

@@ -0,0 +1,458 @@
import datetime
import json
from typing import Optional
import gradio as gr
from common.tts_model import ModelHolder
from style_bert_vits2.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,
GRADIO_THEME,
Languages,
)
from style_bert_vits2.logging import logger
from style_bert_vits2.models.infer import InvalidToneError
from style_bert_vits2.text_processing.japanese.g2p_utils import g2kata_tone, kata_tone2phone_tone
from style_bert_vits2.text_processing.japanese.normalizer import normalize_text
languages = [l.value for l in Languages]
initial_text = "こんにちは、初めまして。あなたの名前はなんていうの?"
examples = [
[initial_text, "JP"],
[
"""あなたがそんなこと言うなんて、私はとっても嬉しい。
あなたがそんなこと言うなんて、私はとっても怒ってる。
あなたがそんなこと言うなんて、私はとっても驚いてる。
あなたがそんなこと言うなんて、私はとっても辛い。""",
"JP",
],
[ # ChatGPTに考えてもらった告白セリフ
"""私、ずっと前からあなたのことを見てきました。あなたの笑顔、優しさ、強さに、心惹かれていたんです。
友達として過ごす中で、あなたのことがだんだんと特別な存在になっていくのがわかりました。
えっと、私、あなたのことが好きです!もしよければ、私と付き合ってくれませんか?""",
"JP",
],
[ # 夏目漱石『吾輩は猫である』
"""吾輩は猫である。名前はまだ無い。
どこで生れたかとんと見当がつかぬ。なんでも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。
吾輩はここで初めて人間というものを見た。しかもあとで聞くと、それは書生という、人間中で一番獰悪な種族であったそうだ。
この書生というのは時々我々を捕まえて煮て食うという話である。""",
"JP",
],
[ # 梶井基次郎『桜の樹の下には』
"""桜の樹の下には屍体が埋まっている!これは信じていいことなんだよ。
何故って、桜の花があんなにも見事に咲くなんて信じられないことじゃないか。俺はあの美しさが信じられないので、このにさんにち不安だった。
しかしいま、やっとわかるときが来た。桜の樹の下には屍体が埋まっている。これは信じていいことだ。""",
"JP",
],
[ # ChatGPTと考えた、感情を表すセリフ
"""やったー!テストで満点取れた!私とっても嬉しいな!
どうして私の意見を無視するの?許せない!ムカつく!あんたなんか死ねばいいのに。
あはははっ!この漫画めっちゃ笑える、見てよこれ、ふふふ、あはは。
あなたがいなくなって、私は一人になっちゃって、泣いちゃいそうなほど悲しい。""",
"JP",
],
[ # 上の丁寧語バージョン
"""やりました!テストで満点取れましたよ!私とっても嬉しいです!
どうして私の意見を無視するんですか?許せません!ムカつきます!あんたなんか死んでください。
あはははっ!この漫画めっちゃ笑えます、見てくださいこれ、ふふふ、あはは。
あなたがいなくなって、私は一人になっちゃって、泣いちゃいそうなほど悲しいです。""",
"JP",
],
[ # ChatGPTに考えてもらった音声合成の説明文章
"""音声合成は、機械学習を活用して、テキストから人の声を再現する技術です。この技術は、言語の構造を解析し、それに基づいて音声を生成します。
この分野の最新の研究成果を使うと、より自然で表現豊かな音声の生成が可能である。深層学習の応用により、感情やアクセントを含む声質の微妙な変化も再現することが出来る。""",
"JP",
],
[
"Speech synthesis is the artificial production of human speech. A computer system used for this purpose is called a speech synthesizer, and can be implemented in software or hardware products.",
"EN",
],
[
"语音合成是人工制造人类语音。用于此目的的计算机系统称为语音合成器,可以通过软件或硬件产品实现。",
"ZH",
],
]
initial_md = f"""
- Ver 2.3で追加されたエディターのほうが実際に読み上げさせるには使いやすいかもしれません。`Editor.bat`か`python server_editor.py --inbrowser`で起動できます。
- 初期からある[jvnvのモデル](https://huggingface.co/litagin/style_bert_vits2_jvnv)は、[JVNVコーパス言語音声と非言語音声を持つ日本語感情音声コーパス](https://sites.google.com/site/shinnosuketakamichi/research-topics/jvnv_corpus)で学習されたモデルです。ライセンスは[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.ja)です。
"""
how_to_md = """
下のように`model_assets`ディレクトリの中にモデルファイルたちを置いてください。
```
model_assets
├── your_model
│ ├── config.json
│ ├── your_model_file1.safetensors
│ ├── your_model_file2.safetensors
│ ├── ...
│ └── style_vectors.npy
└── another_model
├── ...
```
各モデルにはファイルたちが必要です:
- `config.json`:学習時の設定ファイル
- `*.safetensors`学習済みモデルファイル1つ以上が必要、複数可
- `style_vectors.npy`:スタイルベクトルファイル
上2つは`Train.bat`による学習で自動的に正しい位置に保存されます。`style_vectors.npy`は`Style.bat`を実行して指示に従って生成してください。
"""
style_md = f"""
- プリセットまたは音声ファイルから読み上げの声音・感情・スタイルのようなものを制御できます。
- デフォルトの{DEFAULT_STYLE}でも、十分に読み上げる文に応じた感情で感情豊かに読み上げられます。このスタイル制御は、それを重み付きで上書きするような感じです。
- 強さを大きくしすぎると発音が変になったり声にならなかったりと崩壊することがあります。
- どのくらいに強さがいいかはモデルやスタイルによって異なるようです。
- 音声ファイルを入力する場合は、学習データと似た声音の話者(特に同じ性別)でないとよい効果が出ないかもしれません。
"""
def make_interactive():
return gr.update(interactive=True, value="音声合成")
def make_non_interactive():
return gr.update(interactive=False, value="音声合成(モデルをロードしてください)")
def gr_util(item):
if item == "プリセットから選ぶ":
return (gr.update(visible=True), gr.Audio(visible=False, value=None))
else:
return (gr.update(visible=False), gr.update(visible=True))
def create_inference_app(model_holder: ModelHolder) -> gr.Blocks:
def tts_fn(
model_name,
model_path,
text,
language,
reference_audio_path,
sdp_ratio,
noise_scale,
noise_scale_w,
length_scale,
line_split,
split_interval,
assist_text,
assist_text_weight,
use_assist_text,
style,
style_weight,
kata_tone_json_str,
use_tone,
speaker,
pitch_scale,
intonation_scale,
):
model_holder.load_model(model_name, model_path)
assert model_holder.current_model is not None
wrong_tone_message = ""
kata_tone: Optional[list[tuple[str, int]]] = None
if use_tone and kata_tone_json_str != "":
if language != "JP":
logger.warning("Only Japanese is supported for tone generation.")
wrong_tone_message = "アクセント指定は現在日本語のみ対応しています。"
if line_split:
logger.warning("Tone generation is not supported for line split.")
wrong_tone_message = (
"アクセント指定は改行で分けて生成を使わない場合のみ対応しています。"
)
try:
kata_tone = []
json_data = json.loads(kata_tone_json_str)
# tupleを使うように変換
for kana, tone in json_data:
assert isinstance(kana, str) and tone in (0, 1), f"{kana}, {tone}"
kata_tone.append((kana, tone))
except Exception as e:
logger.warning(f"Error occurred when parsing kana_tone_json: {e}")
wrong_tone_message = f"アクセント指定が不正です: {e}"
kata_tone = None
# toneは実際に音声合成に代入される際のみnot Noneになる
tone: Optional[list[int]] = None
if kata_tone is not None:
phone_tone = kata_tone2phone_tone(kata_tone)
tone = [t for _, t in phone_tone]
speaker_id = model_holder.current_model.spk2id[speaker]
start_time = datetime.datetime.now()
try:
sr, audio = model_holder.current_model.infer(
text=text,
language=language,
reference_audio_path=reference_audio_path,
sdp_ratio=sdp_ratio,
noise=noise_scale,
noisew=noise_scale_w,
length=length_scale,
line_split=line_split,
split_interval=split_interval,
assist_text=assist_text,
assist_text_weight=assist_text_weight,
use_assist_text=use_assist_text,
style=style,
style_weight=style_weight,
given_tone=tone,
sid=speaker_id,
pitch_scale=pitch_scale,
intonation_scale=intonation_scale,
)
except InvalidToneError as e:
logger.error(f"Tone error: {e}")
return f"Error: アクセント指定が不正です:\n{e}", None, kata_tone_json_str
except ValueError as e:
logger.error(f"Value error: {e}")
return f"Error: {e}", None, kata_tone_json_str
end_time = datetime.datetime.now()
duration = (end_time - start_time).total_seconds()
if tone is None and language == "JP":
# アクセント指定に使えるようにアクセント情報を返す
norm_text = normalize_text(text)
kata_tone = g2kata_tone(norm_text)
kata_tone_json_str = json.dumps(kata_tone, ensure_ascii=False)
elif tone is None:
kata_tone_json_str = ""
message = f"Success, time: {duration} seconds."
if wrong_tone_message != "":
message = wrong_tone_message + "\n" + message
return message, (sr, audio), kata_tone_json_str
model_names = model_holder.model_names
if len(model_names) == 0:
logger.error(
f"モデルが見つかりませんでした。{model_holder.root_dir}にモデルを置いてください。"
)
with gr.Blocks() as app:
gr.Markdown(
f"Error: モデルが見つかりませんでした。{model_holder.root_dir}にモデルを置いてください。"
)
return app
initial_id = 0
initial_pth_files = model_holder.model_files_dict[model_names[initial_id]]
with gr.Blocks(theme=GRADIO_THEME) as app:
gr.Markdown(initial_md)
with gr.Accordion(label="使い方", open=False):
gr.Markdown(how_to_md)
with gr.Row():
with gr.Column():
with gr.Row():
with gr.Column(scale=3):
model_name = gr.Dropdown(
label="モデル一覧",
choices=model_names,
value=model_names[initial_id],
)
model_path = gr.Dropdown(
label="モデルファイル",
choices=initial_pth_files,
value=initial_pth_files[0],
)
refresh_button = gr.Button("更新", scale=1, visible=True)
load_button = gr.Button("ロード", scale=1, variant="primary")
text_input = gr.TextArea(label="テキスト", value=initial_text)
pitch_scale = gr.Slider(
minimum=0.8,
maximum=1.5,
value=1,
step=0.05,
label="音程(1以外では音質劣化)",
visible=False, # pyworldが必要
)
intonation_scale = gr.Slider(
minimum=0,
maximum=2,
value=1,
step=0.1,
label="抑揚(1以外では音質劣化)",
visible=False, # pyworldが必要
)
line_split = gr.Checkbox(
label="改行で分けて生成(分けたほうが感情が乗ります)",
value=DEFAULT_LINE_SPLIT,
)
split_interval = gr.Slider(
minimum=0.0,
maximum=2,
value=DEFAULT_SPLIT_INTERVAL,
step=0.1,
label="改行ごとに挟む無音の長さ(秒)",
)
line_split.change(
lambda x: (gr.Slider(visible=x)),
inputs=[line_split],
outputs=[split_interval],
)
tone = gr.Textbox(
label="アクセント調整(数値は 0=低 か1=高 のみ)",
info="改行で分けない場合のみ使えます。万能ではありません。",
)
use_tone = gr.Checkbox(label="アクセント調整を使う", value=False)
use_tone.change(
lambda x: (gr.Checkbox(value=False) if x else gr.Checkbox()),
inputs=[use_tone],
outputs=[line_split],
)
language = gr.Dropdown(choices=languages, value="JP", label="Language")
speaker = gr.Dropdown(label="話者")
with gr.Accordion(label="詳細設定", open=False):
sdp_ratio = gr.Slider(
minimum=0,
maximum=1,
value=DEFAULT_SDP_RATIO,
step=0.1,
label="SDP Ratio",
)
noise_scale = gr.Slider(
minimum=0.1,
maximum=2,
value=DEFAULT_NOISE,
step=0.1,
label="Noise",
)
noise_scale_w = gr.Slider(
minimum=0.1,
maximum=2,
value=DEFAULT_NOISEW,
step=0.1,
label="Noise_W",
)
length_scale = gr.Slider(
minimum=0.1,
maximum=2,
value=DEFAULT_LENGTH,
step=0.1,
label="Length",
)
use_assist_text = gr.Checkbox(
label="Assist textを使う", value=False
)
assist_text = gr.Textbox(
label="Assist text",
placeholder="どうして私の意見を無視するの?許せない、ムカつく!死ねばいいのに。",
info="このテキストの読み上げと似た声音・感情になりやすくなります。ただ抑揚やテンポ等が犠牲になる傾向があります。",
visible=False,
)
assist_text_weight = gr.Slider(
minimum=0,
maximum=1,
value=DEFAULT_ASSIST_TEXT_WEIGHT,
step=0.1,
label="Assist textの強さ",
visible=False,
)
use_assist_text.change(
lambda x: (gr.Textbox(visible=x), gr.Slider(visible=x)),
inputs=[use_assist_text],
outputs=[assist_text, assist_text_weight],
)
with gr.Column():
with gr.Accordion("スタイルについて詳細", open=False):
gr.Markdown(style_md)
style_mode = gr.Radio(
["プリセットから選ぶ", "音声ファイルを入力"],
label="スタイルの指定方法",
value="プリセットから選ぶ",
)
style = gr.Dropdown(
label=f"スタイル({DEFAULT_STYLE}が平均スタイル)",
choices=["モデルをロードしてください"],
value="モデルをロードしてください",
)
style_weight = gr.Slider(
minimum=0,
maximum=50,
value=DEFAULT_STYLE_WEIGHT,
step=0.1,
label="スタイルの強さ",
)
ref_audio_path = gr.Audio(
label="参照音声", type="filepath", visible=False
)
tts_button = gr.Button(
"音声合成(モデルをロードしてください)",
variant="primary",
interactive=False,
)
text_output = gr.Textbox(label="情報")
audio_output = gr.Audio(label="結果")
with gr.Accordion("テキスト例", open=False):
gr.Examples(examples, inputs=[text_input, language])
tts_button.click(
tts_fn,
inputs=[
model_name,
model_path,
text_input,
language,
ref_audio_path,
sdp_ratio,
noise_scale,
noise_scale_w,
length_scale,
line_split,
split_interval,
assist_text,
assist_text_weight,
use_assist_text,
style,
style_weight,
tone,
use_tone,
speaker,
pitch_scale,
intonation_scale,
],
outputs=[text_output, audio_output, tone],
)
model_name.change(
model_holder.update_model_files_gr,
inputs=[model_name],
outputs=[model_path],
)
model_path.change(make_non_interactive, outputs=[tts_button])
refresh_button.click(
model_holder.update_model_names_gr,
outputs=[model_name, model_path, tts_button],
)
load_button.click(
model_holder.load_model_gr,
inputs=[model_name, model_path],
outputs=[style, tts_button, speaker],
)
style_mode.change(
gr_util,
inputs=[style_mode],
outputs=[style, ref_audio_path],
)
return app

View File

@@ -1,7 +1,5 @@
import argparse
import json
import os
import sys
from pathlib import Path
import gradio as gr
@@ -29,8 +27,6 @@ with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f:
# dataset_root = path_config["dataset_root"]
assets_root = path_config["assets_root"]
model_holder = ModelHolder(Path(assets_root), device)
def merge_style(model_name_a, model_name_b, weight, output_name, style_triple_list):
"""
@@ -259,6 +255,7 @@ def simple_tts(model_name, text, style=DEFAULT_STYLE, style_weight=1.0):
def update_two_model_names_dropdown():
model_holder = ModelHolder(Path(assets_root), device)
new_names, new_files, _ = model_holder.update_model_names_gr()
return new_names, new_files, new_names, new_files
@@ -331,12 +328,18 @@ Happy, Surprise, HappySurprise
- 構造上の相性の関係でスタイルベクトルを混ぜる重みは上の話し方と同じ比率で混ぜられます例えば話し方が0のときはモデルAのみしか使われません
"""
def create_merge_app(model_holder: ModelHolder) -> gr.Blocks:
model_names = model_holder.model_names
if len(model_names) == 0:
logger.error(
f"モデルが見つかりませんでした。{assets_root}にモデルを置いてください。"
)
sys.exit(1)
with gr.Blocks() as app:
gr.Markdown(
f"Error: モデルが見つかりませんでした。{assets_root}にモデルを置いてください。"
)
return app
initial_id = 0
initial_model_files = model_holder.model_files_dict[model_names[initial_id]]
@@ -405,7 +408,9 @@ with gr.Blocks(theme=GRADIO_THEME) as app:
)
with gr.Column(variant="panel"):
gr.Markdown("## モデルファイルsafetensorsのマージ")
model_merge_button = gr.Button("モデルファイルのマージ", variant="primary")
model_merge_button = gr.Button(
"モデルファイルのマージ", variant="primary"
)
info_model_merge = gr.Textbox(label="情報")
with gr.Column(variant="panel"):
gr.Markdown(style_merge_md)
@@ -496,22 +501,4 @@ with gr.Blocks(theme=GRADIO_THEME) as app:
outputs=[audio_output],
)
parser = argparse.ArgumentParser()
parser.add_argument(
"--server-name",
type=str,
default=None,
help="Server name for Gradio app",
)
parser.add_argument(
"--no-autolaunch",
action="store_true",
default=False,
help="Do not launch app automatically",
)
parser.add_argument("--share", action="store_true", default=False)
args = parser.parse_args()
app.launch(
inbrowser=not args.no_autolaunch, server_name=args.server_name, share=args.share
)
return app

View File

@@ -1,4 +1,3 @@
import argparse
import json
import os
import shutil
@@ -278,9 +277,7 @@ def save_style_vectors_from_files(
return f"成功!\n{style_vector_path}に保存し{config_path}を更新しました。"
initial_md = f"""
# Style Bert-VITS2 スタイルベクトルの作成
how_to_md = f"""
Style-Bert-VITS2でこまかくスタイルを指定して音声合成するにはモデルごとにスタイルベクトルのファイル`style_vectors.npy`を手動で作成する必要があります
ただし学習の過程で自動的に平均スタイル{DEFAULT_STYLE}のみは作成されるのでそれをそのまま使うこともできますその場合はこのWebUIは使いません
@@ -324,8 +321,11 @@ UMAPの場合はepsは0.3くらい、t-SNEの場合は2.5くらいがいいか
https://ja.wikipedia.org/wiki/DBSCAN
"""
def create_style_vectors_app():
with gr.Blocks(theme=GRADIO_THEME) as app:
gr.Markdown(initial_md)
with gr.Accordion("使い方", open=False):
gr.Markdown(how_to_md)
with gr.Row():
model_name = gr.Textbox(placeholder="your_model_name", label="モデル名")
reduction_method = gr.Radio(
@@ -460,22 +460,4 @@ with gr.Blocks(theme=GRADIO_THEME) as app:
outputs=[info2],
)
parser = argparse.ArgumentParser()
parser.add_argument(
"--server-name",
type=str,
default=None,
help="Server name for Gradio app",
)
parser.add_argument(
"--no-autolaunch",
action="store_true",
default=False,
help="Do not launch app automatically",
)
parser.add_argument("--share", action="store_true", default=False)
args = parser.parse_args()
app.launch(
inbrowser=not args.no_autolaunch, server_name=args.server_name, share=args.share
)
return app

View File

@@ -1,4 +1,3 @@
import argparse
import json
import os
import shutil
@@ -14,7 +13,6 @@ from pathlib import Path
import gradio as gr
import yaml
from style_bert_vits2.constants import GRADIO_THEME, VERSION
from style_bert_vits2.logging import logger
from style_bert_vits2.utils.stdout_wrapper import SAFE_STDOUT
from style_bert_vits2.utils.subprocess import run_script_with_log, second_elem_of
@@ -399,9 +397,7 @@ def run_tensorboard(model_name):
yield gr.Button("Tensorboardを開く")
initial_md = f"""
# Style-Bert-VITS2 ver {VERSION} 学習用WebUI
how_to_md = f"""
## 使い方
- データを準備してモデル名を入力して必要なら設定を調整してから自動前処理を実行ボタンを押してください進捗状況等はターミナルに表示されます
@@ -451,9 +447,11 @@ english_teacher.wav|Mary|EN|How are you? I'm fine, thank you, and you?
日本語話者の単一話者データセットでも構いません
"""
if __name__ == "__main__":
with gr.Blocks(theme=GRADIO_THEME).queue() as app:
gr.Markdown(initial_md)
def create_train_app():
with gr.Blocks().queue() as app:
with gr.Accordion("使い方", open=False):
gr.Markdown(how_to_md)
with gr.Accordion(label="データの前準備", open=False):
gr.Markdown(prepare_md)
model_name = gr.Textbox(label="モデル名")
@@ -794,19 +792,5 @@ if __name__ == "__main__":
outputs=[use_jp_extra_train],
)
parser = argparse.ArgumentParser()
parser.add_argument(
"--server-name",
type=str,
default=None,
help="Server name for Gradio app",
)
parser.add_argument(
"--no-autolaunch",
action="store_true",
default=False,
help="Do not launch app automatically",
)
args = parser.parse_args()
app.launch(inbrowser=not args.no_autolaunch, server_name=args.server_name)
# app.launch(inbrowser=not args.no_autolaunch, server_name=args.server_name)
return app

View File

@@ -1,209 +0,0 @@
import argparse
import os
import gradio as gr
import yaml
from style_bert_vits2.constants import GRADIO_THEME
from style_bert_vits2.logging import logger
from style_bert_vits2.utils.subprocess import run_script_with_log
# Get path settings
with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f:
path_config: dict[str, str] = yaml.safe_load(f.read())
dataset_root = path_config["dataset_root"]
# assets_root = path_config["assets_root"]
def do_slice(
model_name: str,
min_sec: float,
max_sec: float,
min_silence_dur_ms: int,
input_dir: str,
):
if model_name == "":
return "Error: モデル名を入力してください。"
logger.info("Start slicing...")
cmd = [
"slice.py",
"--model_name",
model_name,
"--min_sec",
str(min_sec),
"--max_sec",
str(max_sec),
"--min_silence_dur_ms",
str(min_silence_dur_ms),
]
if input_dir != "":
cmd += ["--input_dir", input_dir]
# onnxの警告が出るので無視する
success, message = run_script_with_log(cmd, ignore_warning=True)
if not success:
return f"Error: {message}"
return "音声のスライスが完了しました。"
def do_transcribe(
model_name, whisper_model, compute_type, language, initial_prompt, device
):
if model_name == "":
return "Error: モデル名を入力してください。"
success, message = run_script_with_log(
[
"transcribe.py",
"--model_name",
model_name,
"--model",
whisper_model,
"--compute_type",
compute_type,
"--device",
device,
"--language",
language,
"--initial_prompt",
f'"{initial_prompt}"',
]
)
if not success:
return f"Error: {message}"
return "音声の文字起こしが完了しました。"
initial_md = """
# 簡易学習用データセット作成ツール
Style-Bert-VITS2の学習用データセットを作成するためのツールです。以下の2つからなります。
- 与えられた音声からちょうどいい長さの発話区間を切り取りスライス
- 音声に対して文字起こし
このうち両方を使ってもよいし、スライスする必要がない場合は後者のみを使ってもよいです。
## 必要なもの
学習したい音声が入ったwavファイルいくつか。
合計時間がある程度はあったほうがいいかも、10分とかでも大丈夫だったとの報告あり。単一ファイルでも良いし複数ファイルでもよい。
## スライス使い方
1. `inputs`フォルダにwavファイルをすべて入れる
2. `モデル名`を入力して、設定を必要なら調整して`音声のスライス`ボタンを押す
3. 出来上がった音声ファイルたちは`Data/{モデル名}/raw`に保存される
## 書き起こし使い方
1. 書き起こしたい音声ファイルのあるフォルダを指定(デフォルトは`Data/{モデル名}/raw`なのでスライス後に行う場合は省略してよい)
2. 設定を必要なら調整してボタンを押す
3. 書き起こしファイルは`Data/{モデル名}/esd.list`に保存される
## 注意
- 長すぎる秒数12-15秒くらいより長いのwavファイルは学習に用いられないようです。また短すぎてもあまりよくない可能性もあります。
- 書き起こしの結果をどれだけ修正すればいいかはデータセットに依存しそうです。
- 手動で書き起こしをいろいろ修正したり結果を細かく確認したい場合は、[Aivis Dataset](https://github.com/litagin02/Aivis-Dataset)もおすすめします。書き起こし部分もかなり工夫されています。ですがファイル数が多い場合などは、このツールで簡易的に切り出してデータセットを作るだけでも十分という気もしています。
"""
with gr.Blocks(theme=GRADIO_THEME) as app:
gr.Markdown(initial_md)
model_name = gr.Textbox(
label="モデル名を入力してください(話者名としても使われます)。"
)
with gr.Accordion("音声のスライス"):
with gr.Row():
with gr.Column():
input_dir = gr.Textbox(
label="入力フォルダ名デフォルトはinputs",
placeholder="inputs",
info="下記フォルダにwavファイルを入れておいてください",
)
min_sec = gr.Slider(
minimum=0,
maximum=10,
value=2,
step=0.5,
label="この秒数未満は切り捨てる",
)
max_sec = gr.Slider(
minimum=0,
maximum=15,
value=12,
step=0.5,
label="この秒数以上は切り捨てる",
)
min_silence_dur_ms = gr.Slider(
minimum=0,
maximum=2000,
value=700,
step=100,
label="無音とみなして区切る最小の無音の長さms",
)
slice_button = gr.Button("スライスを実行")
result1 = gr.Textbox(label="結果")
with gr.Row():
with gr.Column():
whisper_model = gr.Dropdown(
["tiny", "base", "small", "medium", "large", "large-v2", "large-v3"],
label="Whisperモデル",
value="large-v3",
)
compute_type = gr.Dropdown(
[
"int8",
"int8_float32",
"int8_float16",
"int8_bfloat16",
"int16",
"float16",
"bfloat16",
"float32",
],
label="計算精度",
value="bfloat16",
)
device = gr.Radio(["cuda", "cpu"], label="デバイス", value="cuda")
language = gr.Dropdown(["ja", "en", "zh"], value="ja", label="言語")
initial_prompt = gr.Textbox(
label="初期プロンプト",
value="こんにちは。元気、ですかー?ふふっ、私は……ちゃんと元気だよ!",
info="このように書き起こしてほしいという例文(句読点の入れ方・笑い方・固有名詞等)",
)
transcribe_button = gr.Button("音声の文字起こし")
result2 = gr.Textbox(label="結果")
slice_button.click(
do_slice,
inputs=[model_name, min_sec, max_sec, min_silence_dur_ms, input_dir],
outputs=[result1],
)
transcribe_button.click(
do_transcribe,
inputs=[
model_name,
whisper_model,
compute_type,
language,
initial_prompt,
device,
],
outputs=[result2],
)
parser = argparse.ArgumentParser()
parser.add_argument(
"--server-name",
type=str,
default=None,
help="Server name for Gradio app",
)
parser.add_argument(
"--no-autolaunch",
action="store_true",
default=False,
help="Do not launch app automatically",
)
args = parser.parse_args()
app.launch(inbrowser=not args.no_autolaunch, server_name=args.server_name)