🔊

OpenAIのAPIとUnityで音声会話チャットボットを作る ~ 音声生成編 ~

2023/12/06に公開

はじめに

この記事は Panda株式会社 Advent Calendar 2023 6日目の記事です。
Panda株式会社として初めてのAdvent Calendarとなります。
Panda株式会社は東京大学松尾研究室・香川高専発のスタートアップで、AR技術とAI技術を駆使したシステム開発と研究に取り組んでいます。
このアドベントカレンダーでは、スタートアップとしての知見、AI・AR技術、バックエンドなど、さまざまな領域の記事を公開していきます。
本記事は、Panda株式会社 Advent Calendar 2023の5日目の記事「 OpenAIのAPIとUnityで音声会話チャットボットを作る ~ 音声入力編 ~」の続きです。

環境

  • Unity 2021.3.9f1以降

システム構成

このシステムでは、ユーザーが音声で質問すると、Unity Technologies Japanが開発者のために提供しているオリジナルキャラクター「ユニティちゃん」が音声で応答するチャットボットを作成します。ユーザーの音声での入力はマイクを使用して録音され、その音声データはテキストに書き起こされます。次に、この書き起こされたテキストをOpenAIのchat completions APIに送信し、ユーザーの質問に対する適切な応答を生成します。chat completions APIに送信するテキストには、ユニティちゃんの喋り方を指定するプロンプトと感情分析を含めることで、返却された応答から「ユニティちゃん」の表情を操作します。さらに、生成された応答はGoogle Cloud Platform(GCP)のText-to-Speechサービスを使用して音声データに変換されます。そして、この音声データをUnity内で再生することで、アバターが音声で応答することを実現します。
それぞれの機能を実現するために本記事を含めて以下の4つの記事に分けて実装について説明します。アドベントカレンダーで公開される記事になっているため、投稿日現在(2023/12/06)、アクセスできない記事が含まれています。

  1. OpenAIのAPIとUnityで音声会話チャットボットを作る ~ チャット機能編 ~
  2. OpenAIのAPIとUnityで音声会話チャットボットを作る ~ 音声入力編 ~
  3. (本記事)OpenAIのAPIとUnityで音声会話チャットボットを作る ~ 音声生成編 ~
  4. OpenAIのAPIとUnityで音声会話チャットボットを作る ~ 喜怒哀楽編 ~

音声出力用のオブジェクトを作成する

前々回の記事でチャット機能を作成し、ユーザの入力に対する応答が返却されるようになりました。この応答を音声に変換し再生することで、ユーザがシステムに話しかけると音声で返事が来るチャットボットを実現します。
Unityで音声を再生するにはAudio Sourceを用います。
ヒエラルキーを右クリックし、Audio → Audio Sourceの順で選択し、Buttonをシーンに追加します。
Audio Sourceを追加

Google Cloud Platform(GCP)のText-to-speechサービスとの連携

ユーザの入力に対する応答テキストを音声データに変換するために、GCPが提供するText-to-Speechを用いて音声ファイルを生成することができます。Text-to-Speechを使用すると、入力されたテキストから自然に聞こえる人間の音声を合成し、再生可能な音声を生成することができます。

この機能を利用するには、音声合成したいテキストを含むリクエストをGCPのAPIに送信し、生成された音声ファイルを受け取る必要があります。UnityからAPIリクエストを送信するにはUnityWebRequestを使います。

1.Google Cloud Platform ConsoleでAPIを有効化する

Google Cloud Text-to-Speech APIを実行するために始める前にのページを参考に基本設定を行います。
必要な基本設定は以下の通りです。

  • GCPプロジェクトでText-to-Speechを有効にする
      1. Text-to-Speechで課金を有効にする
      1. 1つ以上のサービスアカウントをText-to-Speechに割り当てる
      1. サービスアカウントのAPIキーをダウンロードする
  • 認証情報の環境変数を設定する

2.APIに対するリクエストの送受信

UnityWebRequestを用いて、APIにリクエストを送るコードを作成します。リクエストには音声合成したいテキストを含める必要があります。
APIを呼び出すために必要なリクエストと返却される形式についてはGCPのtext-to-speech APIのドキュメントから確認することができます。
https://texttospeech.googleapis.com/v1/text:synthesize 」エンドポイントに生成したいテキストを入れたJSONをPostすることでリクエストを送ることができます。
UnityWebRequestでPostするにはPostリクエストを作成する必要があります。以下は、UnityからAPIリクエストを送信・受信するためのスクリプトです。

GoogleTextToSpeech.csの全体
GoogleTextToSpeech.cs
// 必要なライブラリをインポート
using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using System;

