diff --git a/app.py b/app.py index 1514444..7057f45 100644 --- a/app.py +++ b/app.py @@ -1,502 +1,30 @@ -import argparse -import datetime -import json -import os -import sys -from pathlib import Path -from typing import Optional - +import pyopenjtalk 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 webui import ( + create_dataset_app, + create_train_app, + create_merge_app, + create_style_vectors_app, ) -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 +from pathlib import Path -# 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"] +pyopenjtalk.unset_user_dict() -languages = [l.value for l in Languages] +setting_json = Path("webui/setting.json") + +with gr.Blocks() as app: + with gr.Tabs(): + with gr.Tab("Hello"): + gr.Markdown("## Hello, Gradio!") + gr.Textbox("input", label="Input Text") + with gr.Tab("Dataset"): + create_dataset_app() + with gr.Tab("Train"): + create_train_app() + with gr.Tab("Merge"): + create_merge_app() + with gr.Tab("Create Style Vectors"): + create_style_vectors_app() -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)) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--cpu", action="store_true", help="Use CPU instead of GPU") - parser.add_argument( - "--dir", "-d", type=str, help="Model directory", default=assets_root - ) - parser.add_argument( - "--share", action="store_true", help="Share this app publicly", default=False - ) - parser.add_argument( - "--server-name", - type=str, - default=None, - help="Server name for Gradio app", - ) - parser.add_argument( - "--no-autolaunch", - action="store_true", - default=False, - help="Do not launch app automatically", - ) - 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 - ) +app.launch(inbrowser=True) diff --git a/webui/__init__.py b/webui/__init__.py new file mode 100644 index 0000000..4bc8efa --- /dev/null +++ b/webui/__init__.py @@ -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 diff --git a/webui_dataset.py b/webui/dataset.py similarity index 50% rename from webui_dataset.py rename to webui/dataset.py index fec7a9a..5ed656c 100644 --- a/webui_dataset.py +++ b/webui/dataset.py @@ -1,208 +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)もおすすめします。書き起こし部分もかなり工夫されています。ですがファイル数が多い場合などは、このツールで簡易的に切り出してデータセットを作るだけでも十分という気もしています。 -""" - -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) +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 diff --git a/webui/inference.py b/webui/inference.py new file mode 100644 index 0000000..663d5a6 --- /dev/null +++ b/webui/inference.py @@ -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 diff --git a/webui_merge.py b/webui/merge.py similarity index 65% rename from webui_merge.py rename to webui/merge.py index a58471a..c002e33 100644 --- a/webui_merge.py +++ b/webui/merge.py @@ -330,187 +330,192 @@ Happy, Surprise, HappySurprise - 構造上の相性の関係で、スタイルベクトルを混ぜる重みは、上の「話し方」と同じ比率で混ぜられます。例えば「話し方」が0のときはモデルAのみしか使われません。 """ -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): +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.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.Accordion(label="使い方", open=False): + gr.Markdown(initial_md) 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(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"): - 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) + new_name = gr.Textbox(label="新しいモデル名", placeholder="new_model") 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="情報") + 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="結果") + 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], - ) + 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], + 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", ) - - load_style_button.click( - load_styles_gr, - inputs=[model_name_a, model_name_b], - outputs=[styles_a, styles_b, style_triple_list], + 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() - 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 -) + # app.launch( + # inbrowser=not args.no_autolaunch, server_name=args.server_name, share=args.share + # ) + return app diff --git a/webui_style_vectors.py b/webui/style_vectors.py similarity index 65% rename from webui_style_vectors.py rename to webui/style_vectors.py index b89c9c9..27b95bb 100644 --- a/webui_style_vectors.py +++ b/webui/style_vectors.py @@ -323,158 +323,161 @@ UMAPの場合はepsは0.3くらい、t-SNEの場合は2.5くらいがいいか https://ja.wikipedia.org/wiki/DBSCAN """ -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="上の図を見ながらスタイルの数を試行錯誤してください。", + +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", ) - 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", + 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(): - 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, + 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}として自動的に保存されます。", ) - 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="保存結果") + 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], + 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() + 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 -) + # app.launch( + # inbrowser=not args.no_autolaunch, server_name=args.server_name, share=args.share + # ) + return app diff --git a/webui_train.py b/webui/train.py similarity index 99% rename from webui_train.py rename to webui/train.py index 59cc9f5..1c9a214 100644 --- a/webui_train.py +++ b/webui/train.py @@ -450,7 +450,8 @@ english_teacher.wav|Mary|EN|How are you? I'm fine, thank you, and you? 日本語話者の単一話者データセットでも構いません。 """ -if __name__ == "__main__": + +def create_train_app(): with gr.Blocks(theme=GRADIO_THEME).queue() as app: gr.Markdown(initial_md) with gr.Accordion(label="データの前準備", open=False): @@ -808,4 +809,5 @@ if __name__ == "__main__": ) args = parser.parse_args() - app.launch(inbrowser=not args.no_autolaunch, server_name=args.server_name) + # app.launch(inbrowser=not args.no_autolaunch, server_name=args.server_name) + return app