Feat: slice-and-transcribe

This commit is contained in:
litagin02
2023-12-27 16:16:22 +09:00
parent 0a9a652d3b
commit 5be778284b
22 changed files with 685 additions and 138 deletions

2
.gitignore vendored
View File

@@ -14,3 +14,5 @@ venv/
/pretrained/*.safetensors
/pretrained/*.pth
/scripts/test/

View File

@@ -1,9 +1,11 @@
chcp 65001 > NUL
@echo off
pushd %~dp0
echo Running app.py...
venv\Scripts\python app.py
if %errorlevel% neq 0 ( pause & popd & exit /b %errorlevel% )
popd
pause

11
Dataset.bat Normal file
View File

@@ -0,0 +1,11 @@
chcp 65001 > NUL
@echo off
pushd %~dp0
echo Running webui_dataset.py...
venv\Scripts\python webui_dataset.py
if %errorlevel% neq 0 ( pause & popd & exit /b %errorlevel% )
popd
pause

View File

@@ -38,7 +38,6 @@ python initialize.py
### 音声合成
`App.bat`をダブルクリックするとWebUIが起動します。
TODO: デフォルトモデルをいくつかダウンロードするようにする
ディレクトリ構造:
```
@@ -74,6 +73,12 @@ model_assets
- safetensors形式のサポート、デフォルトでsafetensorsを使用するように
- その他軽微なbugfixやリファクタリング
## TODO
- [ ] 複数話者学習での音声合成対応(学習は現在でも可能)
- [ ] 本家のver 2.1, 2.2, 2.3モデルの推論対応ver 2.1以外は明らかにめんどいのでたぶんやらない)
- [ ] `server_fastapi.py`の対応、とくにAPIで使えるようになると嬉しい人が増えるのかもしれない
- [ ] 音声ファイルだけ与えられた時にスライスしたり書き起こしたりしてデータセットを作れてすぐ学習できる機能をつける?[このライブラリ](https://github.com/litagin02/slice-and-transcribe)を組み込む?
## Bert-VITS2 v2.1と同じ点
- [事前学習モデル](https://huggingface.co/litagin/style_bert_vits2_jvnv)は、実質Bert-VITS2 v2.1と同じものを使用しています不要な重みを削ってsafetensorsに変換したもの

View File

@@ -2,9 +2,11 @@ chcp 65001 > NUL
@echo off
pushd %~dp0
echo Running webui_style_vectors.py...
venv\Scripts\python webui_style_vectors.py
if %errorlevel% neq 0 ( pause & popd & exit /b %errorlevel% )
popd
pause

View File

@@ -2,9 +2,12 @@ chcp 65001 > NUL
@echo off
pushd %~dp0
echo Running webui_train.py...
venv\Scripts\python webui_train.py
if %errorlevel% neq 0 ( pause & popd & exit /b %errorlevel% )
popd
pause

120
app.py
View File

@@ -1,5 +1,6 @@
import argparse
import os
import sys
import gradio as gr
import numpy as np
@@ -248,6 +249,13 @@ initial_text = "こんにちは、初めまして。あなたの名前はなん
example_local = [
[initial_text, "JP"],
[
"""あなたがそんなこと言うなんて、私はとっても嬉しい。
あなたがそんなこと言うなんて、私はとっても怒ってる。
あなたがそんなこと言うなんて、私はとっても驚いてる。
あなたがそんなこと言うなんて、私はとっても辛い。""",
"JP",
],
[ # ChatGPTに考えてもらった告白セリフ
"""私、ずっと前からあなたのことを見てきました。あなたの笑顔、優しさ、強さに、心惹かれていたんです。
友達として過ごす中で、あなたのことがだんだんと特別な存在になっていくのがわかりました。
@@ -268,7 +276,7 @@ example_local = [
"JP",
],
[ # ChatGPTと考えた、感情を表すセリフ
"""やったー!テストで満点取れた!私とっても嬉しいな!
"""やったー!テストで満点取れた!私とっても嬉しいな!
どうして私の意見を無視するの?許せない!ムカつく!あんたなんか死ねばいいのに。
あはははっ!この漫画めっちゃ笑える、見てよこれ、ふふふ、あはは。
あなたがいなくなって、私は一人になっちゃって、泣いちゃいそうなほど悲しい。""",
@@ -306,19 +314,39 @@ example_hf_spaces = [
]
initial_md = """
# Bert-VITS2 okiba TTS デモ
# Style-Bert-VITS2 音声合成
[bert_vits2_okiba](https://huggingface.co/litagin/bert_vits2_okiba) のモデルのデモです。
モデル名は[rvc_okiba](https://huggingface.co/litagin/rvc_okiba)のモデル名と対応しています。
モデルは随時追加していきます。現在のモデルはすべてBert-VITS2のver 2.1のものです。
注意: 初期からある[jvnvのモデル](https://huggingface.co/litagin/style_bert_vits2_jvnv)は、[JVNVコーパス言語音声と非言語音声を持つ日本語感情音声コーパス](https://sites.google.com/site/shinnosuketakamichi/research-topics/jvnv_corpus)で学習されたモデルです。ライセンスは[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.ja)です。とくに**商用利用は不可**です。
"""
**定形サンプルは[こちら](https://huggingface.co/litagin/bert_vits2_okiba/blob/main/examples.md)から聴くほうが速いです。**
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`:スタイルベクトルファイル
- huggingfaceのcpuで動くので、何故かやたら遅いことが多かったりなんか不安定で動かないときもあるみたいです
- huggingface上では最大100文字にしています。
- Style textの実装あたりで本家の内部コードを改造しているので、このapp.pyをそのまま本家に使っても今のところは動きません。
上2つは`Train.bat`による学習で自動的に正しい位置に保存されます。`style_vectors.npy`は`Style.bat`を実行して指示に従って生成してください
現在のところはspeaker_id = 0に固定しています
TODO: 現在のところはspeaker_id = 0に固定しており複数話者の合成には対応していません
"""
style_md = """
- プリセットまたは音声ファイルから読み上げの声音・感情・スタイルのようなものを制御できます。
- デフォルトのNeutralでも、十分に読み上げる文に応じた感情で感情豊かに読み上げられます。このスタイル制御は、それを重み付きで上書きするような感じです。
- 重みを大きくしすぎると発音が変になったり声にならなかったりと崩壊することがあります。
- 音声ファイルを入力する場合は、学習データと似た声音の話者(特に同じ性別)でないとよい効果が出ないかもしれません。
"""
@@ -331,8 +359,8 @@ def make_non_interactive():
def gr_util(item):
if item == "クラスタから選ぶ":
return (gr.update(visible=True), gr.update(visible=False))
if item == "プリセットから選ぶ":
return (gr.update(visible=True), gr.Audio(visible=False, value=None))
else:
return (gr.update(visible=False), gr.update(visible=True))
@@ -357,11 +385,16 @@ if __name__ == "__main__":
examples = example_hf_spaces if is_hf_spaces else example_local
model_names = model_holder.model_names
if len(model_names) == 0:
logger.error(f"モデルが見つかりませんでした。{model_dir}にモデルを置いてください。")
sys.exit(1)
initial_id = 1 if is_hf_spaces else 0
initial_pth_files = model_holder.model_files_dict[model_names[initial_id]]
with gr.Blocks(theme="NoCrypt/miku") 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():
@@ -376,35 +409,17 @@ if __name__ == "__main__":
choices=initial_pth_files,
value=initial_pth_files[0],
)
refresh_button = gr.Button(
"モデル一覧を更新", scale=1, visible=not is_hf_spaces
)
load_button = gr.Button("モデルをロード", scale=1)
refresh_button = gr.Button("更新", scale=1, visible=not is_hf_spaces)
load_button = gr.Button("ロード", scale=1, variant="primary")
text_input = gr.TextArea(label="テキスト", value=initial_text)
use_style_text = gr.Checkbox(label="Style textを使う", value=False)
style_text = gr.Textbox(
label="Style text",
placeholder="どうして私の意見を無視するの?許せない、ムカつく!死ねばいいのに。",
info="このテキストの読み上げと似た声音・感情になりやすくなります。ただ抑揚やテンポ等が犠牲になるかも。",
visible=False,
)
style_text_weight = gr.Slider(
minimum=0,
maximum=1,
value=0.7,
step=0.1,
label="Style textの強さ",
visible=False,
)
use_style_text.change(
lambda x: (gr.Textbox(visible=x), gr.Slider(visible=x)),
inputs=[use_style_text],
outputs=[style_text, style_text_weight],
)
line_split = gr.Checkbox(label="改行で分けて生成", value=True)
split_interval = gr.Slider(
minimum=0.1, maximum=2, value=0.5, step=0.1, label="分けた場合に挟む無音の長さ"
minimum=0.1,
maximum=2,
value=0.5,
step=0.1,
label="分けた場合に挟む無音の長さ(秒)",
)
language = gr.Dropdown(choices=languages, value="JP", label="Language")
with gr.Accordion(label="詳細設定", open=False):
@@ -420,14 +435,36 @@ if __name__ == "__main__":
length_scale = gr.Slider(
minimum=0.1, maximum=2, value=1.0, step=0.1, label="Length"
)
use_style_text = gr.Checkbox(label="Style textを使う", value=False)
style_text = gr.Textbox(
label="Style text",
placeholder="どうして私の意見を無視するの?許せない、ムカつく!死ねばいいのに。",
info="このテキストの読み上げと似た声音・感情になりやすくなります。ただ抑揚やテンポ等が犠牲になる傾向があります。",
visible=False,
)
style_text_weight = gr.Slider(
minimum=0,
maximum=1,
value=0.7,
step=0.1,
label="Style textの強さ",
visible=False,
)
use_style_text.change(
lambda x: (gr.Textbox(visible=x), gr.Slider(visible=x)),
inputs=[use_style_text],
outputs=[style_text, style_text_weight],
)
with gr.Column():
with gr.Accordion("スタイルについて詳細", open=False):
gr.Markdown(style_md)
style_mode = gr.Radio(
["クラスタから選ぶ", "音声ファイルを入力"],
["プリセットから選ぶ", "音声ファイルを入力"],
label="スタイルの指定方法",
value="クラスタから選ぶ",
value="プリセットから選ぶ",
)
style = gr.Dropdown(
label="スタイル(0が平均スタイル)", choices=list(range(7)), value=0
label="スタイル(Neutralが平均スタイル)", choices=["モデルをロードしてください"], value=0
)
style_weight = gr.Slider(
minimum=0,
@@ -442,7 +479,8 @@ if __name__ == "__main__":
)
text_output = gr.Textbox(label="情報")
audio_output = gr.Audio(label="結果")
gr.Examples(examples, inputs=[text_input, language], label="テキスト例")
with gr.Accordion("テキスト例", open=False):
gr.Examples(examples, inputs=[text_input, language])
tts_button.click(
tts_fn,

File diff suppressed because one or more lines are too long

View File

@@ -205,7 +205,7 @@ class Config:
print(
"If you have no special needs, please do not modify default_config.yml."
)
sys.exit(0)
# sys.exit(0)
with open(file=config_path, mode="r", encoding="utf-8") as file:
yaml_config: Dict[str, any] = yaml.safe_load(file.read())
model_name: str = yaml_config["model_name"]

View File

@@ -36,6 +36,34 @@ def download_pretrained_models():
)
def download_jvnv_models():
files = [
"jvnv-F1/config.json",
"jvnv-F1/jvnv-F1.safetensors",
"jvnv-F1/style_vectors.npy",
"jvnv-F2/config.json",
"jvnv-F2/jvnv-F2.safetensors",
"jvnv-F2/style_vectors.npy",
"jvnv-M1/config.json",
"jvnv-M1/jvnv-M1.safetensors",
"jvnv-M1/style_vectors.npy",
# "jvnv-M2/config.json",
# "jvnv-M2/jvnv-M2.safetensors",
# "jvnv-M2/style_vectors.npy",
]
for file in files:
if not Path(f"model_assets/{file}").exists():
logger.info(f"Downloading {file}")
hf_hub_download(
"litagin/style_bert_vits2_jvnv",
file,
local_dir="model_assets",
local_dir_use_symlinks=False,
)
download_bert_models()
download_pretrained_models()
download_jvnv_models()

2
inputs/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -1,5 +1,6 @@
cmudict
cn2an
faster-whisper>=0.10.0
g2p_en
GPUtil
gradio

View File

@@ -65,7 +65,9 @@ if __name__ == "__main__":
twople = (spk_dir, filename, args)
tasks.append(twople)
for _ in tqdm(pool.imap_unordered(process, tasks), file=sys.stdout):
for _ in tqdm(
pool.imap_unordered(process, tasks), file=sys.stdout, total=len(tasks)
):
pass
pool.close()

View File

@@ -58,6 +58,76 @@
"source": [
"save_file(new_dict, f\"pretrained/{model_name.replace('.pth', '.safetensors')}\")"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from glob import glob\n",
"root_dir = \"model_assets\"\n",
"\n",
"safetensors_files = glob(f\"{root_dir}/**/*.safetensors\", recursive=True)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# 推論に不要なenc_qを消し忘れていたのを削除\n",
"\n",
"from safetensors import safe_open\n",
"from safetensors.torch import save_file\n",
"\n",
"for path in safetensors_files:\n",
" print(path)\n",
" tensors = {}\n",
" with safe_open(path, framework=\"pt\", device=\"cpu\") as f:\n",
" for key in f.keys():\n",
" if key.startswith(\"enc_q\"):\n",
" print(key)\n",
" continue\n",
" tensors[key] = f.get_tensor(key)\n",
" save_file(tensors, path.replace(\".safetensors\", \".new.safetensors\"))"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"enc_p.xvec_proj.weight enc_p.style_proj.weight\n",
"enc_p.xvec_proj.bias enc_p.style_proj.bias\n"
]
}
],
"source": [
"# pthファイルを推論用safetensorsに変換\n",
"from safetensors.torch import save_file\n",
"from safetensors import safe_open\n",
"import torch\n",
"\n",
"pth_path = \"model_assets/jvnv-F1/release_7000.pth\"\n",
"pth_weight = torch.load(pth_path, map_location=torch.device(\"cpu\"))\n",
"new_dict = {}\n",
"for key in pth_weight[\"model\"]:\n",
" if key.startswith(\"enc_p.xvec_proj.\"): # 前のモデルの名残\n",
" print(key, key.replace(\"enc_p.xvec_proj.\", \"enc_p.style_proj.\"))\n",
" new_dict[key.replace(\"enc_p.xvec_proj.\", \"enc_p.style_proj.\")] = pth_weight[\"model\"][key].clone().contiguous() # よく分からないおまじないをしないとエラーになる\n",
" elif not key.startswith(\"enc_q\"):\n",
" new_dict[key] = pth_weight[\"model\"][key]\n",
" else:\n",
" continue\n",
"new_dict[\"iteration\"] = torch.LongTensor([pth_weight[\"iteration\"]])\n",
"save_file(new_dict, pth_path.replace(\".pth\", \".pth.safetensors\"))\n"
]
}
],
"metadata": {

119
slice.py Normal file
View File

@@ -0,0 +1,119 @@
import argparse
import os
import shutil
import sys
import torch
from pydub import AudioSegment
from tqdm import tqdm
vad_model, utils = torch.hub.load(
repo_or_dir="snakers4/silero-vad",
model="silero_vad",
onnx=True,
)
(get_speech_timestamps, _, read_audio, *_) = utils
def get_stamps(audio_file, min_silence_dur_ms=700, min_sec=2):
"""
min_silence_dur_ms:
このミリ秒数以上を無音だと判断する。
逆に、この秒数以下の無音区間では区切られない。
小さくすると、音声がぶつ切りに小さくなりすぎ、
大きくすると音声一つ一つが長くなりすぎる。
データセットによってたぶん要調整。
min_sec:
この秒数より小さい発話は無視する。TTSのためには2秒未満は切り捨てたほうがいいかも。
"""
sampling_rate = 16000 # 16kHzか8kHzのみ対応
wav = read_audio(audio_file, sampling_rate=sampling_rate)
speech_timestamps = get_speech_timestamps(
wav,
vad_model,
sampling_rate=sampling_rate,
min_silence_duration_ms=min_silence_dur_ms,
min_speech_duration_ms=min_sec * 1000,
)
return speech_timestamps
def split_wav(
audio_file, target_dir="raw", max_sec=12, min_silence_dur_ms=700, min_sec=2
):
margin = 200 # ミリ秒単位で、音声の前後に余裕を持たせる
upper_bound_ms = max_sec * 1000 # これ以上の長さの音声は無視する
speech_timestamps = get_stamps(
audio_file, min_silence_dur_ms=min_silence_dur_ms, min_sec=min_sec
)
# WAVファイルを読み込む
audio = AudioSegment.from_wav(audio_file)
# リサンプリング44100Hz
audio = audio.set_frame_rate(44100)
# ステレオをモノラルに変換
audio = audio.set_channels(1)
total_ms = len(audio)
file_name = os.path.basename(audio_file).split(".")[0]
os.makedirs(target_dir, exist_ok=True)
total_time_ms = 0
# タイムスタンプに従って分割し、ファイルに保存
for i, ts in enumerate(speech_timestamps):
start_ms = max(ts["start"] / 16 - margin, 0)
end_ms = min(ts["end"] / 16 + margin, total_ms)
if end_ms - start_ms > upper_bound_ms:
continue
segment = audio[start_ms:end_ms]
segment.export(os.path.join(target_dir, f"{file_name}-{i}.wav"), format="wav")
total_time_ms += end_ms - start_ms
return total_time_ms / 1000
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--max_sec", "-M", type=int, default=12)
parser.add_argument("--min_sec", "-m", type=int, default=2)
parser.add_argument("--min_silence_dur_ms", "-s", type=int, default=700)
parser.add_argument("--input_dir", "-i", type=str, default="inputs")
parser.add_argument("--output_dir", "-t", type=str, default="raw")
args = parser.parse_args()
input_dir = args.input_dir
output_dir = args.output_dir
min_sec = args.min_sec
max_sec = args.max_sec
min_silence_dur_ms = args.min_silence_dur_ms
wav_files = [
os.path.join(input_dir, f)
for f in os.listdir(input_dir)
if f.lower().endswith(".wav")
]
if os.path.exists(output_dir): # ディレクトリを削除
print(f"{output_dir}フォルダが存在するので、削除します。")
shutil.rmtree(output_dir)
total_sec = 0
for wav_file in tqdm(wav_files, file=sys.stdout):
time_sec = split_wav(
wav_file,
output_dir,
max_sec=max_sec,
min_sec=min_sec,
min_silence_dur_ms=min_silence_dur_ms,
)
total_sec += time_sec
print(f"Done! Total time: {total_sec / 60:.2f} min.")

View File

@@ -10,7 +10,7 @@ logger.remove()
# 自定义格式并添加到标准输出
log_format = (
"<g>{time:MM-DD HH:mm:ss}</g> <lvl>{level:<9}</lvl>| {file}:{line} | {message}"
"<g>{time:MM-DD HH:mm:ss}</g> |<lvl>{level:^8}</lvl>| {file}:{line} | {message}"
)
logger.add(sys.stdout, format=log_format, backtrace=True, diagnose=True)

View File

@@ -352,6 +352,7 @@ def run():
scheduler_dur_disc.step()
if epoch == hps.train.epochs:
# Save the final models
utils.save_checkpoint(
net_g,
optim_g,
@@ -379,8 +380,9 @@ def run():
epoch,
os.path.join(
out_dir,
f"{config.model_name}.safetensors",
f"{config.model_name}_e{epoch}_s{global_step}.safetensors",
),
for_infer=True,
)
@@ -652,6 +654,7 @@ def train_and_evaluate(
config.model_name,
f"{config.model_name}_e{epoch}_s{global_step}.safetensors",
),
for_infer=True,
)
global_step += 1

