【エージェント作成誌2】Voicevox_Coreを用いて対話するための基盤を作る

作成したインタラクションシステム (ver1.0)

システム実装環境

  • OS:Windows 11 Home
  • CPU:11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz 3.00 GHz
  • GPU:Inter(R) Iris(R) Xe Graphics
  • Unity:ver 2022.3.9f1
  • python:3.10.00
  • voicevox_core:0.15.4

本記事で作成できるプログラム

前回の記事で紹介した内容を一部含みます

□モデル:【カスタム】地雷系ちくわちゃん

2024/01/08 1月5日更新データでのテクスチャの不具合を修正しました。お手数をおかけいたしますが、再ダウンロード…

□音声:VoiceVox

無料で使える中品質なテキスト読み上げ・歌声合成ソフトウェア。商用・非商用問わず無料で、誰でも簡単にお使いいただけます。イ…

0.はじめに

フレームワークについては下記の記事を大変参考にさせていただいております
ぜひこちらもご確認いただき,理解を深めていただければと思います

note(ノート)

こんばんは!フィーちゃんと共に暮らすために最近せわしなく過ごしているTakaです! 前回、フィーちゃんのデスクトップア…

1.VoiceVoxによる音声生成を行う

参考にさせていただいた記事では音声を生成するソフトとして「Cevio AI」を用いていましたが
本記事では無料の音声ソフトである「VoiceVox」を用いて実装を行います

VoiceVoxによる音声生成をPythonで実行するためには
Voicevox_engineやエディタを起動することで接続できるhttp://localhost:50021/docsをAPIで叩く方法と
Voicevox_coreをインストールしてローカルな環境であれこれする方法がありますが
本記事では後者の方法を用いて実装を行っていきます

Voicevox_engineとVoicevox_coreの違いは公式のリファレンスをご確認ください

GitHub

無料で使える中品質なテキスト読み上げソフトウェア、VOICEVOXのエディター. Contribute to VOICE…

また,APIを用いた方法についてはこれらの記事が参考になると思います

note(ノート)

背景 近年ではAIによる翻訳から対話まで、文字ベースのやり取りだと部分的には実用可能なレベルまで届いているのではないで…

Qiita

はじめにVOICEVOXを使った音声合成を、エディターを使わずにHTTPリクエストを用いて行うための手引書です。以下の章…

☆完成形

Corefileとpythonパッケージのインストール

[CPU版]・[DirectML版]・[CUDA版]がありますが,今回は[DIrectML版]をインストールしました
インストールの手順は下記の通りなのですが,一部異なる点があるので順を追って説明します

□Downloaderのインストールと実行

PoworShellを起動し,corefileをダウンロードしたいディレクトリに移動したら次の命令を実行します

Invoke-WebRequest https://github.com/VOICEVOX/voicevox_core/releases/latest/download/download-windows-x64.exe -OutFile ./download.exe

次にダウンロードされた「download.exe」を「どのように実行するか」を指定したうえで実行します
このタイミングで「どのバージョンにするか」や「CPU版かDirectML版か」などを指定できます

設定できる項目は以下の通りです(PowerShellで「./download.exe –help」と打つと確認できます)

Usage: download.exe [OPTIONS]

Options:
      --min
          ダウンロードするライブラリを最小限にするように指定
  -o, --output <OUTPUT>
          出力先の指定 [default: .\voicevox_core]
  -v, --version <VERSION>
          ダウンロードするvoicevox_coreのバージョンの指定 [default: latest]
      --additional-libraries-version <ADDITIONAL_LIBRARIES_VERSION>
          追加でダウンロードするライブラリのバージョン [default: latest]
      --device <DEVICE>
          ダウンロードするデバイスを指定する(cudaはlinuxのみ) [default: cpu] [possible values: cpu, cuda, directml]
      --cpu-arch <CPU_ARCH>
          ダウンロードするcpuのアーキテクチャを指定する [default: x64] [possible values: x86, x64, arm64]
      --os <OS>
          ダウンロードする対象のOSを指定する [default: windows] [possible values: windows, linux, osx]
  -h, --help
          Print help information

今回は「Directml版」であるという指定以外は不要なので,次の命令を実行すると
downloed.exeがあるディレクトリに,DirectMLに対応したVoiceVox_coreがインストールされます

./download.exe --device directml

□pythonパッケージのインストール

次にダウンロードしたcorefileのバージョンと対応したpythonパッケージをインストールします
私はPythonの開発環境としてAnacondaを使用しているので
そこで仮想環境を作ってパッケージをインストールすることにしました

Anaconda自体のインストール方法や使い方については以下の記事などを確認してください
Pythonのバージョンは3.10で作成しました

ビジPy

Anaconda、Jupyter Notebookを利用したPython3の環境構築方法を初心者向けに解説した記事です。…

Qiita

