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

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

504
webui/merge.py Normal file
View File

@@ -0,0 +1,504 @@
import json
import os
from pathlib import Path
import gradio as gr
import numpy as np
import torch
import yaml
from safetensors import safe_open
from safetensors.torch import save_file
from common.tts_model import Model, ModelHolder
from style_bert_vits2.constants import DEFAULT_STYLE, GRADIO_THEME
from style_bert_vits2.logging import logger
voice_keys = ["dec"]
voice_pitch_keys = ["flow"]
speech_style_keys = ["enc_p"]
tempo_keys = ["sdp", "dp"]
device = "cuda" if torch.cuda.is_available() else "cpu"
# Get path settings
with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f:
path_config: dict[str, str] = yaml.safe_load(f.read())
# dataset_root = path_config["dataset_root"]
assets_root = path_config["assets_root"]
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(assets_root, model_name_a, "style_vectors.npy")
) # (style_num_a, 256)
style_vectors_b = np.load(
os.path.join(assets_root, model_name_b, "style_vectors.npy")
) # (style_num_b, 256)
with open(
os.path.join(assets_root, model_name_a, "config.json"), encoding="utf-8"
) as f:
config_a = json.load(f)
with open(
os.path.join(assets_root, model_name_b, "config.json"), encoding="utf-8"
) 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(assets_root, output_name, "style_vectors.npy")
np.save(output_style_path, new_style_vecs)
new_config = config_a.copy()
new_config["data"]["num_styles"] = len(new_style2id)
new_config["data"]["style2id"] = new_style2id
new_config["model_name"] = output_name
with open(
os.path.join(assets_root, output_name, "config.json"), "w", encoding="utf-8"
) as f:
json.dump(new_config, f, indent=2, ensure_ascii=False)
# recipe.jsonを読み込んで、style_triple_listを追記
info_path = os.path.join(assets_root, output_name, "recipe.json")
if os.path.exists(info_path):
with open(info_path, encoding="utf-8") as f:
info = json.load(f)
else:
info = {}
info["style_triple_list"] = style_triple_list
with open(info_path, "w", encoding="utf-8") as f:
json.dump(info, f, indent=2, ensure_ascii=False)
return output_style_path, list(new_style2id.keys())
def lerp_tensors(t, v0, v1):
return v0 * (1 - t) + v1 * t
def slerp_tensors(t, v0, v1, dot_thres=0.998):
device = v0.device
v0c = v0.cpu().numpy()
v1c = v1.cpu().numpy()
dot = np.sum(v0c * v1c / (np.linalg.norm(v0c) * np.linalg.norm(v1c)))
if abs(dot) > dot_thres:
return lerp_tensors(t, v0, v1)
th0 = np.arccos(dot)
sin_th0 = np.sin(th0)
th_t = th0 * t
return torch.from_numpy(
v0c * np.sin(th0 - th_t) / sin_th0 + v1c * np.sin(th_t) / sin_th0
).to(device)
def merge_models(
model_path_a,
model_path_b,
voice_weight,
voice_pitch_weight,
speech_style_weight,
tempo_weight,
output_name,
use_slerp_instead_of_lerp,
):
"""model 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 voice_pitch_keys]):
weight = voice_pitch_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] = (
slerp_tensors if use_slerp_instead_of_lerp else lerp_tensors
)(weight, model_a_weight[key], model_b_weight[key])
merged_model_path = os.path.join(
assets_root, output_name, f"{output_name}.safetensors"
)
os.makedirs(os.path.dirname(merged_model_path), exist_ok=True)
save_file(merged_model_weight, merged_model_path)
info = {
"model_a": model_path_a,
"model_b": model_path_b,
"voice_weight": voice_weight,
"voice_pitch_weight": voice_pitch_weight,
"speech_style_weight": speech_style_weight,
"tempo_weight": tempo_weight,
}
with open(
os.path.join(assets_root, output_name, "recipe.json"), "w", encoding="utf-8"
) as f:
json.dump(info, f, indent=2, ensure_ascii=False)
return merged_model_path
def merge_models_gr(
model_name_a,
model_path_a,
model_name_b,
model_path_b,
output_name,
voice_weight,
voice_pitch_weight,
speech_style_weight,
tempo_weight,
use_slerp_instead_of_lerp,
):
if output_name == "":
return "Error: 新しいモデル名を入力してください。"
merged_model_path = merge_models(
model_path_a,
model_path_b,
voice_weight,
voice_pitch_weight,
speech_style_weight,
tempo_weight,
output_name,
use_slerp_instead_of_lerp,
)
return f"Success: モデルを{merged_model_path}に保存しました。"
def merge_style_gr(
model_name_a,
model_name_b,
weight,
output_name,
style_triple_list_str: str,
):
if output_name == "":
return "Error: 新しいモデル名を入力してください。", None
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, style_weight=1.0):
model_path = os.path.join(assets_root, model_name, f"{model_name}.safetensors")
config_path = os.path.join(assets_root, model_name, "config.json")
style_vec_path = os.path.join(assets_root, model_name, "style_vectors.npy")
model = Model(Path(model_path), Path(config_path), Path(style_vec_path), device)
return model.infer(text, style=style, style_weight=style_weight)
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
def load_styles_gr(model_name_a, model_name_b):
config_path_a = os.path.join(assets_root, model_name_a, "config.json")
with open(config_path_a, encoding="utf-8") as f:
config_a = json.load(f)
styles_a = list(config_a["data"]["style2id"].keys())
config_path_b = os.path.join(assets_root, model_name_b, "config.json")
with open(config_path_b, encoding="utf-8") 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)),
gr.TextArea(
label="スタイルのマージリスト",
placeholder=f"{DEFAULT_STYLE}, {DEFAULT_STYLE},{DEFAULT_STYLE}\nAngry, Angry, Angry",
value="\n".join(
f"{sty_a}, {sty_b}, {sty_a if sty_a != sty_b else ''}{sty_b}"
for sty_a in styles_a
for sty_b in styles_b
),
),
)
initial_md = """
# Style-Bert-VITS2 モデルマージツール
2つのStyle-Bert-VITS2モデルから、声質・話し方・話す速さを取り替えたり混ぜたりできます。
## 使い方
1. マージしたい2つのモデルを選択してください`model_assets`フォルダの中から選ばれます)。
2. マージ後のモデルの名前を入力してください。
3. マージ後のモデルの声質・話し方・話す速さを調整してください。
4. 「モデルファイルのマージ」ボタンを押してくださいsafetensorsファイルがマージされる
5. スタイルベクトルファイルも生成する必要があるので、指示に従ってマージ方法を入力後、「スタイルのマージ」ボタンを押してください。
以上でマージは完了で、`model_assets/マージ後のモデル名`にマージ後のモデルが保存され、音声合成のときに使えます。
また`model_asses/マージ後のモデル名/recipe.json`には、マージの配合レシピが記録されます(推論にはいらないので配合メモ用です)。
一番下にマージしたモデルによる簡易的な音声合成機能もつけています。
## 注意
1.x系と2.x-JP-Extraのモデルマージは失敗するようです。
"""
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のみしか使われません。
"""
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}にモデルを置いてください。"
)
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]]
with gr.Blocks(theme=GRADIO_THEME) 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,
)
voice_pitch_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,
)
use_slerp_instead_of_lerp = gr.Checkbox(
label="線形補完のかわりに球面線形補完を使う",
value=False,
)
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_style_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_style_button.click(
load_styles_gr,
inputs=[model_name_a, model_name_b],
outputs=[styles_a, styles_b, style_triple_list],
)
model_merge_button.click(
merge_models_gr,
inputs=[
model_name_a,
model_path_a,
model_name_b,
model_path_b,
new_name,
voice_slider,
voice_pitch_slider,
speech_style_slider,
tempo_slider,
use_slerp_instead_of_lerp,
],
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],
)
return app

