音声認識結果に基づき複数話者でテキスト音声合成する簡易音声変換アプリをPythonで書いた話

はじめに

PySimpleGUIの応用シリーズ。スクリプトの動作の様子は以下の通り。

本記事の趣旨は今回作成したスクリプトで実現された機能や利用したライブラリ群ほか、関連情報を補足的に説明することである。

作成したスクリプト

スクリプトへのURLを置く。

以下は機能の覚え書きなどである。

動作確認環境

必要なPythonライブラリはコマンドラインからインストールできる(はず)。

pip install numpy
pip install pysimplegui
pip install sounddevice
pip install soundfile
pip install speech_recognition
pip install ttslearn
pip install torch

PySimpleGUI

PySimpleGUIの関数群を用いて、ボタンやフレームを配置してみた。現時点では話者10人分のボタンを直接押すことで話者選択を行っているが、話者数が増えるとしんどいので(実は合成モデルは100人分用意されている)、プルダウンメニュー式の話者選択UIはそのうち作る予定である(やれたらやる)。

PySimpleGUI初学者向けのサンプルプログラムを公開している方がおり、私もこれで最初勉強した。とても参考になった。
PySimpleGUI 基礎解説 | K-TechLaboゼミ用学習ノート

PySimpleGUIの勉強にあたり、以下のWeb記事から概要を知るとよい。

基本的には各ボタンに「key」を紐付けることで、イベントループ内でボタン押下などのイベントを回収し、それぞれの処理を行う構造となる(「音声認識」や「話者変更」など)。

音声収録・音声再生機能

sounddeviceライブラリを用いることにした。

再生にはplay関数、録音にはrec関数を用いる。

再生の際には音割れ防止のための正規化をかけておいた(play_wav関数)。

def play_wav():
    """WAVを再生する"""
    if VARS["audio"] is None or len(VARS["audio"]) == 0:
        raise ValueError("Audio data must not be empty!")

    # 振幅の正規化
    audio = VARS["audio"] / np.abs(VARS["audio"]).max()
    audio = audio * (np.iinfo(np.int16).max / 2 - 1)
    audio = audio.astype(np.int16)

    sd.play(audio, SAMPLE_RATE)

    # 再生は非同期に行われるので、明示的にsleepさせる
    sd.sleep(int(1000 * len(VARS["audio"]) / SAMPLE_RATE))

音声収録の際にはプログレスバーをつけるようにした(listen関数)。sounddeviceのrec関数自体は非同期で動くので、プログレスバーの進行はthreadingを用いた並行処理によった。

def listen():
    """リッスンする関数"""

    # 音声録音の非同期実行
    VARS["audio"] = sd.rec(
        int(DURATION * SAMPLE_RATE), samplerate=SAMPLE_RATE, channels=N_CHANNEL
    )
    time.sleep(BUFFER)  # 少しだけ間を置く

    # 録音している間、プログレスバーを非同期表示
    record_thread = threading.Thread(target=progress_bar, daemon=True)
    record_thread.start()  # 終了すると自動でterminateする

https://gist.github.com/tam17aki/4f11c904832ce270b0fd4e11894bb1aa#file-pysimplegui_recog_tts-py-L204-L215
rec関数の戻り値は(収録サンプル数、チャネル)のshapeをもつarrayであり、モノラルであればチャネル数は1であるが2次元arrayである。

音声認識機能

本記事での音声認識とは、音声データをテキストデータに変換する技術を指す。

実装にはSpeechRecognitionライブラリを用いた。マイク入力で音声取得を行う場合は以前記事を書いたことがある。

今回はマイク入力した直後の音声データだけではなく、既存のwavファイルから音声データを読み込むことのできる仕様にしたいため、音声認識部のスクリプトにも多少の修正が加わっている。音声認識をかける前に、常に一時ファイルに保存したうえで、その一時ファイルをロードする形で音データを取得する。

ちょうどプログラムのこの辺りである(recog関数)。

    # 一旦ファイルに書き込む
    sf.write(
        file=OUTPUT_FILE,
        data=VARS["audio"],
        samplerate=SAMPLE_RATE,
        format="WAV",
        subtype="PCM_16",
    )

    r = sr.Recognizer()
    with sr.AudioFile(OUTPUT_FILE) as source:
        r.adjust_for_ambient_noise(source)
        audio = r.listen(source)  # 音声取得
        try:
            text = r.recognize_google(audio, language="ja-JP")
            VARS["window"]["-RECOG_TEXT-"].Update(text)
        except sr.UnknownValueError:
            VARS["window"]["-RECOG_TEXT-"].Update("認識に失敗しました")

