ESPNetで作るキャラクター音声合成

# 注意
本記事はOSがUbuntuであることを前提としております。他OSを使用する方はご注意ください。加えて機械学習を行うため、特にGPUに関してはそれなりのスペックが必要です。PCメモリに関しましてもデフォルトの設定ですと16GBでは不足するという事例を確認しています。（対処法も確認済み）
また、本記事ではゲーム等の音源をもとに音声合成を作成する方法を紹介しますが、本記事はそれらが合法であることを保証するものではありません。実践する際は自己責任でお願いします。

# 概要
本記事ではプログラミングに関する深い知識が無くても、好きな音声データで高品質な音声合成モデルを作成する方法を紹介します。大まかな手順は以下の通りです。
　**1. 音声データを録音、分割して学習用のデータセットを作成する**
　**2. 作成したデータセットを用いてESPNetで音声合成モデルの学習を行う**
OSがUbuntuである必要があるのは、ESPNetの動作環境に起因します。後者の手順の代替手段を用意すれば他OSでも音声合成モデルの作成は可能です。

# データセットの作成
まず学習に必要なデータセットを作成する必要があります。音声を録音後各wavファイルに対応したテキストデータを作成する必要があります。音声の録音及び分割についてはseichiさんが記事を出してくださっているので[こちら](https://qiita.com/seichi25/private/f15e301a95a32f3881f6#threshold)を参考にしてください。

https://qiita.com/seichi25/private/f15e301a95a32f3881f6#threshold

ここでは主にフォルダの構成やテキストデータの記法について説明していきます。まず、フォルダは以下のように構成してください。

```
CHRACTER_voice_data
    |
    |ーーー voice_text.txt
    |
    |ーーー voice 
             |
             |ーーー CHARACTER_001.wav
             |ーーー CHARACTER_002.wav
             |ーーー CHARACTER_003.wav
             ...
```

CHARACTERに関しては各自好きに置き換えていただいて構いません。置き換えた場合は後に実行する手順の際にも忘れずに同様に置き換えてください。その他の部分は上図に沿って名前を設定することを推奨します。そうでない場合はdata.shとdata_prep.shを修正する必要が出てきます。また、voice_text.txtは以下のように作成してください。

**voice_text.txt**

```
CHARACTER001:ワッフルといえばコレですわ。
CHARACTER002:種類いっぱいありますけどもチョコですわ。
CHARACTER003:これだけあれば勝ちですわ。
...
```


# ESPNetについて
ESPnetとは、End-to-End(E2E)型の音声処理モデルの研究を加速させるべく開発されたオープンソースツールキットです。TTS(Text-to-Speech:音声合成)やASR(Automated Speech Recognition:音声認識)などの音声処理のタスクをサポートしています。また、大きな特徴としてRecipeという構造を持っています。このRecipeこそ音声合成モデルの作成を大幅に簡略してくれる最大の要因です。

## Recipeとは
Recipeは音声合成モデルなどを作成するための前処理や学習といった手順が含まれたシェルスクリプトのことを指します。それらを実行していくことで、複雑なコードを書かずとも音声合成モデルを作成することができます。自前のデータセットを使用する際にはいくらか手を加える必要はありますが、そこまで終えられれば後はほとんど自動で音声合成モデルを作成することができます。また、RecipeはいくつかのStageに細分化されています。各stageは以下の通りです

Stage1:データディレクトリを生成する
　ここで実行するdata.shとdata_prep.shを予め作成する必要があります。
Stage2:特徴量を抽出する
Stage3:発話のフィルタリングを行う
Stage4:トークンリストを作成する
Stage5:音声合成モデルのための統計量を計算
Stage6:音声合成モデルの学習を行う
　音声合成モデルには様々なモデルが存在します。自分の使用したいモデルに合わせて指定するファイルのパスを変更する必要があります。また、後述しますがFine tuningを行うように設定するとモデルの精度を高めることができます。

## モデルの選択について
音声合成モデルには既にたくさんのモデルが存在しています。有名なものとしてはTacotron2やWaveNetなどがあります。使用するモデルによって完成した際の音声合成モデルの品質は大きく変化します。本記事では現在(2021/12月)最もおすすめなモデルとしてVITSをご紹介します。VITSを推奨している理由は主に以下の通りです。
まず、VITSが生成する音声の品質が現状最高レベルであるためです。他モデルと比較してもノイズが非常に少ない綺麗な音声を生成することが可能です。また、VITSのFine tuning用のモデルが配布されているため、後述するFine tuningを簡単に行うことができます。最も品質の高い音声合成モデルを作成したいのならば、VITSを使用することを推奨します。
次にVITSは従来のモデルと違い、言語的特徴量から直接音声を生成するという利点があります。従来のモデルは、言語的特徴量から音響的特徴量を生成する「音響モデル」と音響的特徴量から音声を作成する「ボコーダー」という2段階の構造でした。学習をそれぞれ行わないといけないうえ、使用する際も2つのモデルが必要なので少々手間が増えます。それに対してVITSは単体で完結しているため、学習や使用をする際に比較的手軽だという利点もあります。
ESPNetのVITSのモデルには22.05hzのものと44.1ｈzの2種類がありますが、違いとしては後者のほうが品質が良い代わりに重いモデルという点です。お手持ちのPCのスペックに合わせてどちらを使用するか決めることを推奨します。

## Fine tunigについて
完成時のモデルの品質を高めるためにFine tuningを行うことを推奨します。Fine tuningとは事前に他のデータセットで一度学習を行ってから実際に使用したいデータセットを学習させるという手法です。品質が向上するということに感しては様々な理由がありますが、厳密性をもとめないのならばFine tuningを行う場合のほうが初期値が優れていると考えていただければ問題ないです。
Fine tuningにおすすめなデータセットはJSUTというコーパスです。日本語女性単独話者の音声データが5000文以上、時間にして計10時間ほど入っています。推奨する理由は御覧の通りその圧倒的なデータ量とその品質です。また、ESPNetでFine tuningするためのJSUTで学習されたモデルが配布されているので、自分で学習を行わなくてもそちらをダウンロードすることで簡単にFine tuningを行うことができます。

# ESPNetの使用方法
今回は初期の環境構築については説明を省かせていただきます。最後に記載している参考文献等で確認してください。
まず次のコマンドを実行してESPNetが動かせる状態にしましょう。ファイルのパスは自信の設定に合わせて適宜書き換えておいてください。今回はVITSの44.1khzの場合に合わせて記載しています。22khzの場合に関しましてはESPNetのGithubページにコマンド例が記載されていますのでそちらと合わせて実行してみてください。

```
$ cd espnet/tools
$ . ./activate_python.sh
```

次に実行用のフォルダと実行に必要なdata.shとdata_prep.shを作成していきます。データセット作成時にCHARACTERを置き換えて人は同様のものに置き換えてください。

```
$ cd ..
$ egs2/TEMPLATE/tts1/setup.sh egs2/CHARACTER/tts1
```

/espnet/egs2/CHARACTER/tts1/localの中に次の２つのファイルを作成してください。手段はしていしませんが、VScodeを使用すれば楽にできると思います。また、data.shの後半にある40と20は変更可能な変数です。それぞれ目安はデータ数の1/10と1/20程度です。特にこだわりがなければいじらなくても問題ありません。データセットのファイル名やフォルダ名は前半部で紹介した規則に従っていない場合は各自で適宜コードを修正してください。

<details><summary>**data.sh**</summary><div>

```
#!/usr/bin/env bash

set -e
set -u
set -o pipefail

log() {
    local fname=${BASH_SOURCE[1]##*/}
    echo -e "$(date '+%Y-%m-%dT%H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*"
}
SECONDS=0

stage=0
stop_stage=2
threshold=45
nj=8

log "$0 $*"
# shellcheck disable=SC1091
. utils/parse_options.sh

if [ $# -ne 0 ]; then
    log "Error: No positional arguments are required."
    exit 2
fi

# shellcheck disable=SC1091
. ./path.sh || exit 1;
# shellcheck disable=SC1091
. ./cmd.sh || exit 1;
# shellcheck disable=SC1091
. ./db.sh || exit 1;

if [ -z "${CHARACTER}" ]; then
   log "Fill the value of 'CHARACTER' of db.sh"
   exit 1
fi

db_root=${CHARACTER}
train_set=tr_no_dev
dev_set=dev
eval_set=eval1


if [ ${stage} -le 0 ] && [ ${stop_stage} -ge 0 ]; then
    log "stage 0: local/data_prep.sh"
    local/data_prep.sh "${db_root}/CHARACTER_voice_data" data/all
fi

if [ ${stage} -le 1 ] && [ ${stop_stage} -ge 1 ]; then
    log "stage 1: scripts/audio/trim_silence.sh"
    # shellcheck disable=SC2154
    scripts/audio/trim_silence.sh \
        --cmd "${train_cmd}" \
        --nj "${nj}" \
        --fs 44100 \
        --win_length 2048 \
        --shift_length 512 \
        --threshold "${threshold}" \
        data/all data/all/log
fi

if [ ${stage} -le 2 ] && [ ${stop_stage} -ge 2 ]; then
    log "stage 2: utils/subset_data_dir.sh"
    utils/subset_data_dir.sh data/all 40 data/deveval
    utils/subset_data_dir.sh --first data/deveval 20 "data/${dev_set}"
    utils/subset_data_dir.sh --last data/deveval 20 "data/${eval_set}"
    utils/copy_data_dir.sh data/all "data/${train_set}"
    utils/filter_scp.pl --exclude data/deveval/wav.scp \
        data/all/wav.scp > "data/${train_set}/wav.scp"
    utils/fix_data_dir.sh "data/${train_set}"
fi
log "Successfully finished. [elapsed=${SECONDS}s]"
```
</div></details>

<details><summary>**data_prep.sh**</summary><div>

```
#!/usr/bin/env bash

db_root=$1
data_dir=$2

# check arguments
if [ $# != 2 ]; then
    echo "Usage: $0 <db_root> <data_dir>"
    echo "e.g.: $0 CHARACTER_voice_data/train"
    exit 1
fi

set -euo pipefail

# check directory existence
[ ! -e "${data_dir}" ] && mkdir -p "${data_dir}"

# set filenames
scp=${data_dir}/wav.scp
utt2spk=${data_dir}/utt2spk
spk2utt=${data_dir}/spk2utt
text=${data_dir}/text

# check file existence
[ -e "${scp}" ] && rm "${scp}"
[ -e "${utt2spk}" ] && rm "${utt2spk}"
[ -e "${text}" ] && rm "${text}"

# make scp, utt2spk, and spk2utt
find "${db_root}/voice" -name "*.wav" | sort | while read -r filename; do
    id=CHARACTER_$(basename "${filename}" | sed -e "s/\.[^\.]*$//g")
    echo "${id} sox \"${filename}\" -r 44100 -t wav -c 1 -b 16 - |" >> "${scp}"
    echo "${id} CHARACTER" >> "${utt2spk}"
done
utils/utt2spk_to_spk2utt.pl "${utt2spk}" > "${spk2utt}"
echo "Successfully finished making wav.scp, utt2spk, spk2utt."

# make text
find "${db_root}" -name "*.txt" | grep "voice_text" | while read -r filename; do
    awk -F ":" -v spk=CHARACTER '{print spk "_" $1 " " $2}' < "${filename}" | sort >> "${text}"
done
echo "Successfully finished making text."

utils/fix_data_dir.sh "${data_dir}"
echo "Successfully finished preparing data directory."
```
</div></details>

また実行の都合上/espnet/egs2/CHARACTER/tts1にあるdb.shに *CHRACTER=dataset* を追記する必要があります。下の例では110行目付近に追記していますが、基本的にそれらしい場所に書いておけば問題ありません。

```
…
MUCS_SUBTASK2=downloads
GAMAYUN=downloads
IWSLT21LR=downloads/iwslt21
CHARACTER=dataset
```

後は/espnet/egs2/CHARACTER/tts1内にdatasetという名前でフォルダを作成してその中に自分で作成したデータセットのフォルダを入れておきましょう。また権限の問題でエラーが発生しないように次の2つのコマンドを実行しておきます。

```
$ cd egs2/CHARACTER/tts1
$ chmod +x ./local/data.sh
$ chmod +x ./local/data_prep.sh
```

最後に実行に必要なファイルやフォルダをコピーします。これで下準備はほとんど終了です。

```
$ cp -r  /espnet/egs2/tsukuyomi/tts1/conf/tuning conf/
$ cp /espnet/egs2/tsukuyomi/tts1/run.sh .
```

それではまずstage1からstage5までを実行します。以下のコマンドを実行してください。時間はかかりますが、エラーが発生しなければ上手くいっているので問題ありません。エラーが出た際は以前の部分に間違いがないか見直してみてください。

```
$ ./run.sh \
      --stage 1 \
      --stop-stage 5 \
      --g2p pyopenjtalk_accent_with_pause \
      --min_wav_duration 0.38 \
      --fs 44100 \
      --n_fft 2048 \
      --n_shift 512 \
      --dumpdir dump/44k \
      --win_length null \
      --tts_task gan_tts \
      --feats_extract linear_spectrogram \
      --feats_normalize none \
      --train_config ./conf/tuning/finetune_full_band_vits.yaml

メモリが足りなかった方向け
　　　 --nj 4
をさらに追加して実行してみてください
```

次にstage6の実行前にFine tuningを行うための準備をします。まずESPNetmodelzooからFine tuningに使用するモデルをダウンロードします。

```
$ . ./path.sh
$ espnet_model_zoo_download --unpack true --cachedir downloads kan-bayashi/jsut_full_band_vits_accent_with_pause
```

また、準備のためにいくつかのコマンドを実行します。

```
$ pyscripts/utils/make_token_list_from_config.py downloads/c65dd99aa55a3c4fd6fcb15d3804e5cd/exp/tts_train_full_band_vits_raw_phn_jaconv_pyopenjtalk_accent_with_pause/config.yaml
$ ls downloads/c65dd99aa55a3c4fd6fcb15d3804e5cd/exp/tts_train_full_band_vits_raw_phn_jaconv_pyopenjtalk_accent_with_pause 
$ mv dump/44k/token_list/phn_jaconv_pyopenjtalk_accent_with_pause/tokens.{txt,txt.bak}
$ ln -s $(pwd)/downloads/c65dd99aa55a3c4fd6fcb15d3804e5cd/exp/tts_train_full_band_vits_raw_phn_jaconv_pyopenjtalk_accent_with_pause/tokens.txt dump/44k/token_list/phn_jaconv_pyopenjtalk_accent_with_pause
```

最後にstage6を実行して学習が終わるのを待てば完成です。お疲れ様でした。

```
$ ./run.sh \
    --stage 6 \
    --g2p pyopenjtalk_accent_with_pause \
    --min_wav_duration 0.38 \
    --fs 44100 \
    --n_fft 2048 \
    --n_shift 512 \
    --dumpdir dump/44k \
    --win_length null \
    --tts_task gan_tts \
    --feats_extract linear_spectrogram \
    --feats_normalize none \
    --train_config ./conf/tuning/finetune_full_band_vits.yaml \
    --train_args "--init_param downloads/c65dd99aa55a3c4fd6fcb15d3804e5cd/exp/tts_train_full_band_vits_raw_phn_jaconv_pyopenjtalk_accent_with_pause/train.total_count.ave_10best.pth:tts:tts" \
    --tag finetune_full_band_vits_raw_phn_jaconv_pyopenjtalk_accent_with_pause
```

out of memoryのエラーに遭遇した場合は　espnet/egs2/CHARACTER/tts1/conf/tuning/fineyune_full_band_vits.yaml 内で設定されている batch_binsの値を適宜小さくしてみてください。163行目にあります。

```
batch_bins: 10000000      # batch bins (feats_type=raw)
```

状況によっても変化すると思いますが、以下が検証済みのbatch_binsの適正値です。調整の際に参考にしてください。

```
RTX 3090(24GB)
batch_bins: 8000000

RTX 3080ti(12GB)
batch_bins: 1000000
```
# 作成したモデルの使用方法
### 端末から使用する
作成したモデルは端末から手軽に使用することができます。また、seichiさんが簡易的にアクセントを指定できるスクリプトを作成してくださったので使用方法も含めて[こちら](https://qiita.com/seichi25/items/bb79f66d3eaa6a605c74)を参考にしてみてください。

https://qiita.com/seichi25/items/bb79f66d3eaa6a605c74

### jupyterから使用する
端末から以外にもjuoyterからも使用することが可能です。また、こちらの方はちょっとしたUIがついています。
まずjupyter notebookを開きます。環境構築については各個人でお願いします。インストール済みの方はpipによるインストールは飛ばして構いません。

```
$ conda activate
$ pip3 install torch
$ pip3 install espnet_model_zoo
$ jupyter-notebook
```

notebookで以下のコードを実行すれば使用できるようになります。分かりやすいように3つのセルに分けてあります。/espnet/egs2/CHARACTER/tts1/exp/tts_finetune_vits_raw_phn_jaconv_pyopenjtalk_accent_with_pauseの中に複数の.pthファイルがあると思いますが、いずれかを指定すれば問題ありません。それぞれ少しずつ違うので比べてみて好みのものを見つけてみるのもいいでしょう。

```
from espnet2.bin.tts_inference import Text2Speech
import time
import torch

fs, lang = 44100, "Japanese"
model= "/espnet/egs2/CHARACTER/tts1/exp/tts_finetune_vits_raw_phn_jaconv_pyopenjtalk_accent_with_pause/train.total_count.ave_10best.pth"
#ここのパスだけ書き換える
```

```
text2speech = Text2Speech.from_pretrained(
    model_file=model,
    device="cpu",
    speed_control_alpha=1.0,
    noise_scale=0.333,
    noise_scale_dur=0.333,
)
```

上の2つのセルが無事に実行できれば、後は次のセルを繰り返し実行することで音声合成として利用できます。

```
print(f"Input your favorite sentence in {lang}.")
x = input()

with torch.no_grad():
    start = time.time()
    wav = text2speech(x)["wav"]
rtf = (time.time() - start) / (len(wav) / text2speech.fs)
print(f"RTF = {rtf:5f}")

from IPython.display import display, Audio
display(Audio(wav.view(-1).cpu().numpy(), rate=text2speech.fs))
```


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

https://kan-bayashi.github.io/asj-espnet2-tutorial/

https://tech.fusic.co.jp/posts/2021-08-03-espnet/

22khzを実行する方は以下も参考にしてください。

https://github.com/espnet/espnet/tree/master/egs2/tsukuyomi/tts1

# 最後に
参考文献の著者の方々及びESPNetという素晴らしいツールを開発してくださった方々、そして何より今回の記事を共に製作してくださったseichiさんにこの場を借りて改めて感謝申し上げます。
また、初の寄稿なのでミスや不備等がありましたらお教えください。
最後に、この記事を見てくださった方の中に音声合成やボイスチェンジャーなどに興味がある方がいましたら、ぜひ下記記事で紹介されているYUzushioさんが作成されたDiscordサーバーに参加してみてください。それらに取り組んでいる有志の方々が集まっております。

https://note.com/yuzusio/n/nd0bb4dd6535c