UnityとVOICEVOXでChatGPTと音声対話できるシステムを作る

# はじめに
本記事は`Cocone Advent Calendar 2023 22日目`の記事となります。

こんにちは。ココネ株式会社でクライアントエンジニアをしているtakokurageと申します。
20日の[@maphy](https://qiita.com/maphy)、21日の[LeNia](https://qiita.com/LeNia)と同期のクライアントエンジニアからのパスを受け取り、しっかりと記事を書いていこうと思います！

さて、ここ数年でAI技術が進歩し、一般人もその恩恵を得ることができる機会が増えてきたと感じています。
その最たるものがChat GPTではないでしょうか？
知らない言葉について調べる時や、コードを書いている時に命名に迷った時、はたまた暇な時の雑談の相手として活用しているのですが、音声対話をすることができればより面白いのでは？と思い、今回のアドベントカレンダーのテーマとしてChat GPTと音声対話をすることができるシステムを作ってみることにしました。


# 今回作るもの

今回作るのはOpenAIとVOICEVOXを利用して、AIとの音声対話ができるシステムになります。

音声入力については様々な手段がありますが、今回はUnityのWindows.Speech.DictationRecognizerを使用します。そのため、今回作るシステムはWindows環境でのみ利用可能なものとなります。

# 使用したもの
* Unity
* VOICEVOX
* OpenAI API
* [UniTask](https://github.com/Cysharp/UniTask)
* [Unity VOICEVOX Bridge](https://github.com/mikito/unity-voicevox-bridge)

# 実装

まずは処理の流れを整理します。

1. Unityで音声入力をstringにする
1. 音声入力したものを使い、OpenAI APIを叩く
1. 返答をVOICEVOXを利用して音声ファイルを生成し、再生する

今回のシステムはこの3つのステップに沿って実装をしていきます。

ここで注意点として、あらかじめWindows11のオンライン音声認識をオンにする必要があります。
設定→プライバシーとセキュリティ→音声認識→オンライン音声認識
の順に進んでオンライン音声認識をオンにしておきます。

# Unityで音声入力を実装する
まず初めに音声入力部分を実装します。
今回はWindows.Speech.DictationRecognizer([リファレンス](https://docs.unity3d.com/ja/2021.1/ScriptReference/Windows.Speech.DictationRecognizer.html))を使用するので、リファレンスのサンプルコードなどを参考にしながら実装を行います。

```C#:DictationProcessor.cs
using UnityEngine;
using UnityEngine.Windows.Speech;

public class DictationProcessor : MonoBehaviour
{
    DictationRecognizer dictationRecognizer;

    async void Start()
    {
        dictationRecognizer = new DictationRecognizer();
        
        dictationRecognizer.DictationResult += (text, confidence) => 
        {
            Debug.Log(text);
        };

        dictationRecognizer.DictationComplete += (cause) => 
        {
            dictationRecognizer.Start();
        };

        dictationRecognizer.Start();
    }

    private void OnDestroy()
    {
        dictationRecognizer.Dispose();
    }
}
```

これで音声入力が実装できました。
試しに適当なGameObjectにアタッチして再生してみましょう。


![スクリーンショット 2023-12-21 201353.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1506069/147edaca-168f-fac7-b590-258265b19c33.png)

無事に入力されましたね。

より認識精度が高い結果のみを利用したい場合はconfidenceを利用することで、認識精度が低いものを弾くことができます。

# OpenAI APIを叩く
音声入力を実装できたので、次はOpenAIを叩く部分について実装します。
今回は省略しますが、OpenAIの会員登録をし、APIキーを取得します。
取得したAPIキーと、入力された文章を用いてOpenAIのAPIを叩く処理を実装します。

```C#:ChatGptClient
using System;
using System.Collections.Generic;
using System.Text;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;

public class ChatGptClient
{
    [Serializable]
    public class ChatGPTMessageModel
    {
        public string role;
        public string content;
    }
    
    [Serializable]
    public class ChatGPTOptions
    {
        public string model;
        public List<ChatGPTMessageModel> messages;
    }
    
    [Serializable]
    public class ChatGPTResponseModel
    {
        public string id;
        public string @object;
        public int created;
        public Choice[] choices;
        public Usage usage;

        [System.Serializable]
        public class Choice
        {
            public int index;
            public ChatGPTMessageModel message;
            public string finish_reason;
        }

        [System.Serializable]
        public class Usage
        {
            public int prompt_tokens;
            public int completion_tokens;
            public int total_tokens;
        }
    }

    private readonly string apiKey = "取得したAPIキー";
    private readonly string apiUrl = "https://api.openai.com/v1/chat/completions";
    private readonly List<ChatGPTMessageModel> messageList = new List<ChatGPTMessageModel>();

    public ChatGptClient()
    {
        messageList.Add(new ChatGPTMessageModel(){ role = "system", content = "語尾に「なのだ」をつけてください"});
    }
    
    public async UniTask<string> RequestAsync(string userMessage)
    {
        messageList.Add(new ChatGPTMessageModel {role = "user", content = userMessage});
        
        Dictionary<string, string> headers = GetAPIRequestHeaders();
        
        ChatGPTOptions options = GetChatGptOptions();
        string jsonOptions = JsonUtility.ToJson(options);
        
        UnityWebRequest request = GetRequest(jsonOptions);

        foreach (var header in headers)
        {
            request.SetRequestHeader(header.Key, header.Value);
        }

        await request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.ConnectionError ||
            request.result == UnityWebRequest.Result.ProtocolError)
        {
            Debug.LogError(request.error);
            throw new Exception();
        }
        else
        {
            var responseString = request.downloadHandler.text;
            var responseObject = JsonUtility.FromJson<ChatGPTResponseModel>(responseString);
            string replyMessage = responseObject.choices[0].message.content;
            
            return replyMessage;
        }
    }

    private Dictionary<string, string> GetAPIRequestHeaders()
    {
        var headers = new Dictionary<string, string>
        {
            {"Authorization", "Bearer " + apiKey},
            {"Content-type", "application/json"},
            {"X-Slack-No-Retry", "1"}
        };

        return headers;
    }

    private ChatGPTOptions GetChatGptOptions()
    {
        var options = new ChatGPTOptions()
        {
            model = "gpt-3.5-turbo",
            messages = messageList
        };

        return options;
    }

    private UnityWebRequest GetRequest(string jsonOptions)
    {
        var request = new UnityWebRequest(apiUrl, "POST")
        {
            uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(jsonOptions)),
            downloadHandler = new DownloadHandlerBuffer()
        };

        return request;
    }
}

```

```
 messageList.Add(new ChatGPTMessageModel(){ role = "system", content = "語尾に「なのだ」をつけてください"});
```
この部分ではrole="system"とすることでChatGPTに設定を与えることができます。content内に与えたい設定を入力することで返答の話し方を変えたりすることができるので色々試してみると面白いです。


次に実装したものを音声入力の処理側で呼び出します。
```C#:DictationProcessor
dictationRecognizer.DictationResult += async (text, confidence) => 
{
    dictationRecognizer.Stop();
            
    Debug.Log("Request = " + text);
    ChatGptClient chatGptClient = new ChatGptClient();
    string replyMessage = await chatGptClient.RequestAsync(text);
    Debug.Log("Reply = " + replyMessage);
            
    dictationRecognizer.Start();
};
```

![スクリーンショット 2023-12-21 202117.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1506069/815b80df-eb45-9f48-c2ca-4979359e6940.png)


実際に再生し、何かChatGPTに聞いてみると無事に返答が得られましたね。

# VOICEVOXで音声ファイルを生成し、再生する
最後にChatGPTからの返答をVOICEVOXを用いて音声ファイルを生成し、再生する実装です。
この部分については「[Unity VOICEVOX Bridge](https://github.com/mikito/unity-voicevox-bridge)」というライブラリを使うことで簡単に実装ができます。
先ほど実装したChatGPTからの返答をVOICEVOXで生成し、再生する処理を追加します。
```C#:DictationProcessor
using UnityEngine;
using UnityEngine.Windows.Speech;
using VoicevoxBridge;

public class DictationProcessor : MonoBehaviour
{
    [SerializeField] VOICEVOX voiceVox;
    DictationRecognizer dictationRecognizer;

    async void Start()
    {
        dictationRecognizer = new DictationRecognizer();

        dictationRecognizer.DictationResult += async (text, confidence) => 
        {
            dictationRecognizer.Stop();
            Debug.Log("Request = " + text);
            ChatGptClient chatGptClient = new ChatGptClient();
            string replyMessage = await chatGptClient.RequestAsync(text);
            Debug.Log("Reply = " + replyMessage);
            SpeechVoiceVox(replyMessage);
            
            dictationRecognizer.Start();
        };

        dictationRecognizer.DictationComplete += (cause) => 
        {
            dictationRecognizer.Start();
        };

        dictationRecognizer.Start();
    }

    private void OnDestroy()
    {
        dictationRecognizer.Dispose();
    }

    private async void SpeechVoiceVox(string text)
    {
        var voice = await voiceVox.CreateVoice(0, text);
        await voiceVox.Play(voice);
    }
}
```
これでChatGPTからの返答が来るとVOICEVOXで音声を生成し、Unity上で再生することができます。
注意点としてはVOICEVOXを立ち上げていないと動作しないので、あらかじめ立ち上げておきましょう。

今回のコードではずんだもんの通常ボイスで再生していますが、CreateVoiceの引数を変更することでボイスを変更することができます。

# まとめ
使ってみた感想としては全体的に処理に時間がかかるなぁと感じました。
音声入力、OpenAIからの返答待ち、VOICEVOXでの音声生成の各段階でそれぞれ時間がかかってしまいます。

また、今回の実装では文脈を踏まえての回答はしてくれません。
文脈を踏まえての回答をしてくれるのもChatGPTの強みだと思うので、その点については今後改善をしたいと思いました。

しかし改善点や課題はあるものの、小さい頃にはAIと音声対話をするなんて遠い未来の話に思えていたものが、サービスを利用することで自作できるようになっていることに技術の進歩を感じますね。

明日は@yoshiaki_sさんによる「intellijIDEAでGitHub Copilot Chatを使う方法」です。
お楽しみに！