音声合成機能・話者選択機能

本実装で扱う音声合成技術は深層学習に基づく「テキスト音声合成」である。システムにテキストデータを入力し、テキストに対応する合成音声データを出力として得る技術を指す。今回は入力テキストが音声認識結果により得られたものを想定している。

テキスト音声合成に関する機械学習モデルの発展も手伝って、高度な音声合成システムを比較的容易に、各自のローカル計算機環境下で構築・実現できるようになった。最近では『Pythonで学ぶ音声合成』という書籍も出版されており、機械学習・深層学習に基づく高度な音声合成技術をPythonプログラムとともに1冊で学ぶことができる。書籍全体を通しての難易度は専門性の高さから中級者向けであると言える。

今回の実装では、この書籍に付属のPythonライブラリ ttslearn で提供されている、音声合成のための事前学習済モデルを利用することにした。特にテキストから音響特徴量への変換モデルとしてTacotron、また音響特徴量から音声波形への変換モデル(ニューラルボコーダ)としてHiFi-GANに基づく事前学習済モデルを利用する。この事前学習済モデルの使い方は以下に示すページからJupyter notebookを見ることで確認できる。またGoogle Colaboratory上で実際にプログラムを動かし、合成音声を試聴することもできる。

上記リンクより、利用したのは"multspk_tacotron2_hifipwg_jvs24k"(JVSコーパスで訓練された複数話者Tacotoron with HiFi-GAN、サンプリング周波数24kHz) である。16kHzバージョンも利用できるが、多少音質が落ちる。

話者選択機能については、各話者に対応する「ボタン押下イベント」に話者IDを紐づけ、その話者IDを合成モジュールの引数に与えればよい。

def event_synth(event, values):
    """音声合成系のイベント処理"""
    if event == "-SYNTH-":
        text = VARS["window"]["-RECOG_TEXT-"].DisplayText

        # テキスト音声合成
        wav, sr = PWG_ENGINE.tts(text, spk_id=SPK_ID)  # SPK_IDが話者ID

        # 音割れ防止
        wav = (wav / np.abs(wav).max()) * (np.iinfo(np.int16).max / 2 - 1)

        # 再生
        sd.play(wav.astype(np.int16), sr)
        sd.sleep(int(1000 * len(wav) / sr))

関連事例

Seiren Voice

「誰でも100人の声に変えられる声変換システム」Seiren Voiceが発表された。

解説記事によれば、Seiren Voiceは収録された音声を「音素」(=言語情報のひとつ)と「音高」の成分にわけ、それを「学習済みの音声合成モデル」に渡して音声合成を行っている。モデルへの入力において、発話内容に対応する音素は「文字の情報」なので、話者に依存する成分(=声色)が含まれていないという点が重要である。音声合成モデル自体は、話者の数だけ事前に学習しておけばよい(実はこれが大変)。

収録済み音声を言語情報に変換し、それに基づいて音声波形を生成している点においては今回のアプリと関連している。違いの1つはモデルへの入力であり、Seiren Voiceは「音素」と「音高」に分離した状態となる。2つ目の違いは音声波形生成モデルであり、Seiren VoiceはWaveRNNを用いている。

VOICEVOX

今回利用したTactron + HiFi-GANのttslearn実装では、合成時に声の高さや話速のコントロールはできない。あくまで選択された話者について、テキストデータを渡して音声波形を手に入れるのみである。もしこれをコントロール可能となるよう望むならば、機械学習モデル自体に手を入れなければならない。

そのような声の高さや話速のコントロールまで含めた音声合成システムを提供しているのが、VOICEVOXである。


その合成エンジンのソースコードを紐解けば、上記の意味でコントロール可能な枠組みが実装されていることが分かる。
github.com
VOICEVOXのようなアクセント変更向けのGUIをPySimpleGUIで実現することは(おそらく)かなりの工数を要する。未来のスーパーマンに託したい(他力本願!)。

おわりに

今回はGUI構築にPySimpleGUIを用い、音声認識音声合成を直列につないだ非リアルタイム音声変換アプリをPythonで作成した。PySimpleGUIのおかげでGUI構築はかなり楽になったといえる。

Pythonで学ぶ音声合成』はオススメの1冊なので、気になった方はぜひ。