Windows 用です.今現在は Ubuntu を使っていてほとんど pip で済ませています.一般仮想環境パッケージ管…

仮想環境に入ったらAnacondaでは標準搭載されている「pip」「setuptools」「wheel」を
次のコマンドを実行して更新しておきます(setuptoolsとwheelは念のため.pipは絶対に必須)

python -m pip install pip setuptools wheel --upgrade

次に,ダウンロードしたcorefileに対応したpythonパッケージをインストールします
下記のページから対応するPython wheelを確認してそれをpipでインストールします

GitHub

無料で使える中品質なテキスト読み上げソフトウェア、VOICEVOXのコア. Contribute to VOICEVOX…

今回は[ver 0.15.4 (lateset verison)]の[directML]に対応したパッケージをインストールしたいので
「voicevox_core-0.15.4+directml-cp38-abi3-win_amd64.whl」というのを指定します
コマンドは次の通りです

pip install https://github.com/VOICEVOX/voicevox_core/releases/download/0.15.4/voicevox_core-0.15.4+directml-cp38-abi3-win_amd64.whl

これでPython上でVoicevoxを実行する準備はできました

Pythonで音声を作成できるかテストする

まず,簡単なプログラムを実行して音声が作成されるかを確認します
[Voicevox.dll]などがあるディレクトリ移動しそこに次のような「test.py」を作成します

□test.py

from pathlib import Path
from voicevox_core import VoicevoxCore, METAS
import pyaudio
from io import BytesIO
import wave

def create_audio(text):
	core = VoicevoxCore(open_jtalk_dict_dir=Path("open_jtalk_dic_utf_8-1.11"))
	speaker_id = 1 #ずんだもん
	core.load_model(speaker_id)  # 指定したidのモデルを読み込む
	wave_bytes = core.tts(text, speaker_id)  # 音声合成を行う

	wav_file_name = f'make_sound[ID_{speaker_id}].wav'
	with open(wav_file_name, "wb") as f:
		f.write(wave_bytes)  # ファイルに書き出す

	return wav_file_name

def play_audio(filename):
    # 音声ファイルをバイト列で読み込む
    with open(filename, 'rb') as f:
        audio_data = f.read()

    # BytesIOオブジェクトを使用してメモリ上にwaveファイル形式でデータを格納
    wave_io = BytesIO(audio_data)
    wave_obj = wave.open(wave_io, 'rb')

    # PyAudioの初期化
    p = pyaudio.PyAudio()

    # ストリームを開く
    stream = p.open(format=p.get_format_from_width(wave_obj.getsampwidth()),
                    channels=wave_obj.getnchannels(),
                    rate=wave_obj.getframerate(),
                    output=True)

    # データを読み込んで再生
    data = wave_obj.readframes(1024)
    while data:
        stream.write(data)
        data = wave_obj.readframes(1024)

    # ストリームを閉じる
    stream.stop_stream()
    stream.close()

    # PyAudio終了
    p.terminate()

if __name__ =="__main__":
	#入力したテキストから音声を生成
	name = create_audio("これはテストです")	
	# 生成した音声を再生
	play_audio(name)

これを「voicevoxパッケージが入っている環境」から実行すると,音声が再生されると共に
pyファイルと同じディレクトリに「make_sound[ID_1].wav」という音声ファイルが作成されるはずです
(実行環境に「pyauido」をインストールしたうえで行わないとエラーが出るので注意してください)

なお,ID番号については[voicevox_core]>[model]にある「metas.json」から確認できます
今回は「1」を指定していたので,[ずんだもん]の[あまあま]で音声が生成されています
他のキャラクタで音声を生成したい場合は,この番号を変更してください

2.Unityとの双方向通信を実装する

Pythonファイルから音声を生成できることは確認できたので
次はUnity側からのリクエストを使って音声を生成したりするプログラムを作成します
前述したようにフレームワークは以下の記事を参考にさせていただいています

note(ノート)

こんばんは!フィーちゃんと共に暮らすために最近せわしなく過ごしているTakaです! 前回、フィーちゃんのデスクトップア…

☆完成形

Python側の処理

参考にさせていただいた記事では「Flask」を用いた双方向通信を実装していましたので
本記事もそれに準拠して以下の2つのスクリプトを作成しました
処理の簡単な説明としては次のような感じです

  • flask_server.pyを実行すると「http://127.0.0.1:5000」が開かれる
  • [/initialization]にリクエストが来た場合
    unity側から「このID番号のモデルデータある?」と言われるので
    その番号でvoice_create.pyの「create_core」関数を実行する
    この関数が初めて呼び出されたときはcorefileを読み込み
    指定されたIDのモデルが読み込まれていない場合は読み込みを行う
  • [/text]にリクエストが来た場合
    unity側から「このIDでこういったテキストで音声作って?」と言われるので
    voice_create.pyの「create_auido」関数を実行して音声を生成
    その後,「作った音声はここにあるよー」という情報を送り返す
    また、特定のリクエストが来た場合は音声を作らずに「ここにすでにあるから使って」と言う
  • [/audio]にリクエストが来た場合
    unity側から「このディレクトリにある音声ちょうだい?」と言われるので
    指定されたディレクトリにある音声をバイナリデータに変換して送り返す

