ピッチや話速が調整できる音声合成の作り方【ESPNet】【JETS】

# はじめに  
ESPNetの環境構築は終わっている前提です。
環境構築については[以前の記事](https://qiita.com/seichi25/items/fc6c22042f926baa08e6)で書いていますので、そちらを参考にしてください。
半分メモ用途で書いている記事なので、文末表現のぶれなどはご了承ください。
# 概要

## データセットのファイル構造
```
espnet
└── data_root/    # 様々なデータセットを保管しておくディレクトリ.ディレクトリ名はこれで固定
    └── RECIPE_NAME/    # レシピデータディレクトリ
        ├── ITA_corpus/    # データセットディレクトリ
        │   └── speaker_1/    # 話者
        │       └── style_1/    # スタイル
        │           ├── transcripts_utf8.txt    # 書き起こしファイル
        │           └── wav/    # wavが入っているディレクトリ
        │               ├── EMOTION100_001.wav
        │               ├── EMOTION100_002.wav
        │               ...
        └── Rohan_corpus/
            ├── speaker_1/
            │   └── style_1/
            │       ├── transcripts_utf8.txt
            │       └── wav/
            |           ├── ROHAN4600_0001.wav
            |           ├── ROHAN4600_0002.wav
            |           ...
            └── speaker_2/
                └── style_1/
                    ├── transcripts_utf8.txt
                    └── wav/
                        ├── ROHAN4600_0001.wav
                        ├── ROHAN4600_0002.wav
                        ...
```

### data_root
レシピごとのデータセットを保管しておくディレクトリ。
上記の例は、異なるコーパスデータセットでの複数話者学習をするときのファイル構造。
単話者の単データセットの場合でも、「レシピ名＞データセット名＞話者名＞スタイル＞(transcripts_utf8.txt, wav)」の構造を守る。

::: note info
**後述の「create_recipe.sh」を使用する際は、環境変数\\$TTS_DATA_ROOTにこのディレクトリへの絶対パスを登録しておく必要あり。また、環境変数$ESPNET_ROOTに親ディレクトリの「espnet」への絶対パスを登録しておく必要あり。**
:::

### RECIPE_NAME
レシピ内で使用するデータセットをまとめディレクトリ。
ディレクトリ名は任意。
create_recipe.sh(自作したスクリプト)でレシピ名を指定すれば問題ない。

### ITA_corpus/Rohan_corpus
各種データセット。話者ごとのディレクトリを入れておく。ディレクトリ名は任意(_corpusの部分も任意)。

### speaker_*
話者ディレクトリ。その話者のスタイルごとのディレクトリを入れておく。ディレクトリ名は任意。

### style_*
スタイルディレクトリ。そのスタイルの音声ファイル、書き起こしファイルを入れておく。ディレクトリ名は任意。
音声ファイル名は任意だが、「wav」ディレクトリに入れておく必要あり。
書き起こしファイル名は「transcripts_utf8.txt」で統一しておく必要あり。(JVSコーパス準拠)

### transcripts_utf8.txt
「**拡張子を除くファイル名:発話内容**」
という形式で音声ファイルごとに改行区切りで記述します。全角/半角スペースが含まれると後々エラーが出るので、ご注意ください。

**例：**
```text:transcripts_utf8.txt
voice_001:水を3リットル買わなくてはならないのです。
voice_002:流し斬りが完璧に決まれば、たいていの敵は一撃で倒せる。
voice_003:あらゆる幻想を、一部捻じ曲げたのだ。
...
```

## レシピの作成
### create_recipe.sh
自作シェルスクリプトです。使いたい方は[こちら](https://drive.google.com/file/d/1gsbpg6XP7FdL1I3o-KO_WaJUihUPalbe/view?usp=sharing)からDLできます。拡張子が.txtの状態で配布しているので使用する際は適宜変更してください。

::: note warn
**※jetsの複数話者用のレシピが作成されるようになっています**
:::

以下のようにレシピ名を引数で与えて使用します。
```bash
$ ./create_recipe.sh RECIPE_NAME
```

実行するとespnet/egs2/にレシピディレクトリが作成されます。

```
espnet
├── egs2
|   ├── RECIPE_NAME
... |   └── tts1/
    ...     ├── all_run.sh*
            ├── cmd.sh
            ├── conf/
            ├── db.sh -> ../../TEMPLATE/asr1/db.sh*
            ├── inference.py
            ├── local/
            ├── path.sh -> ../../TEMPLATE/tts1/path.sh*
            ├── pyscripts -> ../../TEMPLATE/asr1/pyscripts/
            ├── run.sh*
            ├── scripts -> ../../TEMPLATE/asr1/scripts/
            ├── sid -> ../../../tools/kaldi/egs/sre08/v1/sid/
            ├── steps -> ../../../tools/kaldi/egs/wsj/s5/steps/
            ├── tts.sh -> ../../TEMPLATE/tts1/tts.sh*
            └── utils -> ../../../tools/kaldi/egs/wsj/s5/utils/
```

## 学習の実行
### all_run.sh
espnet/egs2/RECIPE_NAME/tts1まで移動して「all_run.sh」で前処理から学習まですべての処理を実行します。
```
$ ./all_run.sh
```

<details><summary>なぜ「run.sh」じゃなくて謎のラッパーなの？</summary>

ファインチューニングをする際に、「--stage 5」まで（データの前処理）で止めて、事前学習のモデルデータのtoken_listと差し替えて、「--stage 6」から事前学習モデルのパスを指定して実行再開するという手順を踏むので、run.shに直書きするよりもラップするほうが簡潔に書けるかと思ったのと、公式gitのREADMEのファインチューニングの手順どおりの記述になるのでわかりやすいかなというだけで、特に深い意味はありません。
</details>

## 推論
自作のシェルスクリプトとpythonで推論します。「create_recipe.sh」を実行した時点で作成されています。
``--model_file <Path>``と``--train_config <Path>``を指定すれば基本は動きます。
Pathは「tts1」ディレクトリからの相対パスです。
デフォルトのサンプリング周波数が24000なので、モデルの出力に合わせて変える必要があれば``--fs <sampl_rate>``で指定します。
GSTを使っているモデルであれば``--use_gst true``を指定して実行すると、参照データの番号を選択できるようになります。(参照するのはレシピデータで、番号はwav.scpの行番号を表します。)
複数話者で学習したモデルの場合は、``--use_sid true``を指定すると話者番号が選択できるようになります。

**例：JETS複数話者44100Hzのモデル**
```
./inference.sh --model_file exp/tts_train_full_band_jets_gst_spk_harvest_raw_phn_jaconv_pyopenjtalk/3epoch.pth --train_config exp/tts_train_full_band_jets_gst_spk_harvest_raw_phn_jaconv_pyopenjtalk/config.yaml --fs 44100 --use_sid true
```
JETSは音素ごとのピッチや継続長、エナジーなどを指定することができますが、このスクリプトではそれらを調整するインターフェースは提供していません。

<details><summary>余談</summary>

inference.pyもそのままで同じような引数指定で動くようになっているじゃないか、なんでシェルスクリプトかませているんだ、.pyだけでいいだろ、と思われたそこのあなた、ごもっともでございます。espnetのレシピがシェルスクリプトからpython動かしているという構造で、こういうやり方のほうがなんか都合がいいのかなと思って深く考えずに実装した結果です。ご容赦ください...
</details>

生成された音声は、「generated_wav」ディレクトリに保存されます。音声ファイル名は、入力したテキストになります。

```diff
espnet
├── egs2
|   ├── RECIPE_NAME
... |   └── tts1/
    ...     ├── all_run.sh*
            ├── cmd.sh
            ├── conf/
            ├── data/
            ├── db.sh -> ../../TEMPLATE/asr1/db.sh*
            ├── inference.py
+           ├── generated_wav
+               ├── テスト.wav
+               ├── テストです。.wav
                ...
            ├── local/
            ├── path.sh -> ../../TEMPLATE/tts1/path.sh*
            ├── pyscripts -> ../../TEMPLATE/asr1/pyscripts/
            ├── run.sh*
            ├── scripts -> ../../TEMPLATE/asr1/scripts/
            ├── sid -> ../../../tools/kaldi/egs/sre08/v1/sid/
            ├── steps -> ../../../tools/kaldi/egs/wsj/s5/steps/
            ├── tts.sh -> ../../TEMPLATE/tts1/tts.sh*
            └── utils -> ../../../tools/kaldi/egs/wsj/s5/utils/
```



# 参考
https://github.com/espnet/espnet

https://tech.fusic.co.jp/posts/2021-08-20-ml-espnet-tts-2