wip
This commit is contained in:
16
webui/__init__.py
Normal file
16
webui/__init__.py
Normal 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
|
||||
220
webui/dataset.py
Normal file
220
webui/dataset.py
Normal file
@@ -0,0 +1,220 @@
|
||||
import argparse
|
||||
import os
|
||||
|
||||
import gradio as gr
|
||||
import yaml
|
||||
|
||||
from common.constants import GRADIO_THEME
|
||||
from common.log import logger
|
||||
from common.subprocess_utils 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)もおすすめします。書き起こし部分もかなり工夫されています。ですがファイル数が多い場合などは、このツールで簡易的に切り出してデータセットを作るだけでも十分という気もしています。
|
||||
"""
|
||||
|
||||
|
||||
def create_dataset_app():
|
||||
|
||||
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)
|
||||
return app
|
||||
504
webui/inference.py
Normal file
504
webui/inference.py
Normal file
@@ -0,0 +1,504 @@
|
||||
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 common.constants import (
|
||||
DEFAULT_ASSIST_TEXT_WEIGHT,
|
||||
DEFAULT_LENGTH,
|
||||
DEFAULT_LINE_SPLIT,
|
||||
DEFAULT_NOISE,
|
||||
DEFAULT_NOISEW,
|
||||
DEFAULT_SDP_RATIO,
|
||||
DEFAULT_SPLIT_INTERVAL,
|
||||
DEFAULT_STYLE,
|
||||
DEFAULT_STYLE_WEIGHT,
|
||||
GRADIO_THEME,
|
||||
LATEST_VERSION,
|
||||
Languages,
|
||||
)
|
||||
from common.log import logger
|
||||
from common.tts_model import ModelHolder
|
||||
from infer import InvalidToneError
|
||||
from text.japanese import g2kata_tone, kata_tone2phone_tone, text_normalize
|
||||
|
||||
# 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"]
|
||||
|
||||
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 = text_normalize(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 {LATEST_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))
|
||||
|
||||
|
||||
def create_inference_app():
|
||||
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",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
model_dir = Path(args.dir)
|
||||
|
||||
if args.cpu:
|
||||
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]]
|
||||
|
||||
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],
|
||||
)
|
||||
|
||||
# app.launch(
|
||||
# inbrowser=not args.no_autolaunch, share=args.share, server_name=args.server_name
|
||||
# )
|
||||
|
||||
return app
|
||||
521
webui/merge.py
Normal file
521
webui/merge.py
Normal file
@@ -0,0 +1,521 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
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.constants import DEFAULT_STYLE, GRADIO_THEME
|
||||
from common.log import logger
|
||||
from common.tts_model import Model, ModelHolder
|
||||
|
||||
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"]
|
||||
|
||||
model_holder = ModelHolder(Path(assets_root), device)
|
||||
|
||||
|
||||
def merge_style(model_name_a, model_name_b, weight, output_name, style_triple_list):
|
||||
"""
|
||||
style_triple_list: list[(model_aでのスタイル名, model_bでのスタイル名, 出力するスタイル名)]
|
||||
"""
|
||||
# 新スタイル名リストにNeutralが含まれているか確認し、Neutralを先頭に持ってくる
|
||||
if any(triple[2] == DEFAULT_STYLE for triple in style_triple_list):
|
||||
# 存在する場合、リストをソート
|
||||
sorted_list = sorted(style_triple_list, key=lambda x: x[2] != DEFAULT_STYLE)
|
||||
else:
|
||||
# 存在しない場合、エラーを発生
|
||||
raise ValueError("No element with {DEFAULT_STYLE} output style name found.")
|
||||
|
||||
style_vectors_a = np.load(
|
||||
os.path.join(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():
|
||||
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_names = model_holder.model_names
|
||||
if len(model_names) == 0:
|
||||
logger.error(
|
||||
f"モデルが見つかりませんでした。{assets_root}にモデルを置いてください。"
|
||||
)
|
||||
sys.exit(1)
|
||||
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],
|
||||
)
|
||||
|
||||
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
|
||||
483
webui/style_vectors.py
Normal file
483
webui/style_vectors.py
Normal file
@@ -0,0 +1,483 @@
|
||||
import argparse
|
||||
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 common.constants import DEFAULT_STYLE, GRADIO_THEME
|
||||
from common.log import logger
|
||||
from config import config
|
||||
|
||||
# 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),
|
||||
f"クラスタが数が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 f"スタイル名が重複しています。"
|
||||
|
||||
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 f"スタイル名が重複しています。"
|
||||
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}を更新しました。"
|
||||
|
||||
|
||||
initial_md = f"""
|
||||
# Style Bert-VITS2 スタイルベクトルの作成
|
||||
|
||||
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:
|
||||
gr.Markdown(initial_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],
|
||||
)
|
||||
|
||||
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
|
||||
813
webui/train.py
Normal file
813
webui/train.py
Normal file
@@ -0,0 +1,813 @@
|
||||
import argparse
|
||||
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 common.constants import GRADIO_THEME, LATEST_VERSION
|
||||
from common.log import logger
|
||||
from common.stdout_wrapper import SAFE_STDOUT
|
||||
from common.subprocess_utils 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(f"Step 2: resampling failed.")
|
||||
return False, f"Step 2, Error: 音声ファイルの前処理に失敗しました:\n{message}"
|
||||
elif message:
|
||||
logger.warning(f"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(f"Step 3: preprocessing text failed.")
|
||||
return (
|
||||
False,
|
||||
f"Step 3, Error: 書き起こしファイルの前処理に失敗しました:\n{message}",
|
||||
)
|
||||
elif message:
|
||||
logger.warning(f"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(f"Step 4: bert_gen failed.")
|
||||
return False, f"Step 4, Error: BERT特徴ファイルの生成に失敗しました:\n{message}"
|
||||
elif message:
|
||||
logger.warning(f"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(f"Step 5: style_gen failed.")
|
||||
return (
|
||||
False,
|
||||
f"Step 5, Error: スタイル特徴ファイルの生成に失敗しました:\n{message}",
|
||||
)
|
||||
elif message:
|
||||
logger.warning(f"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(f"Train failed.")
|
||||
return False, f"Error: 学習に失敗しました:\n{message}"
|
||||
elif message:
|
||||
logger.warning(f"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を開く")
|
||||
|
||||
|
||||
initial_md = f"""
|
||||
# Style-Bert-VITS2 ver {LATEST_VERSION} 学習用WebUI
|
||||
|
||||
## 使い方
|
||||
|
||||
- データを準備して、モデル名を入力して、必要なら設定を調整してから、「自動前処理を実行」ボタンを押してください。進捗状況等はターミナルに表示されます。
|
||||
|
||||
- 各ステップごとに実行する場合は「手動前処理」を使ってください(基本的には自動でいいはず)。
|
||||
|
||||
- 前処理が終わったら、「学習を開始する」ボタンを押すと学習が開始されます。
|
||||
|
||||
- 途中から学習を再開する場合は、モデル名を入力してから「学習を開始する」を押せばよいです。
|
||||
|
||||
注意: 標準スタイル以外のスタイルを音声合成で使うには、スタイルベクトルファイル`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(theme=GRADIO_THEME).queue() as app:
|
||||
gr.Markdown(initial_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],
|
||||
)
|
||||
|
||||
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)
|
||||
return app
|
||||
Reference in New Issue
Block a user