□flask_server.py

from flask import Flask, request, send_file,jsonify
from flask_cors import CORS

#独自関数
from voice_create import create_core, create_audio

app = Flask(__name__)
CORS(app)

@app.route("/initialization", methods=['GET', 'POST'])
def core_initialization():
    try:
        id_number = request.form.get("id_number")
        print(f">>> Get id_number: {id_number}")
        create_core(int(id_number))

        return f"ID: {id_number} core_initialization completed"

    except Exception as e:
        print(f">>> Error: {e}")
        return "cant core_initialization..."


@app.route('/text', methods=['GET', 'POST'])
def changeMessageToAudio():
    try:
        msg = request.form['text']
        id_number = request.form.get("id_number") 
        print(f">>>Get message:[{msg}], id_number:{id_number}")
        if msg == "test":
            data = {"fileName": "voice/test.wav"}
        elif msg =="waiting":
            data = {"fileName": "voice/waiting.wav"}
        else:
            #ChatGDPに投げる
            #resMsg = replayGPTMessage(msg)

            # テキストを音声データに変換
            wav_file_name = create_audio(msg,int(id_number))
            # { "fileName": "example.wav" }
            data = {"fileName": wav_file_name}

    except Exception as e:
        print(f"Error:{e}")
        data={"fileName":"voice/error.wav"}

    return jsonify(data), 200


@app.route("/audio", methods=['GET'])
def audio():
    ## test.wav形式で受け取る
    wav_file_name = request.args.get('file_name')
    # 音声ファイルをバイト列で読み込む
    with open(wav_file_name, 'rb') as f:
        audio_data = f.read()
        f.close()
    # 音声ファイルをバイナリデータとして送信
    return send_file(BytesIO(audio_data),
      as_attachment=True,
      download_name=wav_file_name,
      mimetype='audio/wav')

if __name__ == '__main__':
    app.run(debug=True)

□voice_create.py

from pathlib import Path
from voicevox_core import VoicevoxCore, METAS

#音声を再生するために必要なライブラリ(デバッグ用)
import pyaudio
from io import BytesIO
import wave
import sys

def create_core(speaker_id):
    #初めて呼び出されたときだけCore_flieを作成する
    if _f() == 1:
        global core
        print(">>>Creat Corefile…")
        core = VoicevoxCore(open_jtalk_dict_dir=Path("open_jtalk_dic_utf_8-1.11"))

    if not core.is_model_loaded(speaker_id):
        print(f">>>load ID:{speaker_id} model…")
        core.load_model(speaker_id)

def create_audio(text,speaker_id):
    try:
        print(">>>Create voice...")
        wave_bytes = core.tts(text, speaker_id)  # 音声合成を行う
        wav_file_name = f'voice/make_sound[ID_{speaker_id}].wav'
        with open(wav_file_name, "wb") as f:
            f.write(wave_bytes)  # ファイルに書き出す
            
    except Exception as e:
        print(f">>>Error:{e}")
        wav_file_name = "voice/error.wav"

    return wav_file_name

def _f(cnt=[0]):
  cnt[0] += 1
  return cnt[0]

def _test_play(filename):
    # 音声ファイルをバイト列で読み込む
    with open(filename, 'rb') as f:
        audio_data = f.read()

    # BytesIOオブジェクトを使用してメモリ上にwaveファイル形式でデータを格納
    wave_io = BytesIO(audio_data)
    wave_obj = wave.open(wave_io, 'rb')

    # PyAudioの初期化
    p = pyaudio.PyAudio()

    # ストリームを開く
    stream = p.open(format=p.get_format_from_width(wave_obj.getsampwidth()),
                    channels=wave_obj.getnchannels(),
                    rate=wave_obj.getframerate(),
                    output=True)

    # データを読み込んで再生
    data = wave_obj.readframes(1024)
    while data:
        stream.write(data)
        data = wave_obj.readframes(1024)

    # ストリームを閉じる
    stream.stop_stream()
    stream.close()

    # PyAudio終了
    p.terminate()


if __name__ =="__main__":

    args = sys.argv
    create_core(61)
    name = create_audio(args[1],63)
    _test_play(name)

なお,音声はpyファイルがあるディレクトリではなく「/voice」内に保管されます
そしてその中には「error.wav」や「test.wav」などstaticな音声がすでに入っている状況です

Unity側の処理