463
webui/style_vectors.py Normal file
View File

@@ -0,0 +1,463 @@
import json
import os
import shutil
import gradio as gr
import matplotlib.pyplot as plt
import numpy as np
import yaml
from scipy.spatial.distance import pdist, squareform
from sklearn.cluster import DBSCAN, AgglomerativeClustering, KMeans
from sklearn.manifold import TSNE
from umap import UMAP
from config import config
from style_bert_vits2.constants import DEFAULT_STYLE, GRADIO_THEME
from style_bert_vits2.logging import logger
# Get path settings
with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f:
path_config: dict[str, str] = yaml.safe_load(f.read())
dataset_root = path_config["dataset_root"]
# assets_root = path_config["assets_root"]
MAX_CLUSTER_NUM = 10
MAX_AUDIO_NUM = 10
tsne = TSNE(n_components=2, random_state=42, metric="cosine")
umap = UMAP(n_components=2, random_state=42, metric="cosine", n_jobs=1, min_dist=0.0)
wav_files = []
x = np.array([])
x_reduced = None
y_pred = np.array([])
mean = np.array([])
centroids = []
def load(model_name, reduction_method):
global wav_files, x, x_reduced, mean
wavs_dir = os.path.join(dataset_root, model_name, "wavs")
style_vector_files = [
os.path.join(wavs_dir, f) for f in os.listdir(wavs_dir) if f.endswith(".npy")
]
wav_files = [f.replace(".npy", "") for f in style_vector_files]
style_vectors = [np.load(f) for f in style_vector_files]
x = np.array(style_vectors)
mean = np.mean(x, axis=0)
if reduction_method == "t-SNE":
x_reduced = tsne.fit_transform(x)
elif reduction_method == "UMAP":
x_reduced = umap.fit_transform(x)
else:
raise ValueError("Invalid reduction method")
x_reduced = np.asarray(x_reduced)
plt.figure(figsize=(6, 6))
plt.scatter(x_reduced[:, 0], x_reduced[:, 1])
return plt
def do_clustering(n_clusters=4, method="KMeans"):
global centroids, x_reduced, y_pred
if method == "KMeans":
model = KMeans(n_clusters=n_clusters, random_state=42, n_init="auto")
y_pred = model.fit_predict(x)
elif method == "Agglomerative":
model = AgglomerativeClustering(n_clusters=n_clusters)
y_pred = model.fit_predict(x)
elif method == "KMeans after reduction":
assert x_reduced is not None
model = KMeans(n_clusters=n_clusters, random_state=42, n_init="auto")
y_pred = model.fit_predict(x_reduced)
elif method == "Agglomerative after reduction":
assert x_reduced is not None
model = AgglomerativeClustering(n_clusters=n_clusters)
y_pred = model.fit_predict(x_reduced)
else:
raise ValueError("Invalid method")
centroids = []
for i in range(n_clusters):
centroids.append(np.mean(x[y_pred == i], axis=0))
return y_pred, centroids
def do_dbscan(eps=2.5, min_samples=15):
global centroids, x_reduced, y_pred
model = DBSCAN(eps=eps, min_samples=min_samples)
assert x_reduced is not None
y_pred = model.fit_predict(x_reduced)
n_clusters = max(y_pred) + 1
centroids = []
for i in range(n_clusters):
centroids.append(np.mean(x[y_pred == i], axis=0))
return y_pred, centroids
def representative_wav_files(cluster_id, num_files=1):
# y_predの中でcluster_indexに関するメドイドを探す
cluster_indices = np.where(y_pred == cluster_id)[0]
cluster_vectors = x[cluster_indices]
# クラスタ内の全ベクトル間の距離を計算
distances = pdist(cluster_vectors)
distance_matrix = squareform(distances)
# 各ベクトルと他の全ベクトルとの平均距離を計算
mean_distances = distance_matrix.mean(axis=1)
# 平均距離が最も小さい順にnum_files個のインデックスを取得
closest_indices = np.argsort(mean_distances)[:num_files]
return cluster_indices[closest_indices]
def do_dbscan_gradio(eps=2.5, min_samples=15):
global x_reduced, centroids
y_pred, centroids = do_dbscan(eps, min_samples)
assert x_reduced is not None
cmap = plt.get_cmap("tab10")
plt.figure(figsize=(6, 6))
for i in range(max(y_pred) + 1):
plt.scatter(
x_reduced[y_pred == i, 0],
x_reduced[y_pred == i, 1],
color=cmap(i),
label=f"Style {i + 1}",
)
# Noise cluster (-1) is black
plt.scatter(
x_reduced[y_pred == -1, 0],
x_reduced[y_pred == -1, 1],
color="black",
label="Noise",
)
plt.legend()
n_clusters = max(y_pred) + 1
if n_clusters > MAX_CLUSTER_NUM:
# raise ValueError(f"The number of clusters is too large: {n_clusters}")
return [
plt,
gr.Slider(maximum=MAX_CLUSTER_NUM),
f"クラスタ数が多すぎます、パラメータを変えてみてください。: {n_clusters}",
] + [gr.Audio(visible=False)] * MAX_AUDIO_NUM
elif n_clusters == 0:
return [
plt,
gr.Slider(maximum=MAX_CLUSTER_NUM),
"クラスタが数が0です。パラメータを変えてみてください。",
] + [gr.Audio(visible=False)] * MAX_AUDIO_NUM
return [plt, gr.Slider(maximum=n_clusters, value=1), n_clusters] + [
gr.Audio(visible=False)
] * MAX_AUDIO_NUM
def representative_wav_files_gradio(cluster_id, num_files=1):
cluster_id = cluster_id - 1 # UIでは1から始まるので0からにする
closest_indices = representative_wav_files(cluster_id, num_files)
actual_num_files = len(closest_indices) # ファイル数が少ないときのため
return [
gr.Audio(wav_files[i], visible=True, label=wav_files[i])
for i in closest_indices
] + [gr.update(visible=False)] * (MAX_AUDIO_NUM - actual_num_files)
def do_clustering_gradio(n_clusters=4, method="KMeans"):
global x_reduced, centroids
y_pred, centroids = do_clustering(n_clusters, method)
assert x_reduced is not None
cmap = plt.get_cmap("tab10")
plt.figure(figsize=(6, 6))
for i in range(n_clusters):
plt.scatter(
x_reduced[y_pred == i, 0],
x_reduced[y_pred == i, 1],
color=cmap(i),
label=f"Style {i + 1}",
)
plt.legend()
return [plt, gr.Slider(maximum=n_clusters, value=1)] + [
gr.Audio(visible=False)
] * MAX_AUDIO_NUM
def save_style_vectors_from_clustering(model_name, style_names_str: str):
"""centerとcentroidsを保存する"""
result_dir = os.path.join(config.assets_root, model_name)
os.makedirs(result_dir, exist_ok=True)
style_vectors = np.stack([mean] + centroids)
style_vector_path = os.path.join(result_dir, "style_vectors.npy")
if os.path.exists(style_vector_path):
logger.info(f"Backup {style_vector_path} to {style_vector_path}.bak")
shutil.copy(style_vector_path, f"{style_vector_path}.bak")
np.save(style_vector_path, style_vectors)
# config.jsonの更新
config_path = os.path.join(result_dir, "config.json")
if not os.path.exists(config_path):
return f"{config_path}が存在しません。"
style_names = [name.strip() for name in style_names_str.split(",")]
style_name_list = [DEFAULT_STYLE] + style_names
if len(style_name_list) != len(centroids) + 1:
return f"スタイルの数が合いません。`,`で正しく{len(centroids)}個に区切られているか確認してください: {style_names_str}"
if len(set(style_names)) != len(style_names):
return "スタイル名が重複しています。"
logger.info(f"Backup {config_path} to {config_path}.bak")
shutil.copy(config_path, f"{config_path}.bak")
with open(config_path, "r", encoding="utf-8") as f:
json_dict = json.load(f)
json_dict["data"]["num_styles"] = len(style_name_list)
style_dict = {name: i for i, name in enumerate(style_name_list)}
json_dict["data"]["style2id"] = style_dict
with open(config_path, "w", encoding="utf-8") as f:
json.dump(json_dict, f, indent=2, ensure_ascii=False)
return f"成功!\n{style_vector_path}に保存し{config_path}を更新しました。"
def save_style_vectors_from_files(
model_name, audio_files_str: str, style_names_str: str
):
"""音声ファイルからスタイルベクトルを作成して保存する"""
global mean
if len(x) == 0:
return "Error: スタイルベクトルを読み込んでください。"
mean = np.mean(x, axis=0)
result_dir = os.path.join(config.assets_root, model_name)
os.makedirs(result_dir, exist_ok=True)
audio_files = [name.strip() for name in audio_files_str.split(",")]
style_names = [name.strip() for name in style_names_str.split(",")]
if len(audio_files) != len(style_names):
return f"音声ファイルとスタイル名の数が合いません。`,`で正しく{len(style_names)}個に区切られているか確認してください: {audio_files_str}{style_names_str}"
style_name_list = [DEFAULT_STYLE] + style_names
if len(set(style_names)) != len(style_names):
return "スタイル名が重複しています。"
style_vectors = [mean]
wavs_dir = os.path.join(dataset_root, model_name, "wavs")
for audio_file in audio_files:
path = os.path.join(wavs_dir, audio_file)
if not os.path.exists(path):
return f"{path}が存在しません。"
style_vectors.append(np.load(f"{path}.npy"))
style_vectors = np.stack(style_vectors)
assert len(style_name_list) == len(style_vectors)
style_vector_path = os.path.join(result_dir, "style_vectors.npy")
if os.path.exists(style_vector_path):
logger.info(f"Backup {style_vector_path} to {style_vector_path}.bak")
shutil.copy(style_vector_path, f"{style_vector_path}.bak")
np.save(style_vector_path, style_vectors)
# config.jsonの更新
config_path = os.path.join(result_dir, "config.json")
if not os.path.exists(config_path):
return f"{config_path}が存在しません。"
logger.info(f"Backup {config_path} to {config_path}.bak")
shutil.copy(config_path, f"{config_path}.bak")
with open(config_path, "r", encoding="utf-8") as f:
json_dict = json.load(f)
json_dict["data"]["num_styles"] = len(style_name_list)
style_dict = {name: i for i, name in enumerate(style_name_list)}
json_dict["data"]["style2id"] = style_dict
with open(config_path, "w", encoding="utf-8") as f:
json.dump(json_dict, f, indent=2, ensure_ascii=False)
return f"成功!\n{style_vector_path}に保存し{config_path}を更新しました。"
how_to_md = f"""
Style-Bert-VITS2でこまかくスタイルを指定して音声合成するには、モデルごとにスタイルベクトルのファイル`style_vectors.npy`を手動で作成する必要があります。
ただし、学習の過程で自動的に平均スタイル「{DEFAULT_STYLE}」のみは作成されるので、それをそのまま使うこともできますその場合はこのWebUIは使いません
このプロセスは学習とは全く関係がないので、何回でも独立して繰り返して試せます。また学習中にもたぶん軽いので動くはずです。
## 方法
- 方法1: 音声ファイルを自動でスタイル別に分け、その各スタイルの平均を取って保存
- 方法2: スタイルを代表する音声ファイルを手動で選んで、その音声のスタイルベクトルを保存
- 方法3: 自分でもっと頑張ってこだわって作るJVNVコーパスなど、もともとスタイルラベル等が利用可能な場合はこれがよいかも
"""
method1 = f"""
学習の時に取り出したスタイルベクトルを読み込んで、可視化を見ながらスタイルを分けていきます。
手順:
1. 図を眺める
2. スタイル数を決める(平均スタイルを除く)
3. スタイル分けを行って結果を確認
4. スタイルの名前を決めて保存
詳細: スタイルベクトル(256次元)たちを適当なアルゴリズムでクラスタリングして、各クラスタの中心のベクトル(と全体の平均ベクトル)を保存します。
平均スタイル({DEFAULT_STYLE})は自動的に保存されます。
"""
dbscan_md = """
DBSCANという方法でスタイル分けを行います。
こちらの方が方法1よりも特徴がはっきり出るもののみを取り出せ、よいスタイルベクトルが作れるかもしれません。
ただし事前にスタイル数は指定できません。
パラメータ:
- eps: この値より近い点同士をどんどん繋げて同じスタイル分類とする。小さいほどスタイル数が増え、大きいほどスタイル数が減る傾向。
- min_samples: ある点をスタイルの核となる点とみなすために必要な近傍の点の数。小さいほどスタイル数が増え、大きいほどスタイル数が減る傾向。
UMAPの場合はepsは0.3くらい、t-SNEの場合は2.5くらいがいいかもしれません。min_samplesはデータ数に依存するのでいろいろ試してみてください。
詳細:
https://ja.wikipedia.org/wiki/DBSCAN
"""
def create_style_vectors_app():
with gr.Blocks(theme=GRADIO_THEME) as app:
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(
choices=["UMAP", "t-SNE"],
label="次元削減方法",
info="v 1.3以前はt-SNEでしたがUMAPのほうがよい可能性もあります。",
value="UMAP",
)
load_button = gr.Button("スタイルベクトルを読み込む", variant="primary")
output = gr.Plot(label="音声スタイルの可視化")
load_button.click(load, inputs=[model_name, reduction_method], outputs=[output])
with gr.Tab("方法1: スタイル分けを自動で行う"):
with gr.Tab("スタイル分け1"):
n_clusters = gr.Slider(
minimum=2,
maximum=10,
step=1,
value=4,
label="作るスタイルの数(平均スタイルを除く)",
info="上の図を見ながらスタイルの数を試行錯誤してください。",
)
c_method = gr.Radio(
choices=[
"Agglomerative after reduction",
"KMeans after reduction",
"Agglomerative",
"KMeans",
],
label="アルゴリズム",
info="分類する(クラスタリング)アルゴリズムを選択します。いろいろ試してみてください。",
value="Agglomerative after reduction",
)
c_button = gr.Button("スタイル分けを実行")
with gr.Tab("スタイル分け2: DBSCAN"):
gr.Markdown(dbscan_md)
eps = gr.Slider(
minimum=0.1,
maximum=10,
step=0.01,
value=0.3,
label="eps",
)
min_samples = gr.Slider(
minimum=1,
maximum=50,
step=1,
value=15,
label="min_samples",
)
with gr.Row():
dbscan_button = gr.Button("スタイル分けを実行")
num_styles_result = gr.Textbox(label="スタイル数")
gr.Markdown("スタイル分けの結果")
gr.Markdown(
"注意: もともと256次元なものをを2次元に落としているので、正確なベクトルの位置関係ではありません。"
)
with gr.Row():
gr_plot = gr.Plot()
with gr.Column():
with gr.Row():
cluster_index = gr.Slider(
minimum=1,
maximum=MAX_CLUSTER_NUM,
step=1,
value=1,
label="スタイル番号",
info="選択したスタイルの代表音声を表示します。",
)
num_files = gr.Slider(
minimum=1,
maximum=MAX_AUDIO_NUM,
step=1,
value=5,
label="代表音声の数をいくつ表示するか",
)
get_audios_button = gr.Button("代表音声を取得")
with gr.Row():
audio_list = []
for i in range(MAX_AUDIO_NUM):
audio_list.append(gr.Audio(visible=False, show_label=True))
c_button.click(
do_clustering_gradio,
inputs=[n_clusters, c_method],
outputs=[gr_plot, cluster_index] + audio_list,
)
dbscan_button.click(
do_dbscan_gradio,
inputs=[eps, min_samples],
outputs=[gr_plot, cluster_index, num_styles_result] + audio_list,
)
get_audios_button.click(
representative_wav_files_gradio,
inputs=[cluster_index, num_files],
outputs=audio_list,
)
gr.Markdown("結果が良さそうなら、これを保存します。")
style_names = gr.Textbox(
"Angry, Sad, Happy",
label="スタイルの名前",
info=f"スタイルの名前を`,`で区切って入力してください(日本語可)。例: `Angry, Sad, Happy`や`怒り, 悲しみ, 喜び`など。平均音声は{DEFAULT_STYLE}として自動的に保存されます。",
)
with gr.Row():
save_button1 = gr.Button("スタイルベクトルを保存", variant="primary")
info2 = gr.Textbox(label="保存結果")
save_button1.click(
save_style_vectors_from_clustering,
inputs=[model_name, style_names],
outputs=[info2],
)
with gr.Tab("方法2: 手動でスタイルを選ぶ"):
gr.Markdown(
"下のテキスト欄に、各スタイルの代表音声のファイル名を`,`区切りで、その横に対応するスタイル名を`,`区切りで入力してください。"
)
gr.Markdown("例: `angry.wav, sad.wav, happy.wav`と`Angry, Sad, Happy`")
gr.Markdown(
f"注意: {DEFAULT_STYLE}スタイルは自動的に保存されます、手動では{DEFAULT_STYLE}という名前のスタイルは指定しないでください。"
)
with gr.Row():
audio_files_text = gr.Textbox(
label="音声ファイル名", placeholder="angry.wav, sad.wav, happy.wav"
)
style_names_text = gr.Textbox(
label="スタイル名", placeholder="Angry, Sad, Happy"
)
with gr.Row():
save_button2 = gr.Button("スタイルベクトルを保存", variant="primary")
info2 = gr.Textbox(label="保存結果")
save_button2.click(
save_style_vectors_from_files,
inputs=[model_name, audio_files_text, style_names_text],
outputs=[info2],
)
return app

