VITS を使った音声変換で特定話者のゆっくり化を試す



https://arxiv.org/abs/2106.06103

上記 VITS の公式実装、[jaywalnut310/vits](https://github.com/jaywalnut310/vits) の音声変換でゆっくり化を試したメモです。

なお、筆者は VITS が何なのか全くわかっていないため、その辺の内容は別途ご参照ください。

(参考)
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">VITS(<a href="https://t.co/nPfudndtwZ">https://t.co/nPfudndtwZ</a>)についてOV2Lの論文読み会で発表した．なかなか面白いTTSモデルで，資料作りも楽しかった．<br>スライドURL: <a href="https://t.co/5pLbLLe1WP">https://t.co/5pLbLLe1WP</a></p>&mdash; ☕ (@kaffelun) <a href="https://twitter.com/kaffelun/status/1416422508958404608?ref_src=twsrc%5Etfw">July 17, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

https://slideslive.com/38959064/conditional-variational-autoencoder-with-adversarial-learning-for-endtoend-texttospeech

# 1. データの準備

## 1.1. 特定話者のデータを作成

「[Mozilla TTS (Tacotron2) を使って日本語音声合成](https://qiita.com/tset-tset-tset/items/7b388b0536fcc774b2ad)」のデータ準備と同様に準備しておきます。以前に作ったものを流用しているため、音声認識結果を [mecab-ipadic-neologd](https://github.com/neologd/mecab-ipadic-neologd) でカナ化 → [uconv](https://man7.org/linux/man-pages/man1/uconv.1.html) でローマ字化しただけのデータを利用しました。

### 1.1.1. g2p 音素変換処理

今回は上記のように学習をローマ字で行ったため、音素変換処理は行っていませんが、補足です。

<details><summary>音素変換処理する場合</summary><div>


テキストを音素変換して学習する場合、[jaywalnut310/vits](https://github.com/jaywalnut310/vits) 内では [bootphon/phonemizer](https://github.com/bootphon/phonemizer) を使った ```language='en-us'``` 決め打ち実装のため、データ作成段階で自力で変換するか、[jaywalnut310/vits](https://github.com/jaywalnut310/vits) の修正が必要です。いくつか方法がありそうですが、だいたい

- [r9y9/pyopenjtalk](https://github.com/r9y9/pyopenjtalk) などを使って [Open-JTalk](https://github.com/xmeta/Open-JTalk/blob/master/src/jpcommon/jpcommon_rule_utf_8.h) 音素表記にする
    - 例）「ヴァーミリオン」→「b a a m i r i o N」
    - 「[2021年6月に発表された最新の音声合成手法「VITS」でアニメ風合成音声を作ってみた【つくよみちゃんコーパス】](https://shirowanisan.com/entry/2021/08/07/172736)」のように、[jaywalnut310/vits](https://github.com/jaywalnut310/vits) 側も symbols cleaners を修正した方が良さそうです。
- [Julius](https://julius.osdn.jp/) の [segmentation-kit](https://github.com/julius-speech/segmentation-kit/blob/master/bin/yomi2voca.pl) などを使って [IPA(情報処理振興事業協会) 音響モデル準拠の音素表記にする](http://speechresearch.fiw-web.net/119.html#notefoot_5)
    - 例）「う゛ぁーみりおん」→「b a: m i r i o N」
    - ほぼ [Open-JTalk](https://github.com/xmeta/Open-JTalk/blob/master/src/jpcommon/jpcommon_rule_utf_8.h) そうなため、同様に [jaywalnut310/vits](https://github.com/jaywalnut310/vits) 側の symbols cleaners を修正した方が良さそうです。
- [espeak-ng/espeak-ng](https://github.com/espeak-ng/espeak-ng) (やそれをバックエンドに使った [bootphon/phonemizer](https://github.com/bootphon/phonemizer)) などを使って [IPA(国際音声記号)](https://ja.wikipedia.org/wiki/%E5%9B%BD%E9%9A%9B%E9%9F%B3%E5%A3%B0%E8%A8%98%E5%8F%B7) 表記にする
    - 例）「ヴァーミリオン」→「väämiɽio̞ɴ」 ([パッチ](https://github.com/espeak-ng/espeak-ng/pull/810)後)
    - [jaywalnut310/vits](https://github.com/jaywalnut310/vits) の内部的にも英語を [IPA(国際音声記号)](https://ja.wikipedia.org/wiki/%E5%9B%BD%E9%9A%9B%E9%9F%B3%E5%A3%B0%E8%A8%98%E5%8F%B7) に変換しているので、事前に変換しておけば symbols の修正は不要…と思います。
    - 現 [espeak-ng/espeak-ng](https://github.com/espeak-ng/espeak-ng) はカナによって足りない処理がある (ヴァー、等) のでデータによっては[要パッチ](https://github.com/espeak-ng/espeak-ng/pull/810)です。
- ガリガリ書く
    - [mozilla/TTS](https://github.com/mozilla/TTS) のほぼ後継 [coqui-ai/TTS](https://github.com/coqui-ai/TTS) では [bootphon/phonemizer](https://github.com/bootphon/phonemizer) ではなく[変換処理を独自実装に置き換えて、](https://github.com/coqui-ai/TTS/blob/main/TTS/tts/utils/text/japanese/phonemizer.py) IPA(情報処理振興事業協会) 音響モデル準拠の音素表記に変換しているようです。

などになるかと思います。そもそも、音素表記と音声表記は別物そうなので、[IPA(国際音声記号)](https://ja.wikipedia.org/wiki/%E5%9B%BD%E9%9A%9B%E9%9F%B3%E5%A3%B0%E8%A8%98%E5%8F%B7) の出力は [Phoneme](https://en.wikipedia.org/wiki/Phoneme) ではなさそうですが、[jaywalnut310/vits](https://github.com/jaywalnut310/vits) が利用しているところを見るに英語だと近しくなるのかなと想像しています。わかりません。

- [音素 - Wikipedia](https://ja.wikipedia.org/wiki/%E9%9F%B3%E7%B4%A0)
- [音声認識の分野で使われている『音韻認識』と『音素認識』は同じ概念なのですか。『音韻』と『音素』とはどう違うのか，いつも疑問に思います。明確な定義があるのでしょうか。 - 音のなんでもコーナー Q and A | 日本音響学会](https://acoustics.jp/qanda/answer/45.html)
- [IPA (国際音声記号)の基礎 -言語学・音声学を学んでいない人のために-](https://www.jstage.jst.go.jp/article/jasj/66/4/66_KJ00006254176/_pdf)

</div>


## 1.2. ゆっくり音声データを作成

ゆっくり音声は、
「[高い、使いにくい、読みにくい──音声合成研究者を悩ませるハードルを解決する“台本”、明治大学らが発表 - ITmedia NEWS](https://www.itmedia.co.jp/news/articles/2106/18/news099.html)」で記事にもなっていた、[mmorise/ita-corpus](https://github.com/mmorise/ita-corpus) の台本を利用して生成しました。

[SofTalk](https://w.atwiki.jp/softalk/pages/15.html) を落として、[コマンドラインに台本を食わせる](https://w.atwiki.jp/softalk/pages/16.html#id_a75f85c8)と、100 (Emotion) + 324 (Recitation)、合計 424 ファイルのデータができます(「女性02」で作った wav ファイルは[こちら](https://drive.google.com/file/d/1Re4QUlTWPOwNGHVU-Z5kKX2-j_nnekNU/view?usp=sharing))。生成された wav ファイルは 8000 Hz ですが、[jaywalnut310/vits のサンプル設定](https://github.com/jaywalnut310/vits/blob/main/configs/vctk_base.json) では 22050 Hz 想定だったので、ffmpeg などで修正(```ffmpeg -i input.wav -af loudnorm -ar 22050 -ac 1 output.wav```)しました。

特定話者のデータ同様に、台本のカナを [uconv](https://man7.org/linux/man-pages/man1/uconv.1.html) でローマ字化(```uconv -f UTF-8 -t UTF-8 -x Latin```)して、メタデータとします。

「[ゆっくりを商用利用する場合のライセンス](https://www.bugbugnow.net/2018/02/blog-post_7.html)」を読むと、新しい [SofTalk](https://w.atwiki.jp/softalk/pages/15.html) は商用利用に別途ライセンスが必要そうですが、商用利用する予定もないのでそのまま最新版を利用しています。

ちなみに YouTube では 2021/06 から[チャンネルが収益化してるしてないに関わらず広告表示する旨の変更が入り](https://support.google.com/youtube/answer/10090902#zippy=%2C%E5%88%A9%E7%94%A8%E8%A6%8F%E7%B4%84%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%97%E3%81%9F%E3%81%AE%E3%81%AF%E3%81%AA%E3%81%9C%E3%81%A7%E3%81%99%E3%81%8B%2C%E4%B8%BB%E3%81%AA%E5%A4%89%E6%9B%B4%E7%82%B9%E3%81%AF%E4%BD%95%E3%81%A7%E3%81%99%E3%81%8B%2Cyoutube-%E3%83%91%E3%83%BC%E3%83%88%E3%83%8A%E3%83%BC-%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AE%E5%8F%8E%E7%9B%8A%E5%8C%96%E3%81%AB%E3%81%AF%E3%81%A9%E3%81%AE%E3%82%88%E3%81%86%E3%81%AA%E5%BD%B1%E9%9F%BF%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%81%8B%2C%E4%BB%8A%E5%9B%9E%E3%81%AE%E6%9B%B4%E6%96%B0%E3%81%AF%E6%AC%A7%E5%B7%9E%E9%80%A3%E5%90%88%E3%81%AB%E3%82%88%E3%82%8B%E8%91%97%E4%BD%9C%E6%A8%A9%E3%81%AB%E9%96%A2%E3%81%99%E3%82%8B%E6%8C%87%E4%BB%A4%E3%82%84-gdpr-%E3%81%AB%E9%96%A2%E9%80%A3%E3%81%97%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%E3%81%8B%2C%E3%83%97%E3%83%A9%E3%82%A4%E3%83%90%E3%82%B7%E3%83%BC%E3%82%84%E3%83%87%E3%83%BC%E3%82%BF%E3%81%B8%E3%81%AE%E5%BD%B1%E9%9F%BF%E3%81%AF%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%81%8B)、2021/08 あたりから実際に非収益化チャンネルでも広告が出ているようです。後述の、出力表示に使っている埋め込み YouTube で広告が表示されるかもですが、収益化しているわけではありません。

## 1.3. メタデータの整形

[jaywalnut310/vits](https://github.com/jaywalnut310/vits) にある[入力ファイル](https://github.com/jaywalnut310/vits/tree/main/filelists)を見て、複数話者用の入力メタデータを作成しておきます。

```
0001|ichi no bunsyou.|ichi no bunsyou.
0002|ni no bunsyou.|ni no bunsyou.
```

↑上記のような、[LJSpeech](https://keithito.com/LJ-Speech-Dataset/) の metadata.csv に準じたフォーマットは

```
/path/to/wavs/1/0001.wav|1|ichi no bunsyou.
/path/to/wavs/1/0002.wav|1|ni no bunsyou.
/path/to/wavs/2/0001.wav|2|san no bunsyou.
/path/to/wavs/2/0002.wav|2|yon no bunsyou.
```

↑上記のように、

- 1 列目の ID は wav ファイルのパスに変換する(.wav もつける)
- 2 列目にはスピーカー ID (sid) が必要なので、話者ごとに数字をふる
- 生テキストは不要、3 列目のノーマライズテキストだけ利用する

と修正しました。

# 2. [jaywalnut310/vits](https://github.com/jaywalnut310/vits) を利用した学習

コマンドは、Google Colab 上での実行を想定しています。 ```!``` 付きです。

## 2.1. 依存ライブラリのインストール

```shell
!git clone https://github.com/jaywalnut310/vits
!apt-get install espeak-ng
!cd vits/; pip install -r requirements.txt
!cd vits/monotonic_align/; python setup.py build_ext --inplace
```

[jaywalnut310/vits の README](https://github.com/jaywalnut310/vits) に従って、ライブラリをインストールします。

## 2.2. preprocess

[VCTK コーパス](http://www.udialogue.org/ja/download-ja/cstr-vctk-corpus.html) を利用した場合の[入力サンプル](https://github.com/jaywalnut310/vits/tree/main/filelists)を参考に、メタデータを 100 行ぐらいの ```metadata_val.csv``` と残りの ```metadata_train.csv``` に分割しました。その後

```shell
!cd vits/; python preprocess.py --text_index 2 --filelists /content/metadata_train.csv /content/metadata_val.csv --text_cleaners basic_cleaners
```

と ```preprocess.py``` を実行すると、```metadata_train.csv.cleaned``` と ```metadata_val.csv.cleaned``` ができます。
[README](https://github.com/jaywalnut310/vits) の例そのままを実行すると、音素変換を行う ```--text_cleaners english_cleaners2``` が実行されてローマ字を英語読みしてしまうため、何もしない ```--text_cleaners basic_cleaners``` を指定しています。というか何もしてないので大文字小文字混合などがなければ *.cleaned は全く同じファイルになります。別にやらなくて良さそうですが処理の流れを掴むためだけに実施しました。


## 2.3. 設定の修正

[複数話者用の設定の、vctk_base.json](https://github.com/jaywalnut310/vits/blob/main/configs/vctk_base.json) を修正して、```hoge.json``` を作りました。

```json:vctk_base.json
    "batch_size": 64,
～～～
    "training_files":"filelists/vctk_audio_sid_text_train_filelist.txt.cleaned",
    "validation_files":"filelists/vctk_audio_sid_text_val_filelist.txt.cleaned",
    "text_cleaners":["english_cleaners2"],
```

↓コピーして修正

```json:hoge.json
    "batch_size": 32,
～～～
    "training_files":"/content/metadata_train.csv.cleaned",
    "validation_files":"/content/metadata_val.csv.cleaned",
    "text_cleaners":["basic_cleaners"],
```

入力ファイルを指定している ```training_files``` ```validation_files``` を、作成したメタデータファイルにします。また、余計な変換をしないよう ```text_cleaners``` を ```basic_cleaners``` を変えています(```"cleaned_text": true``` なので学習時には利用されないと思いますが、後述する TTS 音声合成時に音素変換がかかってしまい変な音声になります)。```"batch_size": 64,``` は、Google Colab でメモリ確保できずに落ちて ```32``` に減らしただけなので、環境に依っては ```64``` のままで問題なさそうです。

## 2.4. symbols.py の修正

```shell
!cp -n vits/text/symbols.py{,.org}
!cp vits/text/symbols.py{.org,}
!perl -i -pe 's{;:,\.}{-;:,.}' vits/text/symbols.py
!diff -u vits/text/symbols.py{.org,}
```

[symbols.py](https://github.com/jaywalnut310/vits/blob/main/text/symbols.py#L7) には ```_punctuation = ';:,.!?¡¿—…"«»“” '``` とあり、一見ハイフン ```-``` は問題ないようなのですが、これは U+2014 の EM DASH なので ASCII のハイフンではありませんでした… ので U+002D の HYPHEN-MINUS を追加しています。

注意点として、この symbols.py の数でモデル定義が変わってくるため、inference のモデルロード前にも学習時と同様の修正が必要になります。なので、学習途中での symbols.py の修正は厳しそうです。

## 2.5. 学習

```shell
!mkdir -p /content/drive/MyDrive/vits_logs/
!ln -nvs /content/drive/MyDrive/vits_logs vits/logs
!cd vits/; python train_ms.py -c configs/hoge.json -m hoge
```

カレントの ```logs``` にチェックポイントを保存するので、Google Drive 上に保存するようにして学習します。
Google Colab で実行すると一定時間でセッションが切れるのですが、再実行時に ```logs``` 以下の最新のチェックポイントから継続してくれるので便利でした。

### 2.5.1. loss

![tensorboard.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693216/7be1c09b-fa69-19ac-60e9-48781d528671.png)

筆者は[loss が小さけりゃ良いと思っていた](https://gangango.com/2018/11/16/post-322/)ので、どのあたりのモデルが良い塩梅なのか全然わかっていないのですが、とりあえず 300k steps ほど回してみて(Colab Pro で 1 週間弱)、loss/g/mel が最小になったモデルを使いました。

# 3. [inference.ipynb](https://github.com/jaywalnut310/vits/blob/main/inference.ipynb) を参考に inference

[jaywalnut310/vits](https://github.com/jaywalnut310/vits) にある [inference.ipynb](https://github.com/jaywalnut310/vits/blob/main/inference.ipynb) を利用して音声合成・音声変換しました。

## 3.1. 前準備

2.4. で行った symbols.py の修正を先に行った上で、[inference.ipynb](https://github.com/jaywalnut310/vits/blob/main/inference.ipynb) 同様下記を定義します。

```python
%cd /content/vits/
%matplotlib inline
import matplotlib.pyplot as plt
import IPython.display as ipd

import os
import json
import math
import torch
from torch import nn
from torch.nn import functional as F
from torch.utils.data import DataLoader

import commons
import utils
from data_utils import TextAudioLoader, TextAudioCollate, TextAudioSpeakerLoader, TextAudioSpeakerCollate
from models import SynthesizerTrn
from text.symbols import symbols
from text import text_to_sequence

from scipy.io.wavfile import write


def get_text(text, hps):
    text_norm = text_to_sequence(text, hps.data.text_cleaners)
    if hps.data.add_blank:
        text_norm = commons.intersperse(text_norm, 0)
    text_norm = torch.LongTensor(text_norm)
    return text_norm
```

[inference.ipynb](https://github.com/jaywalnut310/vits/blob/main/inference.ipynb) そのままです。import 等を行っています。

```python
hps = utils.get_hparams_from_file("./configs/hoge.json")
```

学習に使った ```hoge.json``` をそのまま利用します。

```python
net_g = SynthesizerTrn(
    len(symbols),
    hps.data.filter_length // 2 + 1,
    hps.train.segment_size // hps.data.hop_length,
    n_speakers=hps.data.n_speakers,
    **hps.model).cuda()
_ = net_g.eval()

_ = utils.load_checkpoint("./logs/hoge/G_60000.pth", net_g, None)
```

```logs``` 下の、学習時に指定したディレクトリにチェックポイントがあるので、良い塩梅の generator (```G_*.pth```) をロードします。

## 3.2. TTS 音声合成

目的ではないのですが、テキストから音声合成もできました。

```python
stn_tst = get_text("tasukete kudasai hennna hitoga araware mashita.", hps)
with torch.no_grad():
    x_tst = stn_tst.cuda().unsqueeze(0)
    x_tst_lengths = torch.LongTensor([stn_tst.size(0)]).cuda()
    sid = torch.LongTensor([2]).cuda()
    audio = net_g.infer(x_tst, x_tst_lengths, sid=sid, noise_scale=.667, noise_scale_w=0.8, length_scale=1)[0][0,0].data.cpu().float().numpy()
ipd.display(ipd.Audio(audio, rate=hps.data.sampling_rate, normalize=False))
```

TTS の入力テキストは学習時と同じにする必要があるので、学習時同様のローマ字入力です。cleaners をまず何もしない ```basic_cleaners``` 以外にしている場合は、この入力に対して cleaners 処理がかかるはずです。
上記の例だと ```torch.LongTensor([2]).cuda()``` なので sid が 2 の話者の TTS になります。```[2]``` を ```[1]``` にすれば sid が 1 の話者の TTS になりました。


## 3.3. VC 音声変換

```python
dataset = TextAudioSpeakerLoader('/content/converttarget.csv', hps.data)
collate_fn = TextAudioSpeakerCollate()
loader = DataLoader(dataset, num_workers=8, shuffle=False,
    batch_size=1, pin_memory=True,
    drop_last=True, collate_fn=collate_fn)
data_list = list(loader)
```

VC 用の入力に使う音声ファイルは、学習同様 3 カラムフォーマット (wav パス、sid、テキストのパイプ区切り) のファイルから読み込むようです。[inference.ipynb](https://github.com/jaywalnut310/vits/blob/main/inference.ipynb) で指定しているように ```hps.data.validation_files``` から入力音声を読み込んでも良いですが、学習に使っていない、[SofTalk](https://w.atwiki.jp/softalk/pages/15.html) で新録した音声での変換を試したかったので、上記では直接指定しています。

```python
with torch.no_grad():
    x, x_lengths, spec, spec_lengths, y, y_lengths, sid_src = [x.cuda() for x in data_list[0]]
    sid_tgt1 = torch.LongTensor([1]).cuda()
    sid_tgt2 = torch.LongTensor([2]).cuda()
    audio1 = net_g.voice_conversion(spec, spec_lengths, sid_src=sid_src, sid_tgt=sid_tgt1)[0][0,0].data.cpu().float().numpy()
    audio2 = net_g.voice_conversion(spec, spec_lengths, sid_src=sid_src, sid_tgt=sid_tgt2)[0][0,0].data.cpu().float().numpy()
print("Original SID: %d" % sid_src.item())
ipd.display(ipd.Audio(y[0].cpu().numpy(), rate=hps.data.sampling_rate))
print("Converted SID: %d" % sid_tgt1.item())
ipd.display(ipd.Audio(audio1, rate=hps.data.sampling_rate))
print("Converted SID: %d" % sid_tgt2.item())
ipd.display(ipd.Audio(audio2, rate=hps.data.sampling_rate))
```

オリジナルの [inference.ipynb](https://github.com/jaywalnut310/vits/blob/main/inference.ipynb) では、指定したファイルの 1 行目のデータ (```data_list[0]```) を入力として、sid が 1, 2, 4 ```torch.LongTensor([1]).cuda(), torch.LongTensor([2]).cuda(), torch.LongTensor([4]).cuda()``` の話者に変換していましたが、今回 sid が 1, 2 だけだったので削っています。また、```ipd.Audio``` に対する ```normalize=False``` の指定がエラーになったので削りました。

## 3.4. inference 結果

<iframe width="560" height="315" src="https://www.youtube.com/embed/n_neV_0HEcQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# 4. まとめ

ゆっくり音声 ([SofTalk](https://w.atwiki.jp/softalk/pages/15.html)) は動画での利用も多く、その合成っぽさを含めた聞き馴染みのあるイントネーションが、音声変換である程度再現できたので良かったです。