概要
進級制作で発表するための作品として、友人の声からTTSを作成したので、作り方や難しかったところ等を記事にしてみました
参考にさせていただいたページは最後に記載しています
目次
TTSとは
Text-To-Speakつまり音声合成のことで、
ゆっくり実況でおなじみの「棒読みちゃん」やVOICEROIDの「東北ずん子」、VOICEVOX「ずんだもん」等のことである
きっかけ
第一に筆者が音声合成読み上げソフトのキャラクターが好きであり、それを利用した動画コンテンツをよくみる
ソフト自体も所持しており、購入しているソフトは20本以上になる推しはFEEちゃん
また、VOICEVOXの登場でブームが来ているというのもあり、作ってみたいと入学時から思っていたため、進級制作として作成した
製作
実際に制作する作業に入る前に目標を定める
目標
今回の目標は以下の3つ
- 独自にモデルを作成する
- ローカルで学習をおこなう
- VOICEVOXのUIで動くようにする
一か月と製作期間は短く、どれも初めて触る技術なので、形として仕上げることを優先して作業を行った
環境構築
まずは学習する環境を整えなければならない
今回学習に利用するPCスペックは以下
- OS : Windows 11
- CPU : CoreI9 13900k
- RAM : 64GB
- GPU : GeForce RTX 4090
OSは音声ファイルの前処理の都合もありWindowsを利用する
学習にはESPNetというEnd-to-End音声処理ツールキットを利用して作成していく
ESPNetはLinux上で動くため、WSLを利用していく
ディストリビューションは、Ubuntu-18.04
を利用する(Ubuntu-22.04
を利用していたが、バージョン違い等のエラーが発生した)
WSLは最新のものを利用する(WindowsアップデーターからWindows の更新時に他の...みたいなやつをオンにする)
wsl --update
wsl --install Ubuntu-18.04
Ubuntuをインストールしたうえで接続したら、GPUと通信ができているかどうかをnvidia-smi
で確認する
うまく実行できない場合(筆者がした対処法)
-
GPUドライバーが古い
最新のドライバーが入っている必要がある
更新やチェックにはGeForce Experienceが便利 -
WSLのバージョンが古い
4.9.121以上でないと動作しない
最新のものを使っておけば問題ない -
Windowsターミナル or PowerShellが管理者モードで起動している
一番ここに躓いた
管理者モードだとうまく実行できない場合がある(条件による?)
それっぽいissueがあったのでリンクしておく
https://github.com/microsoft/WSL/issues/9099
また、この画面でCUDA Versionなるものが表示されているが、すでに入っているわけではなく、対応している最新のCUDAのバージョンが表示されているらしいので気にしなくてよい
確認出来たらCUDAToolKitを入れていく
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
と選択すればよい
古いバージョンにはWSL-Ubuntuが存在しないため以下のサイトを参考してほしい
注意点
apt install で直接落とそうとすると、環境が壊れる
また、Kitの12系は現在CUDNNが対応していないため入れてはならない
4000番台のGPUを使う人は11.7以上にしないと正常に動作しない
導入できたらCUDAにパスを通す
export PATH="/usr/local/cuda/bin:$PATH"
export LD_LIBRARY_PATH="/usr/local/cuda/lib64:$LD_LIBRARY_PATH"
Shellを閉じるとリセットされてしまうので、永続化する方は各自調べてほしい
できたらnvcc -V
で正常にインストールできたか確認する
これでGPU系の環境構築は終わった
以降は以下の記事の通りに進めていく
変更点としてはRTX 40xxシリーズを利用してるため、Anaconda環境を作るときにPyTorchのバージョンをNightlyにし、CUDAと合わせた(筆者はスクリプトを書き換えたが、上書きインストールの方が楽そう)
試しにツクヨミちゃんコーパスを利用し学習してみる
学習には18時間程度かかった 電気代約300円
step8で止まるかと思うが、現時点でモデルはできているため特に問題はない
継続したい場合はconfフォルダーの中に適切なdecode.yamlをコピーする必要がある
できたモデルデータを利用してみる
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コーパスを利用することにした(ツクヨミちゃんコーパスが利用してるのと同じもの)
コーパス作成
コーパスを作成するためには可能な限りノイズの少ない環境で収録する必要があるため、レコーディングスタジオを利用した
利用したスタジオ↓
収録するスタジオは決まったのだが問題がある
- 筆者自体がレコーディングをしたことがないこと
- 友人が、プロの声優ではないこと
一つ目は高校時代の先輩に音楽系の専門(HALではない)に行った人がいるのでその先輩に教わることにした
二つ目はどうしようもないので、音声の前処理に時間をかけ可能な限りごまかすことにした
収録方法はMACでProTools(DAW)に5文ずつ分けて録音した
レコーディングは二日、計8時間かかった(本来はもう一日取りたかったが、学校のレコスタが借りれず日程と金銭の問題で妥協した)
前処理
実際に収録した音声を切り分ける作業から始める
手作業でやっていたら間に合わないかつ、友人の声を嫌いになりかねないので、簡単なスクリプトを書く
# 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ファイルの解析と音量調整を行う
コード(割と長いかつ汚いので注意)
# 追加モジュールは 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)
次は実際に利用するファイルを選んでいくのだが、ファイル名が日付になっており、聞くまで何を読み上げたが不明である
このままでは同じファイルを何度も聞くことになり、友人の声を聴きたくなくなってしまうので音声認識を利用し、フォルダごとに分けていくことにした
# 追加モジュールは 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にアップデートされている)
これはAIを利用したノイズ除去専門ツールである
作業風景はこんな感じ
これを利用してノイズ除去を行う
スタンダード版しか所持していないため、ゲイン調整は手作業で行う(アドバンスド版は自動調整機能があるが高い)
慣れていないため一つのファイルに15分ほどかかった
これが終わったら前処理は終了である
音声にリップノイズが多く含まれており、完全に取り切れないものが多く、結果としてはいまいちである
学習
ここまで来たら実際に学習していくフェーズである
ファイルのビットレートをそろえ、環境構築でやったように学習を進めていく
一度回しているため、音声ファイルを置き換え、以前の学習結果を削除すれば、動作するかと思う
学習には22時間かかった
学習途中のモデルで合成してみたが、完了後のモデルと比べ、大きな変化はなく、ノイズが低減していたり、アクセントが弱くなっていた
これで学習は終了である
UI利用
この時点で目標は二つ達成した
- 独自にモデルを作成する
- ローカルで学習をおこなう
- VOICEVOXのUIで動くようにする
のこった目標を達成していく
ESPNetでしたモデルをVOICEVOXの拡張エンジンとして動かすためのソフトがあったので利用させていただく
READMEにそって作業を進めていけば簡単にできた
拡張エンジンとしては動作するようになったのでUIをカスタムして独自ツールとしていく
このリポジトリをクローンし先ほど作ったファイルをすべてコピーしたうえで、.env
ファイルを書き換え、エディタの色を変更し、実行する
bridge-pluginを利用した副産物として、アクセントや声の高さ、抑揚等のパラメーターを利用することができるようになった
またVOICEVOXのUIを利用しているため、慣れ親しんだUIで合成することができるようになった
その他にもVOICEVOXのAPIに準じているため、ゆかりねっとや、Discordの読み上げボット、ゆっくりムービーメーカー等のツールでも利用しやすくなったのではないだろうか
終わりに
今回は短期間ということで、可能な限り既存ツールを利用し、時短を目指した
コードの記述量は減ったが、ツールを理解するのに時間がかかったため、時短の効果は薄かったかもしれない
利用または参考にさせていただいた記事