51
transcribe.py Normal file
View File

@@ -0,0 +1,51 @@
import argparse
import os
import sys
from faster_whisper import WhisperModel
from tqdm import tqdm
def transcribe(wav_path, initial_prompt=None):
segments, _ = model.transcribe(
wav_path, beam_size=5, language="ja", initial_prompt=initial_prompt
)
texts = [segment.text for segment in segments]
return "".join(texts)
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(
"--initial_prompt", type=str, default="こんにちは。元気、ですかー?私は……ちゃんと元気だよ!"
)
parser.add_argument("--speaker_name", type=str, default=None, required=True)
parser.add_argument("--model", type=str, default="large-v3")
args = parser.parse_args()
speaker_name = args.speaker_name
input_dir = args.input_dir
output_file = args.output_file
initial_prompt = args.initial_prompt
model = WhisperModel("large-v3", device="cuda", compute_type="bfloat16")
wav_files = [
os.path.join(input_dir, f) for f in os.listdir(input_dir) if f.endswith(".wav")
]
if os.path.exists(output_file):
print(f"{output_file}が存在するので、バックアップを{output_file}.bakに作成します。")
if os.path.exists(output_file + ".bak"):
print(f"{output_file}.bakも存在するので、削除します。")
os.remove(output_file + ".bak")
os.rename(output_file, output_file + ".bak")
with open(output_file, "w", encoding="utf-8") as f:
for wav_file in tqdm(wav_files, file=sys.stdout):
file_name = os.path.basename(wav_file)
text = transcribe(wav_file, initial_prompt=initial_prompt)
f.write(f"{file_name}|{speaker_name}|JP|{text}\n")

