初心者が一か月でTTSつくってみた

# 概要
進級制作で発表するための作品として、友人の声からTTSを作成したので、作り方や難しかったところ等を記事にしてみました

参考にさせていただいたページは最後に記載しています

# 目次
- [1.TTSとは](https://qiita.com/niwatiki/private/ae47cae9e93c2daadb5f#tts%E3%81%A8%E3%81%AF)
- [2.きっかけ](https://qiita.com/niwatiki/private/ae47cae9e93c2daadb5f#%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91)
- [3.製作](https://qiita.com/niwatiki/private/ae47cae9e93c2daadb5f#%E8%A3%BD%E4%BD%9C)
- [4.終わりに](https://qiita.com/niwatiki/private/ae47cae9e93c2daadb5f#%E7%B5%82%E3%82%8F%E3%82%8A%E3%81%AB)
- [5.参考記事](https://qiita.com/niwatiki/private/ae47cae9e93c2daadb5f#%E5%88%A9%E7%94%A8%E3%81%BE%E3%81%9F%E3%81%AF%E5%8F%82%E8%80%83%E3%81%AB%E3%81%95%E3%81%9B%E3%81%A6%E3%81%84%E3%81%9F%E3%81%A0%E3%81%84%E3%81%9F%E8%A8%98%E4%BA%8B)

# TTSとは
**Text-To-Speak**つまり音声合成のことで、
ゆっくり実況でおなじみの「棒読みちゃん」やVOICEROIDの「東北ずん子」、VOICEVOX「ずんだもん」等のことである


# きっかけ
第一に筆者が音声合成読み上げソフトのキャラクターが好きであり、それを利用した動画コンテンツをよくみる
ソフト自体も所持しており、購入しているソフトは20本以上になる~~推しは[FEEちゃん](https://www.dlsite.com/soft/work/=/product_id/VJ014896.html)~~
また、VOICEVOXの登場でブームが来ているというのもあり、作ってみたいと入学時から思っていたため、進級制作として作成した

# 製作
実際に制作する作業に入る前に目標を定める
## 目標
今回の目標は以下の3つ

- 独自にモデルを作成する
- ローカルで学習をおこなう
- VOICEVOXのUIで動くようにする  

一か月と製作期間は短く、どれも初めて触る技術なので、形として仕上げることを優先して作業を行った

## 環境構築
まずは学習する環境を整えなければならない

今回学習に利用するPCスペックは以下
- OS : Windows 11
- CPU : CoreI9 13900k
- RAM : 64GB
- GPU : GeForce RTX 4090


OSは音声ファイルの前処理の都合もありWindowsを利用する
学習にはESPNetというEnd-to-End音声処理ツールキットを利用して作成していく

https://github.com/espnet/espnet


ESPNetはLinux上で動くため、WSLを利用していく
ディストリビューションは、`Ubuntu-18.04`を利用する(`Ubuntu-22.04`を利用していたが、バージョン違い等のエラーが発生した)
WSLは最新のものを利用する(WindowsアップデーターからWindows の更新時に他の...みたいなやつをオンにする)
```ps1:wsl_install
wsl --update
wsl --install Ubuntu-18.04
```
Ubuntuをインストールしたうえで接続したら、GPUと通信ができているかどうかを`nvidia-smi`で確認する

うまくいけばこんな感じのものが出てくる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1760850/8cca9b17-6124-db83-68a1-7e9a50cefe3f.png)
<details><summary>うまく実行できない場合(筆者がした対処法)</summary>  

1. GPUドライバーが古い   
　
    最新のドライバーが入っている必要がある
    更新やチェックにはGeForce Experienceが便利
  
2. WSLのバージョンが古い  
  
    4.9.121以上でないと動作しない
    最新のものを使っておけば問題ない

3. Windowsターミナル or PowerShellが管理者モードで起動している

    一番ここに躓いた
    管理者モードだとうまく実行できない場合がある(条件による?)
    それっぽいissueがあったのでリンクしておく
    https://github.com/microsoft/WSL/issues/9099

</details>

また、この画面でCUDA Versionなるものが表示されているが、すでに入っているわけではなく、対応している最新のCUDAのバージョンが表示されているらしいので気にしなくてよい

確認出来たらCUDAToolKitを入れていく

```sh:cuda_toolkit_V11.8
wget https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-wsl-ubuntu.pin
sudo mv cuda-wsl-ubuntu.pin /etc/apt/preferences.d/cuda-repository-pin-600
wget https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda-repo-wsl-ubuntu-11-8-local_11.8.0-1_amd64.deb
sudo dpkg -i cuda-repo-wsl-ubuntu-11-8-local_11.8.0-1_amd64.deb
sudo cp /var/cuda-repo-wsl-ubuntu-11-8-local/cuda-*-keyring.gpg /usr/share/keyrings/
sudo apt-get update
sudo apt-get -y install cuda
```
他のバージョンはこのサイトからディストリビューションを`WSL-Ubuntu`と選択すればよい

https://developer.nvidia.com/cuda-toolkit-archive

古いバージョンにはWSL-Ubuntuが存在しないため以下のサイトを参考してほしい

https://docs.nvidia.com/cuda/wsl-user-guide/index.html#cuda-support-for-wsl-2:~:text=Option%201%3A%20Installation%20of%20Linux%20x86%20CUDA%20Toolkit%20using%20WSL%2DUbuntu%20Package%20%2D%20Recommended

**注意点**

apt install で直接落とそうとすると、環境が壊れる
また、Kitの12系は現在CUDNNが対応していないため入れてはならない
4000番台のGPUを使う人は11.7以上にしないと正常に動作しない

 

  
導入できたらCUDAにパスを通す
```sh
export PATH="/usr/local/cuda/bin:$PATH"
export LD_LIBRARY_PATH="/usr/local/cuda/lib64:$LD_LIBRARY_PATH"
```
Shellを閉じるとリセットされてしまうので、永続化する方は各自調べてほしい

できたら`nvcc -V`で正常にインストールできたか確認する

これでGPU系の環境構築は終わった
以降は以下の記事の通りに進めていく

https://qiita.com/RRR_troisR/items/6288b9bdc6e725aa8440

変更点としてはRTX 40xxシリーズを利用してるため、Anaconda環境を作るときにPyTorchのバージョンをNightlyにし、CUDAと合わせた(筆者はスクリプトを書き換えたが、上書きインストールの方が楽そう)

試しにツクヨミちゃんコーパスを利用し学習してみる

https://tyc.rei-yumesaki.net/material/corpus/


学習には18時間程度かかった ~~電気代約300円~~
step8で止まるかと思うが、現時点でモデルはできているため特に問題はない
継続したい場合はconfフォルダーの中に適切なdecode.yamlをコピーする必要がある


できたモデルデータを利用してみる

```py:get_wavfile.py
import time

import numpy as np
import torch
from espnet2.bin.tts_inference import Text2Speech
from scipy.io.wavfile import write

fs, lang = 44100, "Japanese"
model= "./xxepoch.pth"
x = "これはテストメッセージです"

text2speech = Text2Speech.from_pretrained(
    model_file=model,
    device="cpu",
    speed_control_alpha=1.0,
    noise_scale=0.333,
    noise_scale_dur=0.333,
)
pause = np.zeros(30000, dtype=np.float32)

with torch.no_grad():
    wav = text2speech(x)["wav"]

wav_list = []
wav_list.append(np.concatenate([wav.view(-1).cpu().numpy(), pause]))
final_wav = np.concatenate(wav_list)
write("gen_file.wav", rate=text2speech.fs, data=final_wav)
```

うまく合成できていれば環境構築は終了である
結果としてはエラーやモデルの学習時間で2週間かかった

## 録音
独自のモデルを製作するためにはまず素材となる音声が必要である
同じクラスの友人の声を収録し作成していく

### 声優選定及びコーパス選定
今回は製作期間が非常にみじかく、一からモデルを作成する時間がないため、転移学習と呼ばれる学習方法を利用する
学習に利用する文章や音声のことを**コーパス**というのだか、無料で利用できるコーパスは複数存在する
少ない分量(100文)で学習でき、手軽に利用できる、[JVSコーパス](https://sites.google.com/site/shinnosuketakamichi/research-topics/jvs_corpus)を利用することにした(ツクヨミちゃんコーパスが利用してるのと同じもの)

### コーパス作成
コーパスを作成するためには可能な限りノイズの少ない環境で収録する必要があるため、レコーディングスタジオを利用した

利用したスタジオ↓

https://www.studionoah.jp/baba/

収録するスタジオは決まったのだが問題がある
- 筆者自体がレコーディングをしたことがないこと
- 友人が、プロの声優ではないこと

一つ目は高校時代の先輩に音楽系の専門(HALではない)に行った人がいるのでその先輩に教わることにした
二つ目はどうしようもないので、音声の前処理に時間をかけ可能な限りごまかすことにした

収録方法はMACでProTools(DAW)に5文ずつ分けて録音した

レコーディングは二日、計8時間かかった(本来はもう一日取りたかったが、学校のレコスタが借りれず日程と金銭の問題で妥協した)

### 前処理
実際に収録した音声を切り分ける作業から始める
手作業でやっていたら間に合わないかつ、友人の声を嫌いになりかねないので、簡単なスクリプトを書く

```py:cat_wavfile.py
# pydub と tqdm をインストールしておく
import glob
import os

from pydub import AudioSegment
from pydub.silence import split_on_silence
from tqdm import tqdm

DAY = "**"
FILE_PATH = rf".\output\**\2023_02_{DAY}\*.wav"

file_wav = glob.glob(
    FILE_PATH, recursive=True)

os.makedirs(FILE_PATH, exist_ok=True)
for i in tqdm(file_wav, desc="cut wav"):
    tqdm.write(f"cut {i}")
    sound = AudioSegment.from_file(i, format="wav")
    chunks = split_on_silence(
        sound, min_silence_len=1500, silence_thresh=-40, keep_silence=400)
    for j, chunk in enumerate(chunks):
        chunk.export(
            FILE_PATH + f"\\{os.path.splitext(os.path.basename(i))[0]}+{j}.wav", format="wav")
        tqdm.write(f"gen {i}+{j}")
    tqdm.write(f"{i} done")
```
これで切り分けたファイルができた

次に、切り分けたWAVファイルの解析と音量調整を行う
<details><summary>コード(割と長いかつ汚いので注意)</summary>

```py:wav_analysis.py
# 追加モジュールは numpy matplotlib inaSpeechSegmenter
import glob
import os
import wave
from concurrent.futures import ProcessPoolExecutor
from struct import unpack

import matplotlib.pyplot as plt
import numpy as np
from inaSpeechSegmenter import Segmenter


class AnalysisWav:

    def __init__(self, file_path):
        self.file_path = file_path
        self.seg_model = Segmenter(vad_engine="smn")
        self.__filereader()

    def __filereader(self):
        wf = wave.open(self.file_path, 'rb')
        self.getnframes = wf.getnframes()
        self.readframes = wf.readframes(self.getnframes)
        self.getframerate = wf.getframerate()
        self.getsampwidth = wf.getsampwidth()
        wf.close()

    def analysis_label(self):
        with open(f"{self.__mkdir('raw_label')}.txt", "w") as f:
            for i in self.seg_model(self.file_path):
                f.write(f"{i[0]} {i[1]} {i[2]}\n")

    def analysis_volume(self) -> int:
        return

    def gen_waveimg(self):
        if self.getsampwidth == 2:
            data = np.frombuffer(self.readframes, dtype='int16')
        elif self.getsampwidth == 3:
            self.readframes = [unpack("<i",
                                      bytearray([0]) + self.readframes[self.getsampwidth * i:self.getsampwidth * (i + 1)])[0]
                               for i in range(self.getnframes)]
            data = np.array(self.readframes)
            data = np.where(data > 0,
                            data / (2.0 ** 31 - 1),
                            data / (2.0 ** 31))
        t = np.arange(0, len(data)) / self.getframerate
        plt.plot(t, data)
        plt.grid()
        plt.savefig(f"{self.__mkdir('raw_img')}.png")
        plt.close()

    def __mkdir(self, name):
        path = ""
        file_ls = self.file_path.split("\\")
        path = f".\\output\\{name}\\" + file_ls[file_ls.index("raw")+1]
        os.makedirs(f"{path}", exist_ok=True)
        return f"{path}\{os.path.splitext(os.path.basename(self.file_path))[0]}"


def volume_up(file_path, volume=2):
    path = ""
    file_ls = file_path.split("\\")
    path = ".\\output\\volume_up\\" + file_ls[file_ls.index("raw")+1]
    os.makedirs(f"{path}", exist_ok=True)
    file_out = f"{path}\{os.path.basename(file_path)}"
    os.system(f"ffmpeg -y -i {file_path} -af volume={volume}dB {file_out}")


def extract(file_path):
    volume_up(file_path, -1)
    analysis_wav = AnalysisWav(file_path=f"{file_path}")
    analysis_wav.gen_waveimg()
    print(analysis_wav.analysis_label())


if __name__ == '__main__':
    DAY = "**"
    FILE_PATH = rf".\output\raw\2023_02_{DAY}\main\*.wav"
    files = glob.glob(
        FILE_PATH, recursive=True)
    with ProcessPoolExecutor(max_workers=4) as executor:
        for file_path in files:
            executor.submit(extract, file_path)
```
</details>
これで音声波形とlabelが生成された(labelはほぼ使わなかった)
音声波形を見つつ明らかなノイズがあるファイルはこの時点で取り除く   

次は実際に利用するファイルを選んでいくのだが、ファイル名が日付になっており、聞くまで何を読み上げたが不明である  
このままでは同じファイルを何度も聞くことになり、友人の声を聴きたくなくなってしまうので音声認識を利用し、フォルダごとに分けていくことにした  
  
```py:stt.py
# 追加モジュールは speech_recognition 
import glob
import os
import shutil

import speech_recognition as sr
from tqdm import tqdm

r = sr.Recognizer()
DAY = "**"
FILE_PATH = rf".\output\cut_wavfile\2023_02_{DAY}\*.wav"
files = glob.glob(FILE_PATH, recursive=True)

for audio_file in tqdm(files):
    with sr.AudioFile(audio_file) as source:
        audio = r.record(source)
    text = r.recognize_google(audio, language="ja-JP", show_all=True)
    text = text["alternative"][0]["transcript"]
    folder_name = text[0:5].replace(" ", "_")
    os.makedirs(rf"output\speech\{folder_name}", exist_ok=True)
    if not os.path.exists(r"output\speech\result.txt"):
        with open(r"output\speech\result.txt", "w", encoding="utf-8"):
            pass
    with open(r"output\speech\result.txt", "a", encoding="utf-8") as f:
        f.write(f"{os.path.basename(audio_file)}:{text}\n")
    shutil.copy(audio_file, rf"output\speech\{folder_name}")
```
これでいい感じにフォルダごとに分けることができる

あとは利用するファイルを選ぶだけである
実際に音声を聞きながら正しく発音できているものを分けていく~~友人の声を嫌いになりそうだった~~

ファイルを選んだら、収録中に乗ってしまったホワイトノイズや環境音、リップノイズを除去する作業を行う
RX9というツールを利用した(現在は10にアップデートされている)

https://www.izotope.jp/jp/products/rx-10/

これはAIを利用したノイズ除去専門ツールである
作業風景はこんな感じ![file.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1760850/c542d1fd-4374-5bbc-2b97-c28352077516.png)


これを利用してノイズ除去を行う
スタンダード版しか所持していないため、ゲイン調整は手作業で行う(アドバンスド版は自動調整機能があるが高い)
慣れていないため一つのファイルに15分ほどかかった

これが終わったら前処理は終了である

音声にリップノイズが多く含まれており、完全に取り切れないものが多く、結果としてはいまいちである

## 学習
ここまで来たら実際に学習していくフェーズである
ファイルのビットレートをそろえ、環境構築でやったように学習を進めていく
一度回しているため、音声ファイルを置き換え、以前の学習結果を削除すれば、動作するかと思う

学習には22時間かかった

驚きの消費電力　~~暖房器具~~
![暖房器具](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1760850/3b5da28b-bae5-04be-d8f5-214eb4817546.png)


学習途中のモデルで合成してみたが、完了後のモデルと比べ、大きな変化はなく、ノイズが低減していたり、アクセントが弱くなっていた

これで学習は終了である

## UI利用
この時点で目標は二つ達成した
- [x] 独自にモデルを作成する
- [x] ローカルで学習をおこなう
- [ ] VOICEVOXのUIで動くようにする  

のこった目標を達成していく
ESPNetでしたモデルをVOICEVOXの拡張エンジンとして動かすためのソフトがあったので利用させていただく

https://github.com/voicevox-bridge/bridge-plugin

READMEにそって作業を進めていけば簡単にできた
拡張エンジンとしては動作するようになったのでUIをカスタムして独自ツールとしていく

https://github.com/VOICEVOX/voicevox/

このリポジトリをクローンし先ほど作ったファイルをすべてコピーしたうえで、`.env`ファイルを書き換え、エディタの色を変更し、実行する

動けば終了である
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1760850/755c97f9-b13b-f993-d069-99e96ac9c59b.png)


bridge-pluginを利用した副産物として、アクセントや声の高さ、抑揚等のパラメーターを利用することができるようになった
またVOICEVOXのUIを利用しているため、慣れ親しんだUIで合成することができるようになった
その他にもVOICEVOXのAPIに準じているため、ゆかりねっとや、Discordの読み上げボット、ゆっくりムービーメーカー等のツールでも利用しやすくなったのではないだろうか

# 終わりに
今回は短期間ということで、可能な限り既存ツールを利用し、時短を目指した
コードの記述量は減ったが、ツールを理解するのに時間がかかったため、時短の効果は薄かったかもしれない


# 利用または参考にさせていただいた記事

https://github.com/espnet/espnet

https://tyc.rei-yumesaki.net/material/corpus/

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

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

https://qiita.com/RRR_troisR/items/6288b9bdc6e725aa8440

https://touch-sp.hatenablog.com/entry/2022/07/08/153107

https://qiita.com/wrist/items/4b404230e264c5cb571c

https://qiita.com/ksasaki/items/ee864abd74f95fea1efa

https://ssohsn.fanbox.cc/posts/3377355

https://qiita.com/daiarg/items/ff1b9f91d0804e6a8f18

https://github.com/voicevox-bridge/bridge-plugin

https://github.com/VOICEVOX/voicevox/