// AudioSourceコンポーネントが必要であることを指定
[RequireComponent(typeof(AudioSource))]
public class GoogleTextToSpeech : MonoBehaviour
{
    [SerializeField]
    private string apikey; // Google APIキー
    private string URL; // Google Text-to-Speech APIのURL
    private AudioSource _audioSource; // オーディオソースコンポーネント

    // Google APIリクエストのためのデータ構造
    [System.Serializable]
    private class SynthesisInput
    {
        public string text; // 変換するテキスト
    }

    [System.Serializable]
    private class VoiceSelectionParams
    {
        public string languageCode = "ja-JP"; // 言語コード(日本語)
        public string name; // 音声の名前
    }

    [System.Serializable]
    private class AudioConfig
    {
        public string audioEncoding = "LINEAR16"; // オーディオエンコーディング形式
        public int speakingRate = 1; // 話速
        public int pitch = 0; // ピッチ
        public int sampleRateHertz = 16000; // サンプルレート
    }

    [System.Serializable]
    private class SynthesisRequest
    {
        public SynthesisInput input; // 入力テキスト
        public VoiceSelectionParams voice; // 音声の設定
        public AudioConfig audioConfig; // オーディオ設定
    }

    [System.Serializable]
    private class SynthesisResponse
    {
        public string audioContent; // 変換後のオーディオコンテンツ(Base64エンコーディング)
    }

    // 開始時の初期設定
    private void Start()
    {
        _audioSource = GetComponent<AudioSource>(); // AudioSourceコンポーネントを取得
        URL = "https://texttospeech.googleapis.com/v1/text:synthesize?key=" + apikey; // APIのURLを構築
    }

    // テキストを音声に変換して再生するメソッド
    public void SynthesizeAndPlay(string text)
    {
        StartCoroutine(Synthesize(text)); // 同期処理を開始
    }

    // Google Text-to-Speech APIを呼び出して音声データを取得するコルーチン
    private IEnumerator Synthesize(string text)
    {
        // リクエストデータを作成
        SynthesisRequest requestData = new SynthesisRequest
        {
            input = new SynthesisInput { text = text },
            voice = new VoiceSelectionParams { languageCode = "ja-JP", name = "ja-JP-Neural2-B" },
            audioConfig = new AudioConfig { audioEncoding = "LINEAR16", speakingRate = 1, pitch = 0, sampleRateHertz = 16000 }
        };

        // UnityWebRequestを作成し、POSTリクエストを送信
        using (UnityWebRequest www = new UnityWebRequest(URL, "POST"))
        {
            byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(JsonUtility.ToJson(requestData));
            www.uploadHandler = new UploadHandlerRaw(bodyRaw);
            www.downloadHandler = new DownloadHandlerBuffer();
            www.SetRequestHeader("Content-Type", "application/json");
            yield return www.SendWebRequest();

            // リクエストが成功した場合
            if (www.result == UnityWebRequest.Result.Success)
            {
                string response = www.downloadHandler.text;
                SynthesisResponse synthesisResponse = JsonUtility.FromJson<SynthesisResponse>(response);
                PlayAudioFromBase64(synthesisResponse.audioContent); // Base64エンコードされた音声データを再生
            }
            else // リクエストが失敗した場合
            {
                Debug.LogError("Google Text-to-Speech Error: " + www.error);
            }
        }
    }

    // Base64エンコードされたオーディオデータを再生するメソッド
    private void PlayAudioFromBase64(string base64AudioData)
    {
        byte[] audioBytes = System.Convert.FromBase64String(base64AudioData);
        LoadAudioClipAndPlay(audioBytes); // オーディオクリップをロードして再生
    }

    // オーディオデータをAudioClipに変換して再生するメソッド
    private void LoadAudioClipAndPlay(byte[] audioData)
    {
        int sampleRate = 16000; // Google Text-to-Speechのデフォルトサンプルレートは16kHz
        int channels = 1; // モノラル

        // オーディオデータを浮動小数点配列に変換
        int samplesCount = audioData.Length / 2; // 16-bit PCM, so 2 bytes per sample
        float[] audioFloatData = new float[samplesCount];
        
        // PCMバイトデータをfloat配列に変換
        for (int i = 0; i < samplesCount; i++)
        {
            short sampleInt = BitConverter.ToInt16(audioData, i * 2); // 2バイトをshort intに変換
            audioFloatData[i] = sampleInt / 32768.0f; // short int範囲(-32768 to 32767)をfloat範囲(-1 to 1)に変換
        }

        // AudioClipを作成し、オーディオデータを設定
        AudioClip clip = AudioClip.Create("SynthesizedSpeech", samplesCount, channels, sampleRate, false);
        clip.SetData(audioFloatData, 0);

        // オーディオソースにクリップを設定し、再生
        _audioSource.clip = clip;
        _audioSource.Play();
    }
}