View File

@@ -73,7 +73,7 @@ def load_checkpoint(
# For upgrading from the old version
if "ja_bert_proj" in k:
v = torch.zeros_like(v)
logger.warn(
logger.warning(
f"Seems you are using the old version of the model, the {k} is automatically set to zero for backward compatibility"
)
elif "enc_q" in k and for_infer:
@@ -88,9 +88,7 @@ def load_checkpoint(
else:
model.load_state_dict(new_state_dict, strict=False)
logger.info(
"Loaded checkpoint '{}' (iteration {})".format(checkpoint_path, iteration)
)
logger.info("Loaded '{}' (iteration {})".format(checkpoint_path, iteration))
return model, optimizer, learning_rate, iteration
@@ -165,9 +163,9 @@ def load_safetensors(checkpoint_path, model, for_infer=False):
continue
logger.warning(f"Unexpected key: {key}")
if iteration is None:
logger.info(f"Loaded safetensors '{checkpoint_path}'")
logger.info(f"Loaded '{checkpoint_path}'")
else:
logger.info(f"Loaded safetensors '{checkpoint_path}' (iteration {iteration})")
logger.info(f"Loaded '{checkpoint_path}' (iteration {iteration})")
return model, iteration
@@ -382,7 +380,7 @@ def get_hparams_from_file(config_path):
def check_git_hash(model_dir):
source_dir = os.path.dirname(os.path.realpath(__file__))
if not os.path.exists(os.path.join(source_dir, ".git")):
logger.warn(
logger.warning(
"{} is not a git repository, therefore hash value comparison will be ignored.".format(
source_dir
)
@@ -395,7 +393,7 @@ def check_git_hash(model_dir):
if os.path.exists(path):
saved_hash = open(path).read()
if saved_hash != cur_hash:
logger.warn(
logger.warning(
"git hash values are different. {}(saved) != {}(current)".format(
saved_hash[:8], cur_hash[:8]
)

96
webui_dataset.py Normal file
View File

@@ -0,0 +1,96 @@
import os
import subprocess
import sys
import gradio as gr
python = sys.executable
def subprocess_wrapper(cmd):
return subprocess.run(
cmd,
stdout=sys.stdout,
stderr=subprocess.PIPE,
text=True,
)
def do_slice(model_name):
input_dir = "inputs"
output_dir = os.path.join("Data", model_name, "raw")
result = subprocess_wrapper(
[
python,
"slice.py",
"--input_dir",
input_dir,
"--output_dir",
output_dir,
]
)
return "ターミナルを見て結果を確認してください。"
def do_transcribe(model_name):
input_dir = os.path.join("Data", model_name, "raw")
output_file = os.path.join("Data", model_name, "esd.list")
result = subprocess_wrapper(
[
python,
"transcribe.py",
"--input_dir",
input_dir,
"--output_file",
output_file,
"--speaker_name",
model_name,
]
)
if result.stderr:
return f"{result.stderr}"
return "音声の文字起こしが完了しました。"
initial_md = """
# 学習用データセット作成ツール
Style-Bert-VITS2の学習用データセットを作成するためのツールです。与えられた音声からちょうどいい長さの発話区間を切り取りスライスし、それぞれの音声に対して文字起こしを行います。
## 必要なもの
学習したい音声が入ったwavファイルいくつか。
合計時間がある程度はあったほうがいいかも、10分とかでも大丈夫だったとの報告あり。単一ファイルでも良いし複数ファイルでもよい。
## 使い方
1. `inputs`フォルダ直下にwavファイルをすべて入れる
2. `モデル名`を入力して、`音声のスライス`ボタンを押す
3. 完了したら、`音声の文字起こし`ボタンを押す
細かいパラメータ調整とかがしたい人は、`slice.py`と`transcribe.py`を眺めて直接実行してください。
また、出来上がった音声ファイルたちは`Data/{モデル名}/raw`に、書き起こしファイルは`Data/{モデル名}/esd.list`に保存されます。
**ffmpeg のインストールが別途必要のよう**です、「Couldn't find ffmpeg」とか怒られたら、「Windows ffmpeg インストール」等でググって別途インストールしてください。
"""
with gr.Blocks(theme="NoCrypt/miku") as app:
gr.Markdown(initial_md)
model_name = gr.Textbox(label="モデル名を入力してください(話者名としても使われます)。")
with gr.Row():
slice_button = gr.Button("音声のスライス")
result1 = gr.Textbox(label="結果")
with gr.Row():
transcribe_button = gr.Button("2. 音声の文字起こし")
result2 = gr.Textbox(label="結果")
slice_button.click(
do_slice,
inputs=[model_name],
outputs=[result1],
)
transcribe_button.click(
do_transcribe,
inputs=[model_name],
outputs=[result2],
)
app.launch(inbrowser=True)

View File

@@ -112,6 +112,32 @@ def do_clustering_gradio(n_clusters=4, method="KMeans"):
)
def save_only_mean(model_name):
global mean
if len(x) == 0:
return "Error: スタイルベクトルを読み込んでください。"
result_dir = os.path.join(config.out_dir, model_name)
os.makedirs(result_dir, exist_ok=True)
mean = np.mean(x, axis=0)
style_vectors = np.stack([mean])
style_vector_path = os.path.join(result_dir, "style_vectors.npy")
if os.path.exists(style_vector_path):
return f"{style_vector_path}が既に存在します。削除するか別の名前にバックアップしてください。"
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}が存在しません。"
with open(config_path, "r") as f:
json_dict = json.load(f)
json_dict["data"]["num_styles"] = 1
json_dict["data"]["style2id"] = {"Neutral": 0}
with open(config_path, "w") as f:
json.dump(json_dict, f, indent=2)
return f"成功!\n{style_vector_path}に保存し{config_path}を更新しました。"
def save_style_vectors(model_name, style_names: str):
"""centerとcentroidsを保存する"""
result_dir = os.path.join(config.out_dir, model_name)
@@ -120,7 +146,7 @@ def save_style_vectors(model_name, style_names: str):
style_vector_path = os.path.join(result_dir, "style_vectors.npy")
if os.path.exists(style_vector_path):
return f"{style_vector_path}が既に存在します。削除するか別の名前にバックアップしてください。"
np.save(os.path.join(result_dir, "style_vectors.npy"), style_vectors)
np.save(style_vector_path, style_vectors)
# config.jsonの更新
config_path = os.path.join(result_dir, "config.json")
@@ -142,11 +168,72 @@ def save_style_vectors(model_name, style_names: str):
return f"成功!\n{style_vector_path}に保存し{config_path}を更新しました。"
md1 = """
def save_style_vectors_from_files(model_name, audio_files_text, style_names_text):
"""音声ファイルからスタイルベクトルを作成して保存する"""
global mean
if len(x) == 0:
return "Error: スタイルベクトルを読み込んでください。"
mean = np.mean(x, axis=0)
result_dir = os.path.join(config.out_dir, model_name)
os.makedirs(result_dir, exist_ok=True)
audio_files = audio_files_text.split(",")
style_names = style_names_text.split(",")
if len(audio_files) != len(style_names):
return f"音声ファイルとスタイル名の数が合いません。`,`で正しく{len(style_names)}個に区切られているか確認してください: {audio_files_text}{style_names_text}"
audio_files = [name.strip() for name in audio_files]
style_names = [name.strip() for name in style_names]
style_vectors = [mean]
wavs_dir = os.path.join("Data", 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)
style_vector_path = os.path.join(result_dir, "style_vectors.npy")
if os.path.exists(style_vector_path):
return f"{style_vector_path}が既に存在します。削除するか別の名前にバックアップしてください。"
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_name_list = ["Neutral"]
style_name_list = style_name_list + style_names
assert len(style_name_list) == len(style_vectors)
with open(config_path, "r") 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") as f:
json.dump(json_dict, f, indent=2)
return f"成功!\n{style_vector_path}に保存し{config_path}を更新しました。"
initial_md = """
# Style Bert-VITS2 スタイルベクトルの作成
スタイルを使って音声合成するには、音声ファイルをスタイル別に分け、その各スタイルの特徴を抽出して保存する必要があります。
Style-Bert-VITS2で音声合成するには、スタイルベクトルのファイル`style_vectors.npy`が必要です。これをモデルごとに作成する必要があります。
このプロセスは学習とは全く関係がないので、何回でも独立して繰り返して試せます。また学習中にもたぶん軽いので動くはずです。
## 方法
どうやってスタイルベクトルファイルを作るかはいくつか方法があります。
- 方法1: めんどくさいから平均スタイルのみを使う使えるスタイルは標準のNeutralのみ
- 方法2: 音声ファイルを自動でスタイル別に分け、その各スタイルの平均を取って保存
- 方法3: スタイルを代表する音声ファイルを手動で選んで、その音声のスタイルベクトルを保存
- 方法4: 自分でもっと頑張ってこだわって作るJVNVコーパスなど、もともとスタイルラベル等が利用可能な場合はこれがよいかも
基本的には方法2を使うことを、めんどくさかったりあまり感情に幅がないデータセットなら方法1をおすすめします。
"""
method2 = """
学習の時に取り出したスタイルベクトルを読み込んで、可視化を見ながらスタイルを分けていきます。
手順:
@@ -155,7 +242,6 @@ md1 = """
3. スタイル分けを行って結果を確認
4. スタイルの名前を決めて保存
このプロセスは学習とは関係がないので、何回でも独立して繰り返して試せます。また学習中にもたぶん軽いので動くはずです。
詳細: スタイルベクトル(256次元)たちを適当なアルゴリズムでクラスタリングして、各クラスタの中心のベクトル(と全体の平均ベクトル)を保存します。
@@ -163,62 +249,98 @@ md1 = """
"""
with gr.Blocks(theme="NoCrypt/miku") as app:
gr.Markdown(md1)
gr.Markdown(initial_md)
with gr.Row():
model_name = gr.Textbox("your_model_name", label="モデル名")
load_button = gr.Button("スタイルベクトルを読み込む", variant="primary")
output = gr.Plot(label="音声スタイルの可視化")
load_button.click(load, inputs=[model_name], outputs=[output])
n_clusters = gr.Slider(
minimum=2,
maximum=10,
step=1,
value=4,
label="作るスタイルの数(平均スタイルを除く)",
info="上の図を見ながらスタイルの数を試行錯誤してください。",
)
c_method = gr.Radio(
["Agglomerative after t-SNE", "KMeans after t-SNE", "Agglomerative", "KMeans"],
label="アルゴリズム",
info="分類する(クラスタリング)アルゴリズムを選択します。いろいろ試してみてください。",
value="Agglomerative after t-SNE",
)
c_button = gr.Button("スタイル分けを実行")
audio_list = []
md_list = []
gr.Markdown("スタイル分けの結果と、各スタイルの特徴的な代表音声(図の黒い x 印)")
gr.Markdown("注意: もともと256次元なものをを2次元に落としているので、正確なベクトルの位置関係ではありません。")
with gr.Row():
gr_plot = gr.Plot()
with gr.Tab("方法1: 平均スタイルのみを保存"):
gr.Markdown("平均Neutralスタイルのみを保存する場合は、以下のボタンを押してください。")
with gr.Row():
for i in range(MAX_CLUSTER_NUM):
with gr.Column():
md_list.append(gr.Markdown(visible=False))
audio_list.append(
gr.Audio(
visible=False,
scale=1,
show_label=True,
label=f"スタイル{i+1}",
save_button1 = gr.Button("スタイルベクトルを保存", variant="primary")
info1 = gr.Textbox(label="保存結果")
save_button1.click(save_only_mean, inputs=[model_name], outputs=[info1])
with gr.Tab("方法2: スタイル分けを自動で行う"):
n_clusters = gr.Slider(
minimum=2,
maximum=10,
step=1,
value=4,
label="作るスタイルの数(平均スタイルを除く)",
info="上の図を見ながらスタイルの数を試行錯誤してください。",
)
c_method = gr.Radio(
choices=[
"Agglomerative after t-SNE",
"KMeans after t-SNE",
"Agglomerative",
"KMeans",
],
label="アルゴリズム",
info="分類する(クラスタリング)アルゴリズムを選択します。いろいろ試してみてください。",
value="Agglomerative after t-SNE",
)
c_button = gr.Button("スタイル分けを実行")
audio_list = []
md_list = []
gr.Markdown("スタイル分けの結果と、各スタイルの特徴的な代表音声(図の黒い x 印)")
gr.Markdown("注意: もともと256次元なものをを2次元に落としているので、正確なベクトルの位置関係ではありません。")
with gr.Row():
gr_plot = gr.Plot()
with gr.Row():
for i in range(MAX_CLUSTER_NUM):
with gr.Column():
md_list.append(gr.Markdown(visible=False))
audio_list.append(
gr.Audio(
visible=False,
scale=1,
show_label=True,
label=f"スタイル{i+1}",
)
)
)
c_button.click(
do_clustering_gradio,
inputs=[n_clusters, c_method],
outputs=[gr_plot] + audio_list + md_list,
)
gr.Markdown("結果が良さそうなら、これを保存します。")
with gr.Row():
c_button.click(
do_clustering_gradio,
inputs=[n_clusters, c_method],
outputs=[gr_plot] + audio_list + md_list,
)
gr.Markdown("結果が良さそうなら、これを保存します。")
style_names = gr.Textbox(
"Angry, Sad, Happy",
label="スタイルの名前",
info="スタイルの名前を`,`で区切って入力してください(日本語可)。例: `Angry, Sad, Happy`や`怒り, 悲しみ, 喜び`など。平均音声はNeutralとして自動的に保存されます。",
)
save_button = gr.Button("スタイルベクトルを保存", variant="primary")
info = gr.Textbox(label="保存結果")
with gr.Row():
save_button = gr.Button("スタイルベクトルを保存", variant="primary")
info2 = gr.Textbox(label="保存結果")
save_button.click(
save_style_vectors, inputs=[model_name, style_names], outputs=[info2]
)
with gr.Tab("方法3: 手動でスタイルを選ぶ"):
gr.Markdown("下のテキスト欄に、各スタイルの代表音声のファイル名を`,`区切りで、その横に対応するスタイル名を`,`区切りで入力してください。")
gr.Markdown("例: `angry.wav, sad.wav, happy.wav`と`Angry, Sad, Happy`")
gr.Markdown("注意: Neutralスタイルは自動的に保存されます、手動ではNeutralという名前のスタイルは指定しないでください。")
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_button3 = gr.Button("スタイルベクトルを保存", variant="primary")
info3 = gr.Textbox(label="保存結果")
save_button3.click(
save_style_vectors_from_files,
inputs=[model_name, audio_files_text, style_names_text],
outputs=[info3],
)
with gr.Tab("方法4: がんばる"):
gr.Markdown(
"`clustering.ipynb`にjvnvコーパスの場合の作り方とかクラスタ分けのいろいろを書いています。これを参考に自分で頑張って作ってください。"
)
save_button.click(
save_style_vectors, inputs=[model_name, style_names], outputs=[info]
)
app.launch(inbrowser=True)

View File

@@ -2,10 +2,10 @@ import json
import os
import shutil
import subprocess
import yaml
import sys
import gradio as gr
import sys
import yaml
python = sys.executable
@@ -143,8 +143,23 @@ def train(model_name):
return "Final Step: 学習が完了しました!"
initial_md = """
# Style-Bert-VITS2 学習用WebUI
## 使い方
- データを準備して、各ステップを順に実行してください。進捗状況等はターミナルに表示されます。
- 途中から学習を再開する場合は、モデル名を入力してFinal Stepだけ実行すればよいです。
注意: 音声合成で使うには、スタイルベクトルファイル`style_vectors.npy`を作る必要があります。これは、`Style.bat`を実行してそこで作成してください。
動作は軽いはずなので、学習中でも実行でき、何度でも繰り返して試せます。
"""
prepare_md = """
次のようにデータを置いてください。
まず音声データwavファイルで1ファイルが2-15秒程度の、長すぎず短すぎない発話のものをいくつかと、書き起こしテキストを用意してください。
それを次のように配置します。
```
├── Data
│ ├── {モデルの名前}
@@ -168,13 +183,12 @@ wav_next.wav|taro|JP|はい、聞こえています……。
english_teacher.wav|Mary|EN|How are you? I'm fine, thank you, and you?
...
```
もちろん日本語話者の単一話者データセットでも構いません。
日本語話者の単一話者データセットでも構いません。
"""
if __name__ == "__main__":
with gr.Blocks(theme="NoCrypt/miku") as app:
gr.Markdown("# Style Bert-VITS2 データ前処理")
gr.Markdown("途中から学習を再開する場合は、モデル名を入力してFinal Stepだけ実行すればよいです。")
gr.Markdown(initial_md)
with gr.Accordion(label="データの前準備", open=False):
gr.Markdown(prepare_md)
model_name = gr.Textbox(