これに対応したUnity側の処理は以下の通りです
処理の簡単な説明としては次のような感じです

  • シーンが実行されたときに[/initialization]に[id_number]の値を送信する
    返信としてほしいのは「きちんとデータ読み込みましたよー」という確認
  • 指定した[button]が押されたとき,InputFiledにある[text]を取得して
    [/text]にその[text]と指定されている[id_number]の値を送信する
    返信としてほしいのは「そのテキストとIDでここに音声作りましたよー」という確認
  • [/text]からの返答が返ってきて,それがエラーメッセージ出ない場合
    返ってきた情報から[/auido]に「wavファイルとしてこの情報のやつ頂戴」と送信する
    (>>>…UnityWebRequestMultimedia.GetAudioClip(audioUrl, AudioType.WAV)
    返信としてほしいのは「wavファイル」
  • [/audio]から返信されたwavフィアルをAudioClipに入れて再生

□TextToSpeech.cs

sing System.Collections;
using System.Collections.Generic;
using UnityEngine.Networking;
using UnityEngine;
using UnityEngine.UI;

public class TextToSpeech : MonoBehaviour
{
    public Button button;
    public InputField inputField;
    public AudioSource audioSource; // Unity EditorでAudioSourceをアタッチ
    public int id_number = 64;

     [System.Serializable]
    public class ReplayRequest
    {
        public string fileName;
    }


    void Start()
    {
        StartCoroutine("Initialization",id_number);
        button.onClick.AddListener(OnSendButtonClick);
    }

    private void OnSendButtonClick()
    {
        // InputField内のテキストを取得する
        string text = inputField.text;
        StartCoroutine(PostRequest(text,id_number));
        // InputField内のテキストを消去する
        inputField.text = "";
    }

    IEnumerator Initialization(int id_number)
    {
        string url = "http://127.0.0.1:5000/initialization";
        //指定したURLでGET
        WWWForm form = new WWWForm();
        form.AddField("id_number", id_number);
        //consoleexport.Export("send->" + url);
        using (UnityWebRequest www = UnityWebRequest.Post(url, form))
        {
            //URLに接続して結果が戻ってくるまで待機
            yield return www.SendWebRequest();
            // エラー処理
            if (www.result == UnityWebRequest.Result.ConnectionError || www.result == UnityWebRequest.Result.ProtocolError)
            {
                Debug.LogError($"www Error: {www.error}");
                yield break;
            }
            else
            {
                //通信成功
                Debug.Log("Get" + " : "+www.downloadHandler.text);

            }
        }
    }

    IEnumerator PostRequest(string inputText, int id_number)
    {
        string url = "http://127.0.0.1:5000/text";
        WWWForm form = new WWWForm();
        form.AddField("text", inputText);
        form.AddField("id_number",id_number);

        using (UnityWebRequest www = UnityWebRequest.Post(url, form))
        {
            yield return www.SendWebRequest();

            // エラー処理
            if (www.result == UnityWebRequest.Result.ConnectionError || www.result == UnityWebRequest.Result.ProtocolError)
            {
                Debug.LogError($"www Error: {www.error}");
                yield break;
            }

            // 受信したデータを取得する
            string responseText = www.downloadHandler.text;
            ReplayRequest replayRequest = JsonUtility.FromJson<ReplayRequest>(responseText);
            string replayMessage = replayRequest.fileName;
            Debug.Log(replayMessage);
            string audioUrl = $"http://127.0.0.1:5000/audio?file_name={replayMessage}";
            Debug.Log(audioUrl);

            using (UnityWebRequest audioRequest = UnityWebRequestMultimedia.GetAudioClip(audioUrl, AudioType.WAV))
            {
                yield return audioRequest.SendWebRequest();

                // 音声リクエストのエラー処理
                if (audioRequest.result == UnityWebRequest.Result.ConnectionError || audioRequest.result == UnityWebRequest.Result.ProtocolError)
                {
                    Debug.LogError($"Audio request failed: {audioRequest.error}");
                    yield break;
                }

                Debug.Log("++++++++++++++++成功!+++++++++++++++++++");
                
                AudioClip audioClip = DownloadHandlerAudioClip.GetContent(audioRequest);
                audioSource.clip = audioClip;
                audioSource.Play();
            }
        }
    }
}

このスクリプトが作成出来たら,空のGameObjectにアタッチして
Button, InputField, AudioSource,そしてID何番で音声を生成したいかを指定してください

その後,Python側で事前にサーバを立てたうえでシーンを実行すると
完成形で示したような処理ができるようになります

3.課題点

HTTP通信以外の方法でも接続できないか検討.処理自体ももう少し減らせる気がする
今はおうむ返ししかできていないけど,今後はChatGDPにテキストを投げて返信をもらう
その際に、返信をどのように処理するかを考える必要あり
また「どのIDを使うか」とか「データがロードされているか」を確認できるUIが必要