全体のコードを元に、GCP text-to-speech APIにリクエストを送る方法を紹介します。

UnityでJSON形式のPOSTリクエストを送るには、JSONに変換したいクラスを定義し、そのクラスのインスタンスをJsonUtility.ToJsonメソッドを利用してJSON形式のオブジェクトに変換する必要があります。
以下の部分で、GCP text-to-speech APIにリクエストを行うためのJSONモデルを定義しています。GCP text-to-speech API referenceのリクエストボディの欄を確認すると、以下の二つが必須項目であることがわかります。

  • input object:
    • text string:合成したいテキスト
  • voice object:
    • languageCode string: 言語コードを指定 今回は「ja-JP
    • name string: 声の名前
  • audioConfig object: 生成された音声の構成
    • audioEncoding enum: 音声ファイルの形式 今回は「LINEAR16」(WAV形式のことです)
    • speakingRate number: 話す速度 1が通常
    • pitch number: 音声のピッチ 0が通常
    • sampleRateHertz number: 音声のサンプルレート 今回は16000

以上の要素を持つクラスをSynthesisRequestSynthesisInputVoiceSelectionParamsAudioConfigと定義し、[System.Serializable]属性をつけます。これにより、APIサーバにPOSTリクエストをJSON形式で送るために、クラスのインスタンスをJsonUtility.ToJsonを用いてシリアライゼーションできるようになります。

GoogleTextToSpeech.cs の抜粋
// Google APIリクエストのためのデータ構造
[System.Serializable]
private class SynthesisInput
{
	public string text; // 変換するテキスト
}

[System.Serializable]
private class VoiceSelectionParams
{
	public string languageCode = "ja-JP"; // 言語コード(日本語)
	public string name; // 音声の名前
}

[System.Serializable]
private class AudioConfig
{
	public string audioEncoding = "LINEAR16"; // オーディオエンコーディング形式
	public int speakingRate = 1; // 話速
	public int pitch = 0; // ピッチ
	public int sampleRateHertz = 16000; // サンプルレート
}
[System.Serializable]
private class SynthesisRequest
{
	public SynthesisInput input; // 入力テキスト
	public VoiceSelectionParams voice; // 音声の設定
	public AudioConfig audioConfig; // オーディオ設定
}

以下コードで先ほど定義したSynthesisRequestを元にインスタンスを作ります。この時、textには音声合成したいテキストを、nameには女性の声を使いたいためja-JP-Neural2-Bを指定します。生成したい音声はこの「サポートされている音声と言語」という記事から確認することができます。

GoogleTextToSpeech.cs の抜粋
// リクエストデータを作成
SynthesisRequest requestData = new SynthesisRequest
{
    input = new SynthesisInput { text = text },
    voice = new VoiceSelectionParams { languageCode = "ja-JP", name = "ja-JP-Neural2-B" },
    audioConfig = new AudioConfig { audioEncoding = "LINEAR16", speakingRate = 1, pitch = 0, sampleRateHertz = 16000 }
};

GCP speech-to-text APIにリクエストを送る際はエンドポイントにクエリパラメータでAPIキーを指定する必要があります。そのため、以下のコードでURLを指定します。

GoogleTextToSpeech.cs の抜粋
URL = "https://texttospeech.googleapis.com/v1/text:synthesize?key=" + apikey;

以下にGCP speech-to-text APIにPOSTリクエストを送るためのコードを記述します。まず、UnityWebRequestオブジェクトを作成し、APIのエンドポイントとPOSTメソッドを指定します。リクエストボディには先ほど作ったSynthesisRequestオブジェクトをJsonUtility.ToJsonでJSON形式のバイトデータに変換します。その後、リクエストのuploadHandlerに紐付けます。downloadHandlerにはAPIからのレスポンスを受け取るためのバッファを設定します。

GoogleTextToSpeech.cs の抜粋
// UnityWebRequestを作成し、POSTリクエストを送信
using (UnityWebRequest www = new UnityWebRequest(URL, "POST"))
{
	byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(JsonUtility.ToJson(requestData));
	www.uploadHandler = new UploadHandlerRaw(bodyRaw);
	www.downloadHandler = new DownloadHandlerBuffer();
	www.SetRequestHeader("Content-Type", "application/json");
}

ここまでで作成したリクエストを送信し、応答が返却されるまで待機します。