796
webui/train.py Normal file
View File

@@ -0,0 +1,796 @@
import json
import os
import shutil
import socket
import subprocess
import sys
import time
import webbrowser
from datetime import datetime
from multiprocessing import cpu_count
from pathlib import Path
import gradio as gr
import yaml
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
logger_handler = None
tensorboard_executed = False
# Get path settings
with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f:
path_config: dict[str, str] = yaml.safe_load(f.read())
dataset_root = path_config["dataset_root"]
# assets_root = path_config["assets_root"]
def get_path(model_name):
assert model_name != "", "モデル名は空にできません"
dataset_path = os.path.join(dataset_root, model_name)
lbl_path = os.path.join(dataset_path, "esd.list")
train_path = os.path.join(dataset_path, "train.list")
val_path = os.path.join(dataset_path, "val.list")
config_path = os.path.join(dataset_path, "config.json")
return dataset_path, lbl_path, train_path, val_path, config_path
def initialize(
model_name,
batch_size,
epochs,
save_every_steps,
freeze_EN_bert,
freeze_JP_bert,
freeze_ZH_bert,
freeze_style,
freeze_decoder,
use_jp_extra,
log_interval,
):
global logger_handler
dataset_path, _, train_path, val_path, config_path = get_path(model_name)
# 前処理のログをファイルに保存する
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
file_name = f"preprocess_{timestamp}.log"
if logger_handler is not None:
logger.remove(logger_handler)
logger_handler = logger.add(os.path.join(dataset_path, file_name))
logger.info(
f"Step 1: start initialization...\nmodel_name: {model_name}, batch_size: {batch_size}, epochs: {epochs}, save_every_steps: {save_every_steps}, freeze_ZH_bert: {freeze_ZH_bert}, freeze_JP_bert: {freeze_JP_bert}, freeze_EN_bert: {freeze_EN_bert}, freeze_style: {freeze_style}, freeze_decoder: {freeze_decoder}, use_jp_extra: {use_jp_extra}"
)
default_config_path = (
"configs/config.json" if not use_jp_extra else "configs/configs_jp_extra.json"
)
with open(default_config_path, "r", encoding="utf-8") as f:
config = json.load(f)
config["model_name"] = model_name
config["data"]["training_files"] = train_path
config["data"]["validation_files"] = val_path
config["train"]["batch_size"] = batch_size
config["train"]["epochs"] = epochs
config["train"]["eval_interval"] = save_every_steps
config["train"]["log_interval"] = log_interval
config["train"]["freeze_EN_bert"] = freeze_EN_bert
config["train"]["freeze_JP_bert"] = freeze_JP_bert
config["train"]["freeze_ZH_bert"] = freeze_ZH_bert
config["train"]["freeze_style"] = freeze_style
config["train"]["freeze_decoder"] = freeze_decoder
config["train"]["bf16_run"] = False # デフォルトでFalseのはずだが念のため
model_path = os.path.join(dataset_path, "models")
if os.path.exists(model_path):
logger.warning(
f"Step 1: {model_path} already exists, so copy it to backup to {model_path}_backup"
)
shutil.copytree(
src=model_path,
dst=os.path.join(dataset_path, "models_backup"),
dirs_exist_ok=True,
)
shutil.rmtree(model_path)
pretrained_dir = "pretrained" if not use_jp_extra else "pretrained_jp_extra"
try:
shutil.copytree(
src=pretrained_dir,
dst=model_path,
)
except FileNotFoundError:
logger.error(f"Step 1: {pretrained_dir} folder not found.")
return False, f"Step 1, Error: {pretrained_dir}フォルダが見つかりません。"
with open(config_path, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
if not os.path.exists("config.yml"):
shutil.copy(src="default_config.yml", dst="config.yml")
# yml_data = safe_load(open("config.yml", "r", encoding="utf-8"))
with open("config.yml", "r", encoding="utf-8") as f:
yml_data = yaml.safe_load(f)
yml_data["model_name"] = model_name
yml_data["dataset_path"] = dataset_path
with open("config.yml", "w", encoding="utf-8") as f:
yaml.dump(yml_data, f, allow_unicode=True)
logger.success("Step 1: initialization finished.")
return True, "Step 1, Success: 初期設定が完了しました"
def resample(model_name, normalize, trim, num_processes):
logger.info("Step 2: start resampling...")
dataset_path, _, _, _, _ = get_path(model_name)
in_dir = os.path.join(dataset_path, "raw")
out_dir = os.path.join(dataset_path, "wavs")
cmd = [
"resample.py",
"--in_dir",
in_dir,
"--out_dir",
out_dir,
"--num_processes",
str(num_processes),
"--sr",
"44100",
]
if normalize:
cmd.append("--normalize")
if trim:
cmd.append("--trim")
success, message = run_script_with_log(cmd)
if not success:
logger.error("Step 2: resampling failed.")
return False, f"Step 2, Error: 音声ファイルの前処理に失敗しました:\n{message}"
elif message:
logger.warning("Step 2: resampling finished with stderr.")
return True, f"Step 2, Success: 音声ファイルの前処理が完了しました:\n{message}"
logger.success("Step 2: resampling finished.")
return True, "Step 2, Success: 音声ファイルの前処理が完了しました"
def preprocess_text(model_name, use_jp_extra, val_per_lang, yomi_error):
logger.info("Step 3: start preprocessing text...")
dataset_path, lbl_path, train_path, val_path, config_path = get_path(model_name)
try:
lines = open(lbl_path, "r", encoding="utf-8").readlines()
except FileNotFoundError:
logger.error(f"Step 3: {lbl_path} not found.")
return False, f"Step 3, Error: 書き起こしファイル {lbl_path} が見つかりません。"
new_lines = []
for line in lines:
if len(line.strip().split("|")) != 4:
logger.error(f"Step 3: {lbl_path} has invalid format at line:\n{line}")
return (
False,
f"Step 3, Error: 書き起こしファイル次の行の形式が不正です:\n{line}",
)
path, spk, language, text = line.strip().split("|")
# pathをファイル名だけ取り出して正しいパスに変更
path = Path(dataset_path) / "wavs" / Path(path).name
new_lines.append(f"{path}|{spk}|{language}|{text}\n")
with open(lbl_path, "w", encoding="utf-8") as f:
f.writelines(new_lines)
cmd = [
"preprocess_text.py",
"--config-path",
config_path,
"--transcription-path",
lbl_path,
"--train-path",
train_path,
"--val-path",
val_path,
"--val-per-lang",
str(val_per_lang),
"--yomi_error",
yomi_error,
]
if use_jp_extra:
cmd.append("--use_jp_extra")
success, message = run_script_with_log(cmd)
if not success:
logger.error("Step 3: preprocessing text failed.")
return (
False,
f"Step 3, Error: 書き起こしファイルの前処理に失敗しました:\n{message}",
)
elif message:
logger.warning("Step 3: preprocessing text finished with stderr.")
return (
True,
f"Step 3, Success: 書き起こしファイルの前処理が完了しました:\n{message}",
)
logger.success("Step 3: preprocessing text finished.")
return True, "Step 3, Success: 書き起こしファイルの前処理が完了しました"
def bert_gen(model_name):
logger.info("Step 4: start bert_gen...")
_, _, _, _, config_path = get_path(model_name)
success, message = run_script_with_log(
[
"bert_gen.py",
"--config",
config_path,
# "--num_processes", # bert_genは重いのでプロセス数いじらない
# str(num_processes),
]
)
if not success:
logger.error("Step 4: bert_gen failed.")
return False, f"Step 4, Error: BERT特徴ファイルの生成に失敗しました:\n{message}"
elif message:
logger.warning("Step 4: bert_gen finished with stderr.")
return (
True,
f"Step 4, Success: BERT特徴ファイルの生成が完了しました:\n{message}",
)
logger.success("Step 4: bert_gen finished.")
return True, "Step 4, Success: BERT特徴ファイルの生成が完了しました"
def style_gen(model_name, num_processes):
logger.info("Step 5: start style_gen...")
_, _, _, _, config_path = get_path(model_name)
success, message = run_script_with_log(
[
"style_gen.py",
"--config",
config_path,
"--num_processes",
str(num_processes),
]
)
if not success:
logger.error("Step 5: style_gen failed.")
return (
False,
f"Step 5, Error: スタイル特徴ファイルの生成に失敗しました:\n{message}",
)
elif message:
logger.warning("Step 5: style_gen finished with stderr.")
return (
True,
f"Step 5, Success: スタイル特徴ファイルの生成が完了しました:\n{message}",
)
logger.success("Step 5: style_gen finished.")
return True, "Step 5, Success: スタイル特徴ファイルの生成が完了しました"
def preprocess_all(
model_name,
batch_size,
epochs,
save_every_steps,
num_processes,
normalize,
trim,
freeze_EN_bert,
freeze_JP_bert,
freeze_ZH_bert,
freeze_style,
freeze_decoder,
use_jp_extra,
val_per_lang,
log_interval,
yomi_error,
):
if model_name == "":
return False, "Error: モデル名を入力してください"
success, message = initialize(
model_name=model_name,
batch_size=batch_size,
epochs=epochs,
save_every_steps=save_every_steps,
freeze_EN_bert=freeze_EN_bert,
freeze_JP_bert=freeze_JP_bert,
freeze_ZH_bert=freeze_ZH_bert,
freeze_style=freeze_style,
freeze_decoder=freeze_decoder,
use_jp_extra=use_jp_extra,
log_interval=log_interval,
)
if not success:
return False, message
success, message = resample(
model_name=model_name,
normalize=normalize,
trim=trim,
num_processes=num_processes,
)
if not success:
return False, message
success, message = preprocess_text(
model_name=model_name,
use_jp_extra=use_jp_extra,
val_per_lang=val_per_lang,
yomi_error=yomi_error,
)
if not success:
return False, message
success, message = bert_gen(
model_name=model_name
) # bert_genは重いのでプロセス数いじらない
if not success:
return False, message
success, message = style_gen(model_name=model_name, num_processes=num_processes)
if not success:
return False, message
logger.success("Success: All preprocess finished!")
return (
True,
"Success: 全ての前処理が完了しました。ターミナルを確認しておかしいところがないか確認するのをおすすめします。",
)
def train(model_name, skip_style=False, use_jp_extra=True, speedup=False):
dataset_path, _, _, _, config_path = get_path(model_name)
# 学習再開の場合は念のためconfig.ymlの名前等を更新
with open("config.yml", "r", encoding="utf-8") as f:
yml_data = yaml.safe_load(f)
yml_data["model_name"] = model_name
yml_data["dataset_path"] = dataset_path
with open("config.yml", "w", encoding="utf-8") as f:
yaml.dump(yml_data, f, allow_unicode=True)
train_py = "train_ms.py" if not use_jp_extra else "train_ms_jp_extra.py"
cmd = [train_py, "--config", config_path, "--model", dataset_path]
if skip_style:
cmd.append("--skip_default_style")
if speedup:
cmd.append("--speedup")
success, message = run_script_with_log(cmd, ignore_warning=True)
if not success:
logger.error("Train failed.")
return False, f"Error: 学習に失敗しました:\n{message}"
elif message:
logger.warning("Train finished with stderr.")
return True, f"Success: 学習が完了しました:\n{message}"
logger.success("Train finished.")
return True, "Success: 学習が完了しました"
def wait_for_tensorboard(port=6006, timeout=10):
start_time = time.time()
while True:
try:
with socket.create_connection(("localhost", port), timeout=1):
return True # ポートが開いている場合
except OSError:
pass # ポートがまだ開いていない場合
if time.time() - start_time > timeout:
return False # タイムアウト
time.sleep(0.1)
def run_tensorboard(model_name):
global tensorboard_executed
if not tensorboard_executed:
python = sys.executable
tensorboard_cmd = [
python,
"-m",
"tensorboard.main",
"--logdir",
f"Data/{model_name}/models",
]
subprocess.Popen(
tensorboard_cmd,
stdout=SAFE_STDOUT, # type: ignore
stderr=SAFE_STDOUT, # type: ignore
)
yield gr.Button("起動中…")
if wait_for_tensorboard():
tensorboard_executed = True
else:
logger.error("Tensorboard did not start in the expected time.")
webbrowser.open("http://localhost:6006")
yield gr.Button("Tensorboardを開く")
how_to_md = f"""
## 使い方
- データを準備して、モデル名を入力して、必要なら設定を調整してから、「自動前処理を実行」ボタンを押してください。進捗状況等はターミナルに表示されます。
- 各ステップごとに実行する場合は「手動前処理」を使ってください(基本的には自動でいいはず)。
- 前処理が終わったら、「学習を開始する」ボタンを押すと学習が開始されます。
- 途中から学習を再開する場合は、モデル名を入力してから「学習を開始する」を押せばよいです。
注意: 標準スタイル以外のスタイルを音声合成で使うには、スタイルベクトルファイル`style_vectors.npy`を作る必要があります。これは、`Style.bat`を実行してそこで作成してください。
動作は軽いはずなので、学習中でも実行でき、何度でも繰り返して試せます。
## JP-Extra版について
元とするモデル構造として [Bert-VITS2 Japanese-Extra](https://github.com/fishaudio/Bert-VITS2/releases/tag/JP-Exta) を使うことができます。
日本語のアクセントやイントネーションや自然性が上がる傾向にありますが、英語と中国語は話せなくなります。
"""
prepare_md = """
まず音声データwavファイルで1ファイルが2-12秒程度の、長すぎず短すぎない発話のものをいくつかと、書き起こしテキストを用意してください。
それを次のように配置します。
```
├── Data
│ ├── {モデルの名前}
│ │ ├── esd.list
│ │ ├── raw
│ │ │ ├── ****.wav
│ │ │ ├── ****.wav
│ │ │ ├── ...
```
wavファイル名やモデルの名前は空白を含まない半角で、wavファイルの拡張子は小文字`.wav`である必要があります。
`raw` フォルダにはすべてのwavファイルを入れ、`esd.list` ファイルには、以下のフォーマットで各wavファイルの情報を記述してください。
```
****.wav|{話者名}|{言語ID、ZHかJPかEN}|{書き起こしテキスト}
```
例:
```
wav_number1.wav|hanako|JP|こんにちは、聞こえて、いますか?
wav_next.wav|taro|JP|はい、聞こえています……。
english_teacher.wav|Mary|EN|How are you? I'm fine, thank you, and you?
...
```
日本語話者の単一話者データセットでも構いません。
"""
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="モデル名")
gr.Markdown("### 自動前処理")
with gr.Row(variant="panel"):
with gr.Column():
use_jp_extra = gr.Checkbox(
label="JP-Extra版を使う日本語の性能が上がるが英語と中国語は話せなくなる",
value=True,
)
batch_size = gr.Slider(
label="バッチサイズ",
info="学習速度が遅い場合は小さくして試し、VRAMに余裕があれば大きくしてください。JP-Extra版でのVRAM使用量目安: 1: 6GB, 2: 8GB, 3: 10GB, 4: 12GB",
value=2,
minimum=1,
maximum=64,
step=1,
)
epochs = gr.Slider(
label="エポック数",
info="100もあれば十分そうだけどもっと回すと質が上がるかもしれない",
value=100,
minimum=10,
maximum=1000,
step=10,
)
save_every_steps = gr.Slider(
label="何ステップごとに結果を保存するか",
info="エポック数とは違うことに注意",
value=1000,
minimum=100,
maximum=10000,
step=100,
)
normalize = gr.Checkbox(
label="音声の音量を正規化する(音量の大小が揃っていない場合など)",
value=False,
)
trim = gr.Checkbox(
label="音声の最初と最後の無音を取り除く",
value=False,
)
yomi_error = gr.Radio(
label="書き起こしが読めないファイルの扱い",
choices=[
("エラー出たらテキスト前処理が終わった時点で中断", "raise"),
("読めないファイルは使わず続行", "skip"),
("読めないファイルも無理やり読んで学習に使う", "use"),
],
value="raise",
)
with gr.Accordion("詳細設定", open=False):
num_processes = gr.Slider(
label="プロセス数",
info="前処理時の並列処理プロセス数、前処理でフリーズしたら下げてください",
value=cpu_count() // 2,
minimum=1,
maximum=cpu_count(),
step=1,
)
val_per_lang = gr.Slider(
label="検証データ数",
info="学習には使われず、tensorboardで元音声と合成音声を比較するためのもの",
value=0,
minimum=0,
maximum=100,
step=1,
)
log_interval = gr.Slider(
label="Tensorboardのログ出力間隔",
info="Tensorboardで詳しく見たい人は小さめにしてください",
value=200,
minimum=10,
maximum=1000,
step=10,
)
gr.Markdown("学習時に特定の部分を凍結させるかどうか")
freeze_EN_bert = gr.Checkbox(
label="英語bert部分を凍結",
value=False,
)
freeze_JP_bert = gr.Checkbox(
label="日本語bert部分を凍結",
value=False,
)
freeze_ZH_bert = gr.Checkbox(
label="中国語bert部分を凍結",
value=False,
)
freeze_style = gr.Checkbox(
label="スタイル部分を凍結",
value=False,
)
freeze_decoder = gr.Checkbox(
label="デコーダ部分を凍結",
value=False,
)
with gr.Column():
preprocess_button = gr.Button(
value="自動前処理を実行", variant="primary"
)
info_all = gr.Textbox(label="状況")
with gr.Accordion(open=False, label="手動前処理"):
with gr.Row(variant="panel"):
with gr.Column():
gr.Markdown(value="#### Step 1: 設定ファイルの生成")
use_jp_extra_manual = gr.Checkbox(
label="JP-Extra版を使う",
value=True,
)
batch_size_manual = gr.Slider(
label="バッチサイズ",
value=2,
minimum=1,
maximum=64,
step=1,
)
epochs_manual = gr.Slider(
label="エポック数",
value=100,
minimum=1,
maximum=1000,
step=1,
)
save_every_steps_manual = gr.Slider(
label="何ステップごとに結果を保存するか",
value=1000,
minimum=100,
maximum=10000,
step=100,
)
log_interval_manual = gr.Slider(
label="Tensorboardのログ出力間隔",
value=200,
minimum=10,
maximum=1000,
step=10,
)
freeze_EN_bert_manual = gr.Checkbox(
label="英語bert部分を凍結",
value=False,
)
freeze_JP_bert_manual = gr.Checkbox(
label="日本語bert部分を凍結",
value=False,
)
freeze_ZH_bert_manual = gr.Checkbox(
label="中国語bert部分を凍結",
value=False,
)
freeze_style_manual = gr.Checkbox(
label="スタイル部分を凍結",
value=False,
)
freeze_decoder_manual = gr.Checkbox(
label="デコーダ部分を凍結",
value=False,
)
with gr.Column():
generate_config_btn = gr.Button(value="実行", variant="primary")
info_init = gr.Textbox(label="状況")
with gr.Row(variant="panel"):
with gr.Column():
gr.Markdown(value="#### Step 2: 音声ファイルの前処理")
num_processes_resample = gr.Slider(
label="プロセス数",
value=cpu_count() // 2,
minimum=1,
maximum=cpu_count(),
step=1,
)
normalize_resample = gr.Checkbox(
label="音声の音量を正規化する",
value=False,
)
trim_resample = gr.Checkbox(
label="音声の最初と最後の無音を取り除く",
value=False,
)
with gr.Column():
resample_btn = gr.Button(value="実行", variant="primary")
info_resample = gr.Textbox(label="状況")
with gr.Row(variant="panel"):
with gr.Column():
gr.Markdown(value="#### Step 3: 書き起こしファイルの前処理")
val_per_lang_manual = gr.Slider(
label="検証データ数",
value=0,
minimum=0,
maximum=100,
step=1,
)
yomi_error_manual = gr.Radio(
label="書き起こしが読めないファイルの扱い",
choices=[
("エラー出たらテキスト前処理が終わった時点で中断", "raise"),
("読めないファイルは使わず続行", "skip"),
("読めないファイルも無理やり読んで学習に使う", "use"),
],
value="raise",
)
with gr.Column():
preprocess_text_btn = gr.Button(value="実行", variant="primary")
info_preprocess_text = gr.Textbox(label="状況")
with gr.Row(variant="panel"):
with gr.Column():
gr.Markdown(value="#### Step 4: BERT特徴ファイルの生成")
with gr.Column():
bert_gen_btn = gr.Button(value="実行", variant="primary")
info_bert = gr.Textbox(label="状況")
with gr.Row(variant="panel"):
with gr.Column():
gr.Markdown(value="#### Step 5: スタイル特徴ファイルの生成")
num_processes_style = gr.Slider(
label="プロセス数",
value=cpu_count() // 2,
minimum=1,
maximum=cpu_count(),
step=1,
)
with gr.Column():
style_gen_btn = gr.Button(value="実行", variant="primary")
info_style = gr.Textbox(label="状況")
gr.Markdown("## 学習")
with gr.Row():
skip_style = gr.Checkbox(
label="スタイルファイルの生成をスキップする",
info="学習再開の場合の場合はチェックしてください",
value=False,
)
use_jp_extra_train = gr.Checkbox(
label="JP-Extra版を使う",
value=True,
)
speedup = gr.Checkbox(
label="ログ等をスキップして学習を高速化する",
value=False,
visible=False, # Experimental
)
train_btn = gr.Button(value="学習を開始する", variant="primary")
tensorboard_btn = gr.Button(value="Tensorboardを開く")
gr.Markdown(
"進捗はターミナルで確認してください。結果は指定したステップごとに随時保存されており、また学習を途中から再開することもできます。学習を終了するには単にターミナルを終了してください。"
)
info_train = gr.Textbox(label="状況")
preprocess_button.click(
second_elem_of(preprocess_all),
inputs=[
model_name,
batch_size,
epochs,
save_every_steps,
num_processes,
normalize,
trim,
freeze_EN_bert,
freeze_JP_bert,
freeze_ZH_bert,
freeze_style,
freeze_decoder,
use_jp_extra,
val_per_lang,
log_interval,
yomi_error,
],
outputs=[info_all],
)
# Manual preprocess
generate_config_btn.click(
second_elem_of(initialize),
inputs=[
model_name,
batch_size_manual,
epochs_manual,
save_every_steps_manual,
freeze_EN_bert_manual,
freeze_JP_bert_manual,
freeze_ZH_bert_manual,
freeze_style_manual,
freeze_decoder_manual,
use_jp_extra_manual,
log_interval_manual,
],
outputs=[info_init],
)
resample_btn.click(
second_elem_of(resample),
inputs=[
model_name,
normalize_resample,
trim_resample,
num_processes_resample,
],
outputs=[info_resample],
)
preprocess_text_btn.click(
second_elem_of(preprocess_text),
inputs=[
model_name,
use_jp_extra_manual,
val_per_lang_manual,
yomi_error_manual,
],
outputs=[info_preprocess_text],
)
bert_gen_btn.click(
second_elem_of(bert_gen),
inputs=[model_name],
outputs=[info_bert],
)
style_gen_btn.click(
second_elem_of(style_gen),
inputs=[model_name, num_processes_style],
outputs=[info_style],
)
# Train
train_btn.click(
second_elem_of(train),
inputs=[model_name, skip_style, use_jp_extra_train, speedup],
outputs=[info_train],
)
tensorboard_btn.click(
run_tensorboard, inputs=[model_name], outputs=[tensorboard_btn]
)
use_jp_extra.change(
lambda x: gr.Checkbox(value=x),
inputs=[use_jp_extra],
outputs=[use_jp_extra_train],
)
use_jp_extra_manual.change(
lambda x: gr.Checkbox(value=x),
inputs=[use_jp_extra_manual],
outputs=[use_jp_extra_train],
)
# app.launch(inbrowser=not args.no_autolaunch, server_name=args.server_name)
return app