From c7571a7c3969d58e4520f748e841b4a7628a2fd0 Mon Sep 17 00:00:00 2001 From: litagin02 Date: Thu, 4 Jan 2024 11:30:14 +0900 Subject: [PATCH] Update --- README.md | 8 ++-- colab.ipynb | 49 +++++++++++++++++------- configs/config.json | 2 +- docs/CHANGELOG.md | 13 ++++++- docs/CLI.md | 56 +++++++++++++++++++++++++++ docs/tutorial.md | 1 - resample.py | 2 + slice.py | 12 +----- speech_mos.py | 24 ++++++++++-- transcribe.py | 10 +++-- webui_dataset.py | 86 ++++++++++++++++++++++++++++++++++-------- webui_style_vectors.py | 4 +- 12 files changed, 213 insertions(+), 54 deletions(-) create mode 100644 docs/CLI.md delete mode 100644 docs/tutorial.md diff --git a/README.md b/README.md index 2cbaad0..de7df22 100644 --- a/README.md +++ b/README.md @@ -87,14 +87,16 @@ model_assets - デフォルトスタイル「Neutral」以外のスタイルを使いたい人向けです。 - `Style.bat`をダブルクリックか`python webui_style_vectors.py`するとWebUIが起動します。 -- 学習とは独立しているので、学習中でもできるし、学習が終わっても何度もやりなおせます。 -- スタイルについての詳細は[clustering.ipynb](clustering.ipynb)を参照してください。 +- 学習とは独立しているので、学習中でもできるし、学習が終わっても何度もやりなおせます(前処理は終わらせている必要があります)。 +- スタイルについての仕様の詳細は[clustering.ipynb](clustering.ipynb)を参照してください。 ### データセット作り - `Dataset.bat`をダブルクリックか`python webui_dataset.py`すると、音声ファイルからデータセットを作るためのWebUIが起動します。音声ファイルのみからでもこれを使って学習できます。 -注意: データセットの手動修正やノイズ除去や、より高品質なデータセットを作りたい場合は、[Aivis](https://github.com/tsukumijima/Aivis)や、そのデータセット部分のWindows対応版 [Aivis Dataset](https://github.com/litagin02/Aivis-Dataset) を使うのをおすすめします。 +注意: データセットの手動修正やノイズ除去等、細かい修正を行いたい場合は[Aivis](https://github.com/tsukumijima/Aivis)や、そのデータセット部分のWindows対応版 [Aivis Dataset](https://github.com/litagin02/Aivis-Dataset) を使うといいかもしれません。ですがファイル数が多い場合などは、このツールで簡易的に切り出してデータセットを作るだけでも十分という気もしています。 + +データセットがどのようなものがいいかは各自試行錯誤中してください。 ### API Server diff --git a/colab.ipynb b/colab.ipynb index f9a900d..d02670e 100644 --- a/colab.ipynb +++ b/colab.ipynb @@ -54,11 +54,23 @@ "!python initialize.py" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Google driveを使う方はこちらを実行してください。\n", + "\n", + "from google.colab import drive\n", + "drive.mount(\"/content/drive\")" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 1. 初期設定\n", + "## 1. 初期設定(とデータセット作成)\n", "\n", "学習とその結果を保存するディレクトリ名を指定します。\n", "Google driveの場合はそのまま実行、カスタマイズしたい方は変更して実行してください。" @@ -70,12 +82,17 @@ "metadata": {}, "outputs": [], "source": [ + "# 学習に必要なファイルや途中経過が保存されるディレクトリ\n", "dataset_root = \"/content/drive/MyDrive/Style-Bert-VITS2/Data\"\n", + "\n", + "# 学習結果(音声合成に必要なファイルたち)が保存されるディレクトリ\n", "assets_root = \"/content/drive/MyDrive/Style-Bert-VITS2/model_assets\"\n", "\n", "import yaml\n", "\n", + "\n", "with open(\"configs/paths.yml\", \"w\", encoding=\"utf-8\") as f:\n", + "\n", " yaml.dump({\"dataset_root\": dataset_root, \"assets_root\": assets_root}, f)" ] }, @@ -83,25 +100,31 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 2. Google Driveとの連携とデータの配置\n", + "### 音声ファイルからのデータセットの作成(ある人はスキップ可)\n", "\n", - "Google driveにデータを保存する方は、次のセルを実行して、Google driveと連携します。" + "音声ファイル(1ファイル2-12秒程度)とその書き起こしのデータセットを持っていない方は、音声ファイルのみから以下の手順でデータセットを作成することができます。デフォルトではGoogle drive上の`Style-Bert-VITS2/inputs/`に音声ファイル(wavファイル形式、1ファイルでも複数ファイルでも可)を置いて、下を実行すると、データセットが作られます。" ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "xzbmRmVfP29m", - "outputId": "bd492fba-3cba-4002-b6d9-f144f701fce6" - }, + "metadata": {}, "outputs": [], "source": [ - "from google.colab import drive\n", - "drive.mount(\"/content/drive\")" + "# 元となる音声ファイル(wav形式)を入れるディレクトリ\n", + "input_dir = \"/content/drive/MyDrive/Style-Bert-VITS2/inputs\"\n", + "# モデル名(話者名)を入力\n", + "model_name = \"your_model_name\"\n", + "\n", + "!python slice.py -i {input_dir} -o {dataset_root}/{model_name}/raw\n", + "!python transcribe.py -i {dataset_root}/{model_name}/raw -o {dataset_root}/{model_name}/esd.list --speaker_name {model_name} --compute_type float16" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. データの配置" ] }, { @@ -186,7 +209,7 @@ "# 上でつけたフォルダの名前`Data/{model_name}/`\n", "model_name = \"your_model_name\"\n", "\n", - "# 学習のバッチサイズ。4ぐらいが最適か。VRAMのはみ出具合に応じて調整してください。\n", + "# 学習のバッチサイズ。VRAMのはみ出具合に応じて調整してください。\n", "batch_size = 4\n", "\n", "# 学習のエポック数(データセットを合計何周するか)。\n", diff --git a/configs/config.json b/configs/config.json index 3ff4fa3..640577a 100644 --- a/configs/config.json +++ b/configs/config.json @@ -66,5 +66,5 @@ "use_spectral_norm": false, "gin_channels": 256 }, - "version": "1.2" + "version": "1.3" } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 942fe8e..25fa811 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,9 +1,20 @@ # Changelog +## v1.3 + +- `Dataset.bat`の音声スライスと書き起こしをよりカスタマイズできるように(秒数指定や書き起こしのWhisperモデル指定や言語指定等) +- `Style.bat`のスタイル作成で、新しい方法(DBSCAN)を追加(こちらのほうがスタイル数を指定できない代わりに特徴がよく出るかもしれません)。 +- クラウド実行等の際にパスの指定をこちらでできるように、パスの設定を`configs/paths.yml`にまとめました(colabの[ノートブック](http://colab.research.google.com/github/litagin02/Style-Bert-VITS2/blob/master/colab.ipynb)もそれに伴って変えたので新しいものを使ってください)。デフォルトは`dataset_root: Data`と`assets_root: model_assets`です。クラウド等でやる方はここを変更してください。 +- どのステップ数の出力がよいかの「一つの」指標として [SpeechMOS](https://github.com/tarepan/SpeechMOS) を使うスクリプトを追加: +```bash +python speech_mos.py -m [-o ] +``` +ステップごとの自然性評価が表示されるはず。読み上げさせたい文章を変えたかったら中のファイルを弄って各自調整してください。またあくまで目安のひとつなので、実際に読み上げさせて選別するのが一番だと思います。 + ## v1.2 (2023-12-31) - グラボがないユーザーでの音声合成をサポート、`Install-Style-Bert-VITS2-CPU.bat`でインストール。 -- Google Colabでの学習をサポート、[ノートブック](../colab.ipynb)を追加 +- Google Colabでの学習をサポート、[ノートブック](http://colab.research.google.com/github/litagin02/Style-Bert-VITS2/blob/master/colab.ipynb)を追加 - 音声合成のAPIサーバーを追加、`python server_fastapi.py`で起動します。API仕様は起動後に`/docs`にて確認ください。( @darai0512 様によるPRです、ありがとうございます!) - 学習時に自動的にデフォルトスタイル Neutral を生成するように。特にスタイル指定が必要のない方は、学習したらそのまま音声合成を試せます。これまで通りスタイルを自分で作ることもできます。 - マージ機能の新規追加: `Merge.bat`, `webui_merge.py` diff --git a/docs/CLI.md b/docs/CLI.md new file mode 100644 index 0000000..582365e --- /dev/null +++ b/docs/CLI.md @@ -0,0 +1,56 @@ +# CLI + + + +## Dataset + +`Dataset.bat` webui (`python webui_dataset.py`) consists of **slice audio** and **transcribe wavs**. + +### Slice audio + +```bash +python slice.py -i -o -m -M +``` + +Required: +- `input_dir`: Path to the directory containing the audio files to slice. +- `output_dir`: Path to the directory where the sliced audio files will be saved. + +Optional: +- `min_sec`: Minimum duration of the sliced audio files in seconds (default 2). +- `max_sec`: Maximum duration of the sliced audio files in seconds (default 12). + +### Transcribe wavs + +```bash +python transcribe.py -i -o --speaker_name +``` + +Required: +- `input_dir`: Path to the directory containing the audio files to transcribe. +- `output_file`: Path to the file where the transcriptions will be saved. +- `speaker_name`: Name of the speaker. + +Optional +- `--initial_prompt`: Initial prompt to use for the transcription (default value is specific to Japanese). +- `--device`: `cuda` or `cpu` (default: `cuda`). +- `--language`: `jp`, `en`, or `en` (default: `jp`). +- `--model`: Whisper model, default: `large-v3` +- `--compute_type`: default: `bfloat16` + +## Train + +`Train.bat` webui (`python webui_train.py`) consists of the following. + +### Preprocess audio +```bash +python resample.py -i -o [--normalize] [--trim] +``` + +Required: +- `input_dir`: Path to the directory containing the audio files to preprocess. +- `output_dir`: Path to the directory where the preprocessed audio files will be saved. + +TO BE WRITTEN (WIP) + +これいる? diff --git a/docs/tutorial.md b/docs/tutorial.md deleted file mode 100644 index 317179a..0000000 --- a/docs/tutorial.md +++ /dev/null @@ -1 +0,0 @@ -To be written. \ No newline at end of file diff --git a/resample.py b/resample.py index c04c9cf..4af63ae 100644 --- a/resample.py +++ b/resample.py @@ -56,12 +56,14 @@ if __name__ == "__main__": ) parser.add_argument( "--in_dir", + "-i", type=str, default=config.resample_config.in_dir, help="path to source dir", ) parser.add_argument( "--out_dir", + "-o", type=str, default=config.resample_config.out_dir, help="path to target dir", diff --git a/slice.py b/slice.py index cbdcfcd..10e15b0 100644 --- a/slice.py +++ b/slice.py @@ -9,7 +9,6 @@ from tqdm import tqdm from common.log import logger from common.stdout_wrapper import SAFE_STDOUT -from resample import normalize_audio vad_model, utils = torch.hub.load( repo_or_dir="snakers4/silero-vad", @@ -59,7 +58,6 @@ def split_wav( min_sec=2, max_sec=12, min_silence_dur_ms=700, - normalize=False, ): margin = 200 # ミリ秒単位で、音声の前後に余裕を持たせる speech_timestamps = get_stamps( @@ -87,9 +85,6 @@ def split_wav( end_sample = int(end_ms / 1000 * sr) segment = data[start_sample:end_sample] - if normalize: - segment = normalize_audio(segment, sr) - sf.write(os.path.join(target_dir, f"{file_name}-{i}.wav"), segment, sr) total_time_ms += end_ms - start_ms @@ -113,14 +108,11 @@ if __name__ == "__main__": ) parser.add_argument( "--output_dir", - "-t", + "-o", type=str, default="raw", help="Directory of output wav files", ) - parser.add_argument( - "--normalize", action="store_true", help="Whether to normalize loudness" - ) parser.add_argument( "--min_silence_dur_ms", "-s", @@ -135,7 +127,6 @@ if __name__ == "__main__": min_sec = args.min_sec max_sec = args.max_sec min_silence_dur_ms = args.min_silence_dur_ms - normalize = args.normalize wav_files = Path(input_dir).glob("**/*.wav") wav_files = list(wav_files) @@ -151,7 +142,6 @@ if __name__ == "__main__": min_sec=min_sec, max_sec=max_sec, min_silence_dur_ms=min_silence_dur_ms, - normalize=normalize, ) total_sec += time_sec diff --git a/speech_mos.py b/speech_mos.py index cd76136..ef3f133 100644 --- a/speech_mos.py +++ b/speech_mos.py @@ -7,6 +7,7 @@ import matplotlib.pyplot as plt import pandas as pd import torch from tqdm import tqdm +import numpy as np from common.log import logger from common.tts_model import Model @@ -35,8 +36,7 @@ predictor = torch.hub.load( parser = argparse.ArgumentParser() parser.add_argument("--model_name", "-m", type=str, required=True) -parser.add_argument("--device", "-d", type=str, default="cuda") -parser.add_argument("--output", "-o", type=str, default="mos.csv") +parser.add_argument("--device", "-d", type=str, default="cuda")) args = parser.parse_args() @@ -84,12 +84,14 @@ results = sorted(results, key=lambda x: x[2][-1], reverse=True) for model_file, step, scores in results: logger.info(f"{model_file}: {scores[-1]}") -with open(args.output, "w", encoding="utf-8", newline="") as f: +with open(f"mos_{model_name}.csv", "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) writer.writerow(["model_path"] + ["step"] + test_texts + ["mean"]) for model_file, step, scores in results: writer.writerow([model_file] + [step] + scores) +logger.info(f"mos_{model_name}.csv has been saved.") + # step countと各MOSの値を格納するリストを初期化 steps = [] mos_values = [] @@ -117,8 +119,24 @@ plt.title("TTS Model Naturalness MOS") plt.xlabel("Step Count") plt.ylabel("MOS") +# ステップ数の軸ラベルを1000単位で表示するように調整 +plt.xticks( + ticks=np.arange(0, max(steps) + 1000, 1000), + labels=[f"{int(x/1000)}k" for x in np.arange(0, max(steps) + 1000, 5000)], +) + +# 縦の補助線を追加 +plt.grid(True, axis="x") + +# 凡例をグラフの外側に配置 +plt.legend(loc="center left", bbox_to_anchor=(1, 0.5)) + # 凡例を表示 plt.legend() # グラフを表示 plt.show() + +plt.savefig(f"mos_{model_name}.png", bbox_inches="tight") + +logger.info(f"mos_{model_name}.png has been saved.") diff --git a/transcribe.py b/transcribe.py index 14e818f..43dabb8 100644 --- a/transcribe.py +++ b/transcribe.py @@ -19,10 +19,10 @@ def transcribe(wav_path, initial_prompt=None, language="ja"): if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--input_dir", type=str, default="raw") - parser.add_argument("--output_file", type=str, default="esd.list") + parser.add_argument("--input_dir", "-i", type=str, default="raw") + parser.add_argument("--output_file", "-o", type=str, default="esd.list") parser.add_argument( - "--initial_prompt", type=str, default="こんにちは。元気、ですかー?私は……ちゃんと元気だよ!" + "--initial_prompt", type=str, default="こんにちは。元気、ですかー?ふふっ、私は……ちゃんと元気だよ!" ) parser.add_argument( "--language", type=str, default="ja", choices=["ja", "en", "zh"] @@ -43,13 +43,15 @@ if __name__ == "__main__": device = args.device compute_type = args.compute_type + os.makedirs(os.path.dirname(output_file), exist_ok=True) + logger.info( f"Loading Whisper model ({args.model}) with compute_type={compute_type}" ) try: model = WhisperModel(args.model, device=device, compute_type=compute_type) except ValueError as e: - logger.warning(f"Failed to load model: {e}") + logger.warning(f"Failed to load model, so use `auto` compute_type: {e}") model = WhisperModel(args.model, device=device) wav_files = [ diff --git a/webui_dataset.py b/webui_dataset.py index 35a5c7b..6984ae9 100644 --- a/webui_dataset.py +++ b/webui_dataset.py @@ -13,9 +13,16 @@ with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f: # assets_root = path_config["assets_root"] -def do_slice(model_name: str, min_sec: float, max_sec: float, input_dir="inputs"): +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...") - input_dir = "inputs" output_dir = os.path.join(dataset_root, model_name, "raw") cmd = [ "slice.py", @@ -27,20 +34,28 @@ def do_slice(model_name: str, min_sec: float, max_sec: float, input_dir="inputs" str(min_sec), "--max_sec", str(max_sec), + "--min_silence_dur_ms", + str(min_silence_dur_ms), ] + # 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): +def do_transcribe( + model_name, whisper_model, compute_type, language, initial_prompt, input_dir, device +): + if model_name == "": + return "Error: モデル名を入力してください。" if initial_prompt == "": - initial_prompt = "こんにちは。元気、ですかー?私は……ちゃんと元気だよ!" + initial_prompt = "こんにちは。元気、ですかー?私は……ふふっ、ちゃんと元気だよ!" logger.debug(f"initial_prompt: {initial_prompt}") - input_dir = os.path.join(dataset_root, model_name, "raw") + if input_dir == "": + input_dir = os.path.join(dataset_root, model_name, "raw") output_file = os.path.join(dataset_root, model_name, "esd.list") - result = run_script_with_log( + success, message = run_script_with_log( [ "transcribe.py", "--input_dir", @@ -53,29 +68,44 @@ def do_transcribe(model_name, whisper_model, compute_type, language, initial_pro 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の学習用データセットを作成するためのツールです。与えられた音声からちょうどいい長さの発話区間を切り取りスライスし、それぞれの音声に対して文字起こしを行います。 +Style-Bert-VITS2の学習用データセットを作成するためのツールです。以下の2つからなります。 + +- 与えられた音声からちょうどいい長さの発話区間を切り取りスライス +- 音声に対して文字起こし + +このうち両方を使ってもよいし、スライスする必要がない場合は後者のみを使ってもよいです。 ## 必要なもの + 学習したい音声が入ったwavファイルいくつか。 合計時間がある程度はあったほうがいいかも、10分とかでも大丈夫だったとの報告あり。単一ファイルでも良いし複数ファイルでもよい。 -## 使い方 -1. `inputs`フォルダ直下にwavファイルをすべて入れる -2. `モデル名`を入力して、`音声のスライス`ボタンを押す -3. 完了したら、`音声の文字起こし`ボタンを押す -4. 出来上がった音声ファイルたちは`Data/{モデル名}/raw`に、書き起こしファイルは`Data/{モデル名}/esd.list`に保存されます。 +## スライス使い方 +1. `inputs`フォルダにwavファイルをすべて入れる +2. `モデル名`を入力して、設定を必要なら調整して`音声のスライス`ボタンを押す +3. 出来上がった音声ファイルたちは`Data/{モデル名}/raw`に保存される + +## 書き起こし使い方 + +1. 書き起こしたい音声ファイルのあるフォルダを指定(デフォルトは`Data/{モデル名}/raw`なのでスライス後に行う場合は省略してよい) +2. 設定を必要なら調整してボタンを押す +3. 書き起こしファイルは`Data/{モデル名}/esd.list`に保存される ## 注意 @@ -90,16 +120,31 @@ with gr.Blocks(theme="NoCrypt/miku") as app: with gr.Accordion("音声のスライス"): with gr.Row(): with gr.Column(): + input_dir = gr.Textbox( + label="入力フォルダ名(デフォルトはinputs)", + placeholder="inputs", + info="inputsフォルダに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(): + raw_dir = gr.Textbox( + label="書き起こしたい音声ファイルが入っているフォルダ(スライスした場合など、`Data/{モデル名}/raw`の場合は省略可", + ) whisper_model = gr.Dropdown( ["tiny", "base", "small", "medium", "large", "large-v2", "large-v3"], label="Whisperモデル", @@ -119,20 +164,31 @@ with gr.Blocks(theme="NoCrypt/miku") as app: 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="初期プロンプト(日本語の場合は省略可)", placeholder="こんにちは。元気、ですかー?私は……ちゃんと元気だよ!" + label="初期プロンプト", + placeholder="こんにちは。元気、ですかー?ふふっ、私は……ちゃんと元気だよ!", + info="このように書き起こしてほしいという例文、日本語なら省略可、英語等なら書いてください", ) transcribe_button = gr.Button("音声の文字起こし") result2 = gr.Textbox(label="結果") slice_button.click( do_slice, - inputs=[model_name, min_sec, max_sec], + 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], + inputs=[ + model_name, + whisper_model, + compute_type, + language, + initial_prompt, + raw_dir, + device, + ], outputs=[result2], ) diff --git a/webui_style_vectors.py b/webui_style_vectors.py index 8b2b101..b71de54 100644 --- a/webui_style_vectors.py +++ b/webui_style_vectors.py @@ -138,7 +138,7 @@ def do_dbscan_gradio(eps=2.5, min_samples=15): f"クラスタが数が0です。パラメータを変えてみてください。", ] + [gr.Audio(visible=False)] * MAX_AUDIO_NUM - return [plt, gr.Slider(maximum=n_clusters), n_clusters] + [ + return [plt, gr.Slider(maximum=n_clusters, value=1), n_clusters] + [ gr.Audio(visible=False) ] * MAX_AUDIO_NUM @@ -170,7 +170,7 @@ def do_clustering_gradio(n_clusters=4, method="KMeans"): ) plt.legend() - return [plt.gcf(), gr.Slider(maximum=n_clusters)] + [ + return [plt.gcf(), gr.Slider(maximum=n_clusters, value=1)] + [ gr.Audio(visible=False) ] * MAX_AUDIO_NUM