GoogleTextToSpeech.cs の抜粋
yield return www.SendWebRequest();

リクエストが返却されたら、その結果を処理します。リクエストがうまくいかなかった場合はエラー処理し、うまくいった場合は、レスポンスをクラスに変換し、Unity側から扱えるようにします。JsonUtility.FromJsonを用いることで、JSON形式のテキストをclassオブジェクトにシリアライズすることができます。

GoogleTextToSpeech.cs の抜粋
// リクエストが成功した場合
if (www.result == UnityWebRequest.Result.Success)
{
	string response = www.downloadHandler.text;
	SynthesisResponse synthesisResponse = JsonUtility.FromJson<SynthesisResponse>(response);
		PlayAudioFromBase64(synthesisResponse.audioContent); // Base64エンコードされた音声データを再生
}
else // リクエストが失敗した場合
{
	Debug.LogError("Google Text-to-Speech Error: " + www.error);
}

GCP text-to-speech APIから返却されるレスポンスは次のようになっています。

  • audioContent string: リクエストで指定された通りにエンコードされた音声データのバイトデータ Base64でエンコードされた文字列

この要素を持つクラスをSynthesisResponseとして定義します。以下のように[System.Serializable]属性を指定し、JSON形式のテキストをシリアライズできるようにします。

GoogleTextToSpeech.cs の抜粋
[System.Serializable]
private class SynthesisResponse
{
	public string audioContent; // 変換後のオーディオコンテンツ(Base64エンコーディング)
}

PlayAudioFromBase64(synthesisResponse.audioContent);を用いて、Base64でエンコードされたWAV形式の音声データを再生します。再生に用いるコードは以下の通りです。

GoogleTextToSpeech.cs の抜粋
// Base64エンコードされたオーディオデータを再生するメソッド
private void PlayAudioFromBase64(string base64AudioData)
{
	byte[] audioBytes = System.Convert.FromBase64String(base64AudioData);
	LoadAudioClipAndPlay(audioBytes); // オーディオクリップをロードして再生
}

Unityで音声を再生するにはAudioSourceAudioClip形式の音声を設定し再生する必要があります。そのため、LoadAudioClipAndPlayという音声のバイトデータをAudioClipに変換して再生するメソッドを定義し、実行します。

GoogleTextToSpeech.cs の抜粋
// オーディオデータをAudioClipに変換して再生するメソッド
private void LoadAudioClipAndPlay(byte[] audioData)
{
	int sampleRate = 16000; // Google Text-to-Speechのデフォルトサンプルレートは16kHz
	int channels = 1; // モノラル

	// オーディオデータを浮動小数点配列に変換
	int samplesCount = audioData.Length / 2; // 16-bit PCM, so 2 bytes per sample
	float[] audioFloatData = new float[samplesCount];

	// PCMバイトデータをfloat配列に変換
	for (int i = 0; i < samplesCount; i++)
	{
	    short sampleInt = BitConverter.ToInt16(audioData, i * 2); // 2バイトをshort intに変換
	    audioFloatData[i] = sampleInt / 32768.0f; // short int範囲(-32768 to 32767)をfloat範囲(-1 to 1)に変換
	}

	// AudioClipを作成し、オーディオデータを設定
	AudioClip clip = AudioClip.Create("SynthesizedSpeech", samplesCount, channels, sampleRate, false);
	clip.SetData(audioFloatData, 0);

	// オーディオソースにクリップを設定し、再生
	_audioSource.clip = clip;
	_audioSource.Play();
}

GCP text-to-speech APIでリクエストの送受信を実現するスクリプトの解説は以上です。

GoogleTextToSpeech.csを最初に作成したAudio Sourceオブジェクトにアタッチします。
インスペクターからApiKeyに先ほど取得したGCPのAPIキーを記述します。
Audio SourceにAPIキーを登録
これにより、ユーザのメッセージに対応する応答が音声で返ってくるようなチャットボットを実現することができます。

おわりに

今回は「OpenAIのAPIとUnityで音声会話チャットボットを作る ~ 音声入力編 ~」というテーマでPanda株式会社 Advent Calendar 2023 6日目を執筆させていただきました。
本記事では、UnityでGCPを用いてメッセージの応答を音声合成で出力できるチャットボットを作成しました。
明日の記事も私、Panda株式会社代表取締役の田貝奈央による「OpenAIのAPIとUnityで音声会話チャットボットを作る ~ 喜怒哀楽編 ~」です。この記事を読めばチャットボットの応答を音声で出力できるようになります。明日の記事をお楽しみに!

Panda株式会社

Discussion