Feat: slice-and-transcribe
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,3 +14,5 @@ venv/
|
|||||||
|
|
||||||
/pretrained/*.safetensors
|
/pretrained/*.safetensors
|
||||||
/pretrained/*.pth
|
/pretrained/*.pth
|
||||||
|
|
||||||
|
/scripts/test/
|
||||||
|
|||||||
2
App.bat
2
App.bat
@@ -1,9 +1,11 @@
|
|||||||
chcp 65001 > NUL
|
chcp 65001 > NUL
|
||||||
@echo off
|
@echo off
|
||||||
|
|
||||||
|
pushd %~dp0
|
||||||
echo Running app.py...
|
echo Running app.py...
|
||||||
venv\Scripts\python app.py
|
venv\Scripts\python app.py
|
||||||
|
|
||||||
if %errorlevel% neq 0 ( pause & popd & exit /b %errorlevel% )
|
if %errorlevel% neq 0 ( pause & popd & exit /b %errorlevel% )
|
||||||
|
|
||||||
|
popd
|
||||||
pause
|
pause
|
||||||
11
Dataset.bat
Normal file
11
Dataset.bat
Normal 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
|
||||||
@@ -38,7 +38,6 @@ python initialize.py
|
|||||||
### 音声合成
|
### 音声合成
|
||||||
|
|
||||||
`App.bat`をダブルクリックするとWebUIが起動します。
|
`App.bat`をダブルクリックするとWebUIが起動します。
|
||||||
TODO: デフォルトモデルをいくつかダウンロードするようにする
|
|
||||||
|
|
||||||
ディレクトリ構造:
|
ディレクトリ構造:
|
||||||
```
|
```
|
||||||
@@ -74,6 +73,12 @@ model_assets
|
|||||||
- safetensors形式のサポート、デフォルトでsafetensorsを使用するように
|
- safetensors形式のサポート、デフォルトでsafetensorsを使用するように
|
||||||
- その他軽微なbugfixやリファクタリング
|
- その他軽微な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と同じ点
|
## Bert-VITS2 v2.1と同じ点
|
||||||
- [事前学習モデル](https://huggingface.co/litagin/style_bert_vits2_jvnv)は、実質Bert-VITS2 v2.1と同じものを使用しています(不要な重みを削ってsafetensorsに変換したもの)。
|
- [事前学習モデル](https://huggingface.co/litagin/style_bert_vits2_jvnv)は、実質Bert-VITS2 v2.1と同じものを使用しています(不要な重みを削ってsafetensorsに変換したもの)。
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ chcp 65001 > NUL
|
|||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
|
|
||||||
|
pushd %~dp0
|
||||||
echo Running webui_style_vectors.py...
|
echo Running webui_style_vectors.py...
|
||||||
venv\Scripts\python webui_style_vectors.py
|
venv\Scripts\python webui_style_vectors.py
|
||||||
|
|
||||||
if %errorlevel% neq 0 ( pause & popd & exit /b %errorlevel% )
|
if %errorlevel% neq 0 ( pause & popd & exit /b %errorlevel% )
|
||||||
|
|
||||||
|
popd
|
||||||
pause
|
pause
|
||||||
@@ -2,9 +2,12 @@ chcp 65001 > NUL
|
|||||||
|
|
||||||
@echo off
|
@echo off
|
||||||
|
|
||||||
|
pushd %~dp0
|
||||||
|
|
||||||
echo Running webui_train.py...
|
echo Running webui_train.py...
|
||||||
venv\Scripts\python webui_train.py
|
venv\Scripts\python webui_train.py
|
||||||
|
|
||||||
if %errorlevel% neq 0 ( pause & popd & exit /b %errorlevel% )
|
if %errorlevel% neq 0 ( pause & popd & exit /b %errorlevel% )
|
||||||
|
|
||||||
|
popd
|
||||||
pause
|
pause
|
||||||
120
app.py
120
app.py
@@ -1,5 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
import gradio as gr
|
import gradio as gr
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -248,6 +249,13 @@ initial_text = "こんにちは、初めまして。あなたの名前はなん
|
|||||||
|
|
||||||
example_local = [
|
example_local = [
|
||||||
[initial_text, "JP"],
|
[initial_text, "JP"],
|
||||||
|
[
|
||||||
|
"""あなたがそんなこと言うなんて、私はとっても嬉しい。
|
||||||
|
あなたがそんなこと言うなんて、私はとっても怒ってる。
|
||||||
|
あなたがそんなこと言うなんて、私はとっても驚いてる。
|
||||||
|
あなたがそんなこと言うなんて、私はとっても辛い。""",
|
||||||
|
"JP",
|
||||||
|
],
|
||||||
[ # ChatGPTに考えてもらった告白セリフ
|
[ # ChatGPTに考えてもらった告白セリフ
|
||||||
"""私、ずっと前からあなたのことを見てきました。あなたの笑顔、優しさ、強さに、心惹かれていたんです。
|
"""私、ずっと前からあなたのことを見てきました。あなたの笑顔、優しさ、強さに、心惹かれていたんです。
|
||||||
友達として過ごす中で、あなたのことがだんだんと特別な存在になっていくのがわかりました。
|
友達として過ごす中で、あなたのことがだんだんと特別な存在になっていくのがわかりました。
|
||||||
@@ -268,7 +276,7 @@ example_local = [
|
|||||||
"JP",
|
"JP",
|
||||||
],
|
],
|
||||||
[ # ChatGPTと考えた、感情を表すセリフ
|
[ # ChatGPTと考えた、感情を表すセリフ
|
||||||
"""やったー!テストで満点取れたよ!私とっても嬉しいな!
|
"""やったー!テストで満点取れた!私とっても嬉しいな!
|
||||||
どうして私の意見を無視するの?許せない!ムカつく!あんたなんか死ねばいいのに。
|
どうして私の意見を無視するの?許せない!ムカつく!あんたなんか死ねばいいのに。
|
||||||
あはははっ!この漫画めっちゃ笑える、見てよこれ、ふふふ、あはは。
|
あはははっ!この漫画めっちゃ笑える、見てよこれ、ふふふ、あはは。
|
||||||
あなたがいなくなって、私は一人になっちゃって、泣いちゃいそうなほど悲しい。""",
|
あなたがいなくなって、私は一人になっちゃって、泣いちゃいそうなほど悲しい。""",
|
||||||
@@ -306,19 +314,39 @@ example_hf_spaces = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
initial_md = """
|
initial_md = """
|
||||||
# Bert-VITS2 okiba TTS デモ
|
# Style-Bert-VITS2 音声合成
|
||||||
|
|
||||||
[bert_vits2_okiba](https://huggingface.co/litagin/bert_vits2_okiba) のモデルのデモです。
|
注意: 初期からある[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)です。とくに**商用利用は不可**です。
|
||||||
モデル名は[rvc_okiba](https://huggingface.co/litagin/rvc_okiba)のモデル名と対応しています。
|
"""
|
||||||
モデルは随時追加していきます。現在のモデルはすべてBert-VITS2のver 2.1のものです。
|
|
||||||
|
|
||||||
**定形サンプルは[こちら](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で動くので、何故かやたら遅いことが多かったりなんか不安定で動かないときもあるみたいです。
|
上2つは`Train.bat`による学習で自動的に正しい位置に保存されます。`style_vectors.npy`は`Style.bat`を実行して指示に従って生成してください。
|
||||||
- huggingface上では最大100文字にしています。
|
|
||||||
- Style textの実装あたりで本家の内部コードを改造しているので、このapp.pyをそのまま本家に使っても今のところは動きません。
|
|
||||||
|
|
||||||
現在のところはspeaker_id = 0に固定しています。
|
TODO: 現在のところはspeaker_id = 0に固定しており複数話者の合成には対応していません。
|
||||||
|
"""
|
||||||
|
|
||||||
|
style_md = """
|
||||||
|
- プリセットまたは音声ファイルから読み上げの声音・感情・スタイルのようなものを制御できます。
|
||||||
|
- デフォルトのNeutralでも、十分に読み上げる文に応じた感情で感情豊かに読み上げられます。このスタイル制御は、それを重み付きで上書きするような感じです。
|
||||||
|
- 重みを大きくしすぎると発音が変になったり声にならなかったりと崩壊することがあります。
|
||||||
|
- 音声ファイルを入力する場合は、学習データと似た声音の話者(特に同じ性別)でないとよい効果が出ないかもしれません。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -331,8 +359,8 @@ def make_non_interactive():
|
|||||||
|
|
||||||
|
|
||||||
def gr_util(item):
|
def gr_util(item):
|
||||||
if item == "クラスタから選ぶ":
|
if item == "プリセットから選ぶ":
|
||||||
return (gr.update(visible=True), gr.update(visible=False))
|
return (gr.update(visible=True), gr.Audio(visible=False, value=None))
|
||||||
else:
|
else:
|
||||||
return (gr.update(visible=False), gr.update(visible=True))
|
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
|
examples = example_hf_spaces if is_hf_spaces else example_local
|
||||||
|
|
||||||
model_names = model_holder.model_names
|
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_id = 1 if is_hf_spaces else 0
|
||||||
initial_pth_files = model_holder.model_files_dict[model_names[initial_id]]
|
initial_pth_files = model_holder.model_files_dict[model_names[initial_id]]
|
||||||
|
|
||||||
with gr.Blocks(theme="NoCrypt/miku") as app:
|
with gr.Blocks(theme="NoCrypt/miku") as app:
|
||||||
gr.Markdown(initial_md)
|
gr.Markdown(initial_md)
|
||||||
|
with gr.Accordion(label="使い方", open=False):
|
||||||
|
gr.Markdown(how_to_md)
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
with gr.Column():
|
with gr.Column():
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
@@ -376,35 +409,17 @@ if __name__ == "__main__":
|
|||||||
choices=initial_pth_files,
|
choices=initial_pth_files,
|
||||||
value=initial_pth_files[0],
|
value=initial_pth_files[0],
|
||||||
)
|
)
|
||||||
refresh_button = gr.Button(
|
refresh_button = gr.Button("更新", scale=1, visible=not is_hf_spaces)
|
||||||
"モデル一覧を更新", scale=1, visible=not is_hf_spaces
|
load_button = gr.Button("ロード", scale=1, variant="primary")
|
||||||
)
|
|
||||||
load_button = gr.Button("モデルをロード", scale=1)
|
|
||||||
text_input = gr.TextArea(label="テキスト", value=initial_text)
|
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)
|
line_split = gr.Checkbox(label="改行で分けて生成", value=True)
|
||||||
split_interval = gr.Slider(
|
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")
|
language = gr.Dropdown(choices=languages, value="JP", label="Language")
|
||||||
with gr.Accordion(label="詳細設定", open=False):
|
with gr.Accordion(label="詳細設定", open=False):
|
||||||
@@ -420,14 +435,36 @@ if __name__ == "__main__":
|
|||||||
length_scale = gr.Slider(
|
length_scale = gr.Slider(
|
||||||
minimum=0.1, maximum=2, value=1.0, step=0.1, label="Length"
|
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.Column():
|
||||||
|
with gr.Accordion("スタイルについて詳細", open=False):
|
||||||
|
gr.Markdown(style_md)
|
||||||
style_mode = gr.Radio(
|
style_mode = gr.Radio(
|
||||||
["クラスタから選ぶ", "音声ファイルを入力"],
|
["プリセットから選ぶ", "音声ファイルを入力"],
|
||||||
label="スタイルの指定方法",
|
label="スタイルの指定方法",
|
||||||
value="クラスタから選ぶ",
|
value="プリセットから選ぶ",
|
||||||
)
|
)
|
||||||
style = gr.Dropdown(
|
style = gr.Dropdown(
|
||||||
label="スタイル(0が平均スタイル)", choices=list(range(7)), value=0
|
label="スタイル(Neutralが平均スタイル)", choices=["モデルをロードしてください"], value=0
|
||||||
)
|
)
|
||||||
style_weight = gr.Slider(
|
style_weight = gr.Slider(
|
||||||
minimum=0,
|
minimum=0,
|
||||||
@@ -442,7 +479,8 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
text_output = gr.Textbox(label="情報")
|
text_output = gr.Textbox(label="情報")
|
||||||
audio_output = gr.Audio(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_button.click(
|
||||||
tts_fn,
|
tts_fn,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -205,7 +205,7 @@ class Config:
|
|||||||
print(
|
print(
|
||||||
"If you have no special needs, please do not modify default_config.yml."
|
"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:
|
with open(file=config_path, mode="r", encoding="utf-8") as file:
|
||||||
yaml_config: Dict[str, any] = yaml.safe_load(file.read())
|
yaml_config: Dict[str, any] = yaml.safe_load(file.read())
|
||||||
model_name: str = yaml_config["model_name"]
|
model_name: str = yaml_config["model_name"]
|
||||||
|
|||||||
@@ -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_bert_models()
|
||||||
|
|
||||||
download_pretrained_models()
|
download_pretrained_models()
|
||||||
|
|
||||||
|
download_jvnv_models()
|
||||||
|
|||||||
2
inputs/.gitignore
vendored
Normal file
2
inputs/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
cmudict
|
cmudict
|
||||||
cn2an
|
cn2an
|
||||||
|
faster-whisper>=0.10.0
|
||||||
g2p_en
|
g2p_en
|
||||||
GPUtil
|
GPUtil
|
||||||
gradio
|
gradio
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ if __name__ == "__main__":
|
|||||||
twople = (spk_dir, filename, args)
|
twople = (spk_dir, filename, args)
|
||||||
tasks.append(twople)
|
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
|
pass
|
||||||
|
|
||||||
pool.close()
|
pool.close()
|
||||||
|
|||||||
@@ -58,6 +58,76 @@
|
|||||||
"source": [
|
"source": [
|
||||||
"save_file(new_dict, f\"pretrained/{model_name.replace('.pth', '.safetensors')}\")"
|
"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": {
|
"metadata": {
|
||||||
|
|||||||
119
slice.py
Normal file
119
slice.py
Normal 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.")
|
||||||
@@ -10,7 +10,7 @@ logger.remove()
|
|||||||
|
|
||||||
# 自定义格式并添加到标准输出
|
# 自定义格式并添加到标准输出
|
||||||
log_format = (
|
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)
|
logger.add(sys.stdout, format=log_format, backtrace=True, diagnose=True)
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ def run():
|
|||||||
scheduler_dur_disc.step()
|
scheduler_dur_disc.step()
|
||||||
|
|
||||||
if epoch == hps.train.epochs:
|
if epoch == hps.train.epochs:
|
||||||
|
# Save the final models
|
||||||
utils.save_checkpoint(
|
utils.save_checkpoint(
|
||||||
net_g,
|
net_g,
|
||||||
optim_g,
|
optim_g,
|
||||||
@@ -379,8 +380,9 @@ def run():
|
|||||||
epoch,
|
epoch,
|
||||||
os.path.join(
|
os.path.join(
|
||||||
out_dir,
|
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,
|
config.model_name,
|
||||||
f"{config.model_name}_e{epoch}_s{global_step}.safetensors",
|
f"{config.model_name}_e{epoch}_s{global_step}.safetensors",
|
||||||
),
|
),
|
||||||
|
for_infer=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
global_step += 1
|
global_step += 1
|
||||||
|
|||||||
51
transcribe.py
Normal file
51
transcribe.py
Normal 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")
|
||||||
14
utils.py
14
utils.py
@@ -73,7 +73,7 @@ def load_checkpoint(
|
|||||||
# For upgrading from the old version
|
# For upgrading from the old version
|
||||||
if "ja_bert_proj" in k:
|
if "ja_bert_proj" in k:
|
||||||
v = torch.zeros_like(v)
|
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"
|
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:
|
elif "enc_q" in k and for_infer:
|
||||||
@@ -88,9 +88,7 @@ def load_checkpoint(
|
|||||||
else:
|
else:
|
||||||
model.load_state_dict(new_state_dict, strict=False)
|
model.load_state_dict(new_state_dict, strict=False)
|
||||||
|
|
||||||
logger.info(
|
logger.info("Loaded '{}' (iteration {})".format(checkpoint_path, iteration))
|
||||||
"Loaded checkpoint '{}' (iteration {})".format(checkpoint_path, iteration)
|
|
||||||
)
|
|
||||||
|
|
||||||
return model, optimizer, learning_rate, iteration
|
return model, optimizer, learning_rate, iteration
|
||||||
|
|
||||||
@@ -165,9 +163,9 @@ def load_safetensors(checkpoint_path, model, for_infer=False):
|
|||||||
continue
|
continue
|
||||||
logger.warning(f"Unexpected key: {key}")
|
logger.warning(f"Unexpected key: {key}")
|
||||||
if iteration is None:
|
if iteration is None:
|
||||||
logger.info(f"Loaded safetensors '{checkpoint_path}'")
|
logger.info(f"Loaded '{checkpoint_path}'")
|
||||||
else:
|
else:
|
||||||
logger.info(f"Loaded safetensors '{checkpoint_path}' (iteration {iteration})")
|
logger.info(f"Loaded '{checkpoint_path}' (iteration {iteration})")
|
||||||
return model, iteration
|
return model, iteration
|
||||||
|
|
||||||
|
|
||||||
@@ -382,7 +380,7 @@ def get_hparams_from_file(config_path):
|
|||||||
def check_git_hash(model_dir):
|
def check_git_hash(model_dir):
|
||||||
source_dir = os.path.dirname(os.path.realpath(__file__))
|
source_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
if not os.path.exists(os.path.join(source_dir, ".git")):
|
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(
|
"{} is not a git repository, therefore hash value comparison will be ignored.".format(
|
||||||
source_dir
|
source_dir
|
||||||
)
|
)
|
||||||
@@ -395,7 +393,7 @@ def check_git_hash(model_dir):
|
|||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
saved_hash = open(path).read()
|
saved_hash = open(path).read()
|
||||||
if saved_hash != cur_hash:
|
if saved_hash != cur_hash:
|
||||||
logger.warn(
|
logger.warning(
|
||||||
"git hash values are different. {}(saved) != {}(current)".format(
|
"git hash values are different. {}(saved) != {}(current)".format(
|
||||||
saved_hash[:8], cur_hash[:8]
|
saved_hash[:8], cur_hash[:8]
|
||||||
)
|
)
|
||||||
|
|||||||
96
webui_dataset.py
Normal file
96
webui_dataset.py
Normal 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)
|
||||||
@@ -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):
|
def save_style_vectors(model_name, style_names: str):
|
||||||
"""centerとcentroidsを保存する"""
|
"""centerとcentroidsを保存する"""
|
||||||
result_dir = os.path.join(config.out_dir, model_name)
|
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")
|
style_vector_path = os.path.join(result_dir, "style_vectors.npy")
|
||||||
if os.path.exists(style_vector_path):
|
if os.path.exists(style_vector_path):
|
||||||
return f"{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.jsonの更新
|
||||||
config_path = os.path.join(result_dir, "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}を更新しました。"
|
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-Bert-VITS2で音声合成するには、スタイルベクトルのファイル`style_vectors.npy`が必要です。これをモデルごとに作成する必要があります。
|
||||||
|
このプロセスは学習とは全く関係がないので、何回でも独立して繰り返して試せます。また学習中にもたぶん軽いので動くはずです。
|
||||||
|
|
||||||
|
## 方法
|
||||||
|
|
||||||
|
どうやってスタイルベクトルファイルを作るかはいくつか方法があります。
|
||||||
|
- 方法1: めんどくさいから平均スタイルのみを使う(使えるスタイルは標準のNeutralのみ)
|
||||||
|
- 方法2: 音声ファイルを自動でスタイル別に分け、その各スタイルの平均を取って保存
|
||||||
|
- 方法3: スタイルを代表する音声ファイルを手動で選んで、その音声のスタイルベクトルを保存
|
||||||
|
- 方法4: 自分でもっと頑張ってこだわって作る(JVNVコーパスなど、もともとスタイルラベル等が利用可能な場合はこれがよいかも)
|
||||||
|
|
||||||
|
基本的には方法2を使うことを、めんどくさかったりあまり感情に幅がないデータセットなら方法1をおすすめします。
|
||||||
|
"""
|
||||||
|
|
||||||
|
method2 = """
|
||||||
学習の時に取り出したスタイルベクトルを読み込んで、可視化を見ながらスタイルを分けていきます。
|
学習の時に取り出したスタイルベクトルを読み込んで、可視化を見ながらスタイルを分けていきます。
|
||||||
|
|
||||||
手順:
|
手順:
|
||||||
@@ -155,7 +242,6 @@ md1 = """
|
|||||||
3. スタイル分けを行って結果を確認
|
3. スタイル分けを行って結果を確認
|
||||||
4. スタイルの名前を決めて保存
|
4. スタイルの名前を決めて保存
|
||||||
|
|
||||||
このプロセスは学習とは関係がないので、何回でも独立して繰り返して試せます。また学習中にもたぶん軽いので動くはずです。
|
|
||||||
|
|
||||||
詳細: スタイルベクトル(256次元)たちを適当なアルゴリズムでクラスタリングして、各クラスタの中心のベクトル(と全体の平均ベクトル)を保存します。
|
詳細: スタイルベクトル(256次元)たちを適当なアルゴリズムでクラスタリングして、各クラスタの中心のベクトル(と全体の平均ベクトル)を保存します。
|
||||||
|
|
||||||
@@ -163,12 +249,19 @@ md1 = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
with gr.Blocks(theme="NoCrypt/miku") as app:
|
with gr.Blocks(theme="NoCrypt/miku") as app:
|
||||||
gr.Markdown(md1)
|
gr.Markdown(initial_md)
|
||||||
with gr.Row():
|
with gr.Row():
|
||||||
model_name = gr.Textbox("your_model_name", label="モデル名")
|
model_name = gr.Textbox("your_model_name", label="モデル名")
|
||||||
load_button = gr.Button("スタイルベクトルを読み込む", variant="primary")
|
load_button = gr.Button("スタイルベクトルを読み込む", variant="primary")
|
||||||
output = gr.Plot(label="音声スタイルの可視化")
|
output = gr.Plot(label="音声スタイルの可視化")
|
||||||
load_button.click(load, inputs=[model_name], outputs=[output])
|
load_button.click(load, inputs=[model_name], outputs=[output])
|
||||||
|
with gr.Tab("方法1: 平均スタイルのみを保存"):
|
||||||
|
gr.Markdown("平均(Neutral)スタイルのみを保存する場合は、以下のボタンを押してください。")
|
||||||
|
with gr.Row():
|
||||||
|
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(
|
n_clusters = gr.Slider(
|
||||||
minimum=2,
|
minimum=2,
|
||||||
maximum=10,
|
maximum=10,
|
||||||
@@ -178,7 +271,12 @@ with gr.Blocks(theme="NoCrypt/miku") as app:
|
|||||||
info="上の図を見ながらスタイルの数を試行錯誤してください。",
|
info="上の図を見ながらスタイルの数を試行錯誤してください。",
|
||||||
)
|
)
|
||||||
c_method = gr.Radio(
|
c_method = gr.Radio(
|
||||||
["Agglomerative after t-SNE", "KMeans after t-SNE", "Agglomerative", "KMeans"],
|
choices=[
|
||||||
|
"Agglomerative after t-SNE",
|
||||||
|
"KMeans after t-SNE",
|
||||||
|
"Agglomerative",
|
||||||
|
"KMeans",
|
||||||
|
],
|
||||||
label="アルゴリズム",
|
label="アルゴリズム",
|
||||||
info="分類する(クラスタリング)アルゴリズムを選択します。いろいろ試してみてください。",
|
info="分類する(クラスタリング)アルゴリズムを選択します。いろいろ試してみてください。",
|
||||||
value="Agglomerative after t-SNE",
|
value="Agglomerative after t-SNE",
|
||||||
@@ -208,17 +306,41 @@ with gr.Blocks(theme="NoCrypt/miku") as app:
|
|||||||
outputs=[gr_plot] + audio_list + md_list,
|
outputs=[gr_plot] + audio_list + md_list,
|
||||||
)
|
)
|
||||||
gr.Markdown("結果が良さそうなら、これを保存します。")
|
gr.Markdown("結果が良さそうなら、これを保存します。")
|
||||||
with gr.Row():
|
|
||||||
style_names = gr.Textbox(
|
style_names = gr.Textbox(
|
||||||
"Angry, Sad, Happy",
|
"Angry, Sad, Happy",
|
||||||
label="スタイルの名前",
|
label="スタイルの名前",
|
||||||
info="スタイルの名前を`,`で区切って入力してください(日本語可)。例: `Angry, Sad, Happy`や`怒り, 悲しみ, 喜び`など。平均音声はNeutralとして自動的に保存されます。",
|
info="スタイルの名前を`,`で区切って入力してください(日本語可)。例: `Angry, Sad, Happy`や`怒り, 悲しみ, 喜び`など。平均音声はNeutralとして自動的に保存されます。",
|
||||||
)
|
)
|
||||||
|
with gr.Row():
|
||||||
save_button = gr.Button("スタイルベクトルを保存", variant="primary")
|
save_button = gr.Button("スタイルベクトルを保存", variant="primary")
|
||||||
info = gr.Textbox(label="保存結果")
|
info2 = gr.Textbox(label="保存結果")
|
||||||
|
|
||||||
save_button.click(
|
save_button.click(
|
||||||
save_style_vectors, inputs=[model_name, style_names], outputs=[info]
|
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コーパスの場合の作り方とかクラスタ分けのいろいろを書いています。これを参考に自分で頑張って作ってください。"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
app.launch(inbrowser=True)
|
app.launch(inbrowser=True)
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import json
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import yaml
|
import sys
|
||||||
|
|
||||||
import gradio as gr
|
import gradio as gr
|
||||||
import sys
|
import yaml
|
||||||
|
|
||||||
python = sys.executable
|
python = sys.executable
|
||||||
|
|
||||||
@@ -143,8 +143,23 @@ def train(model_name):
|
|||||||
return "Final Step: 学習が完了しました!"
|
return "Final Step: 学習が完了しました!"
|
||||||
|
|
||||||
|
|
||||||
|
initial_md = """
|
||||||
|
# Style-Bert-VITS2 学習用WebUI
|
||||||
|
|
||||||
|
## 使い方
|
||||||
|
|
||||||
|
- データを準備して、各ステップを順に実行してください。進捗状況等はターミナルに表示されます。
|
||||||
|
|
||||||
|
- 途中から学習を再開する場合は、モデル名を入力してFinal Stepだけ実行すればよいです。
|
||||||
|
|
||||||
|
注意: 音声合成で使うには、スタイルベクトルファイル`style_vectors.npy`を作る必要があります。これは、`Style.bat`を実行してそこで作成してください。
|
||||||
|
動作は軽いはずなので、学習中でも実行でき、何度でも繰り返して試せます。
|
||||||
|
"""
|
||||||
|
|
||||||
prepare_md = """
|
prepare_md = """
|
||||||
次のようにデータを置いてください。
|
まず音声データ(wavファイルで1ファイルが2-15秒程度の、長すぎず短すぎない発話のものをいくつか)と、書き起こしテキストを用意してください。
|
||||||
|
|
||||||
|
それを次のように配置します。
|
||||||
```
|
```
|
||||||
├── Data
|
├── 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?
|
english_teacher.wav|Mary|EN|How are you? I'm fine, thank you, and you?
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
もちろん日本語話者の単一話者データセットでも構いません。
|
日本語話者の単一話者データセットでも構いません。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
with gr.Blocks(theme="NoCrypt/miku") as app:
|
with gr.Blocks(theme="NoCrypt/miku") as app:
|
||||||
gr.Markdown("# Style Bert-VITS2 データ前処理")
|
gr.Markdown(initial_md)
|
||||||
gr.Markdown("途中から学習を再開する場合は、モデル名を入力してFinal Stepだけ実行すればよいです。")
|
|
||||||
with gr.Accordion(label="データの前準備", open=False):
|
with gr.Accordion(label="データの前準備", open=False):
|
||||||
gr.Markdown(prepare_md)
|
gr.Markdown(prepare_md)
|
||||||
model_name = gr.Textbox(
|
model_name = gr.Textbox(
|
||||||
|
|||||||
Reference in New Issue
Block a user