MetaQuest2でユニティちゃん（ChatGPT）とおしゃべりしたい

最近、Unity、OpenAIのAPI、VOICEVOXを組み合わせてAIのキャラクターとおしゃべりをするという記事やサービスがたくさん出てきているように感じます。
私自身もそういった記事を見て、PCの画面から文字を打ち込んだり音声認識できるものを作ってみたんですが、せっかくなのでVR上でやってみようと思いました。

# 作ったものはこちら


<iframe width="376" height="368" src="https://www.youtube.com/embed/BbiStb1hUEQ" title="ユニティちゃんとVRでおしゃべりしたい" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

動画内では音声入力の部分が録れていませんが、実際はMetaQuest2のマイクで入力ができています。

コピーしてそのまま使ったコードも混ざっているので、部分部分にはなりますが一部コードを記載します。
音声を入力してテキストに変換する部分はWhisper APIを使いました。
また、テキストをGPT3.5-turboで渡し、応答してもらいます。

```C#
    private const string _openAIChatCompletionsUrl = "https://api.openai.com/v1/chat/completions";
    private const string _openAIAudioTranscriptionsUrl = "https://api.openai.com/v1/audio/transcriptions";
    private const int _maximumNumberOfMessages = 5;

    [System.Serializable]
    public class Message{
        public string role;
        public string content;
    }

    //APIキーを外部ファイルから読み取る
    _openAIApiKey = config.openAIApiKey;
    //初期プロンプトを格納しておく
    _conversationHistory = new requestJsonInfo(){
        model = "gpt-3.5-turbo",
        messages = new Message[]{
            new Message(){
                role = "system",
                content = "When replying, please use no more than 100 characters in Japanese.";,
            }
        }
    };
    //Whisper API
    private UnityWebRequest CreateUnityWebRequestForAudioFile(string directoryPath, string url, string openAIApiKey){
        UnityEngine.Debug.Log("CreateUnityWebRequestForAudioFile");
        byte[] getBinalyData = File.ReadAllBytes(directoryPath);

        List<IMultipartFormSection> formData = new List<IMultipartFormSection>();
        formData.Add(new MultipartFormFileSection("file", getBinalyData, Path.GetFileName(directoryPath), "audio/wav"));
        formData.Add(new MultipartFormDataSection("model", "whisper-1"));

        UnityWebRequest www = UnityWebRequest.Post(url, formData);
        www.downloadHandler = new DownloadHandlerBuffer();
        www.SetRequestHeader("Authorization", $"Bearer {_openAIApiKey}");

        return www;
    }

 //ChatGPT
 private UnityWebRequest CreateUnityWebRequestForJson(string jsonBody , string url, string openAIAipKey){
        UnityWebRequest www = new UnityWebRequest(url, "POST");
        byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonBody);
        www.uploadHandler = new UploadHandlerRaw(bodyRaw);
        www.downloadHandler = new DownloadHandlerBuffer();
        www.SetRequestHeader("Content-Type", "application/json");
        www.SetRequestHeader("Authorization", $"Bearer {_openAIApiKey}");

        return www;
    }

```

文章量が増えると費用が怖いので、100文字までの日本語で返してもらうようにSystemとして常に扱うようにしました。


リクエストして返ってきたUnityWebRequestからChatGPTの応答文を取り出してVOICEVOXに渡しています。
なお、VoiceVoxConnectionというクラスはこちらを使わせていただきました。
https://note.com/negipoyoc/n/n081e25f5ee9e

```C#
    public async UniTask<UnityWebRequest> GetCompletion(string prompt)
    {

        try {
            UnityWebRequest www = CreateUnityWebRequestForJson(文字列, _openAIChatCompletionsUrl, _openAIApiKey);

            await www.SendWebRequest();
            var json = www.downloadHandler.text;
            var obj = JsonConvert.DeserializeObject<responseText>(json);

            return www;
        }
        catch(Exception ex)
        {
            UnityEngine.Debug.LogError("Error getting completion: " + ex.Message);
            // throw;
            return null;
        }
    }

    public async UniTask GenerateAudioFromText()
    {
        var openAIResponse = await _openAIAgent.GetCompletion(inputText);

        //応答文
        string openAIReplyText = openAIResponse.downloadHandler.text;
        openAIResponse.Dispose();

        //
        audioResponseFile.clip = await _voiceVoxConnection.TranslateTextToAudioClip(aiReplyMessage);

        await UniTask.SwitchToMainThread();

        //VOICEVOXから取得した音声を再生
        audioResponseFile.Play();
    }


```

VOICEVOXはGoogle CloudのVM上で立ち上げました。
![VM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/128052/b3042c18-c4af-a36a-2da2-1ac47bf897c4.png)


ただCPU版では返答がめちゃくちゃ遅い。GPUで構築すればだいぶ早くなるはずですが、費用が結構かかるので今回は見送り。

基本的にはすでに作られている方の記事を参考にしつつ、ChatGPT(GPT-4)で質問しながら作っていったので、自分でコードを考えて書いた部分はかなり少なくできました。

難しかったのはLip Syncの部分で
```Array index (15) is out of bounds (size=11)```
というエラーが表示され、LaughterBlendTargetの設定をいじっても実行するたびにクラッシュしてしまっていたことで、こちらは回避はしたものの根本解決はしていないので継続して調べたいと思います。


# 今後やってみたいこと
今はまだ決まった動作しかしていませんが、例えば感情に合わせて表情や動きを変えるとかできれば、よりVR体験として面白そう。
あとは最低限の部分しか作ってないので、RPG風にしたりしてみたい。


# 使用した素材など

VOICEVOX:春日部つむぎ
© Unity Technologies Japan/UCL



# 参考：

https://note.com/negipoyoc/n/n081e25f5ee9e

https://zenn.dev/umyomyomyon/articles/5f07abe67a289b

https://blog.gcrest.com/archives/404