📚

【Unity × ChatGPT API × WhisperAPI】音声認識AIアシスタントを実装する

2023/03/13に公開

恋愛メタバースMemoriaを運営するFlamers CTOの設楽(だーら)です。
今回は、OpenAIのAPIを用いて、UnityからAIアシスタントを実装する過程を記事に残したいと思います。

やりたいこと

  • OpenAIのAPIを使ったAIアシスタントを作る
  • こちらは声で話しかけ、相手は文字で返答する(いずれ音声合成もやりたい)
  • こちらの声の音声認識はWhisperAPIを利用
  • 相手の返答は、WhisperAPIによって書き起こされた文字を元に、ChatGPT APIを利用

環境

  • Unity 2021.3.7f1

完成物

全体の構成

詳細

  • ソースコードはリポジトリを見ていただければよいので、ここではそれぞれについて補足をします。

1. 録音について

  • こちらの記事を参考にしました。
  • インスタンス変数としてAudioClipを持つのはとても簡単ですね。

2. AudioClipからwavへの変換について

mp3, mp4, mpeg, mpga, m4a, wav, webm.
  • AudioClipからwav形式に変換するのが一番簡単そうだったのでそうしました。
  • こちらの記事を参考にしました。
  • 戻り値のbyte[]型の値を、WhisperAPIに音源データとして直接渡しています。
WavConverter.cs
    // wavへの変更を担当するクラスの中
    public static byte[] ToWav(this AudioClip audioClip)
    {
        using var stream = new MemoryStream();
        
        WriteRiffChunk(audioClip, stream);
        WriteFmtChunk(audioClip, stream);
        WriteDataChunk(audioClip, stream);

        return stream.ToArray();
    }
  • なお、WhisperAPIをたたくリクエストのBodyを作る際、ファイル名の拡張子は.wavにしないとエラーではじかれます。ファイル名は適当でよいですが、拡張子は正しく指定する必要があります
WhisperAPIConnection.cs
// UnityWebRequestでWhisperAPIをたたいているクラスのメソッドの中
form.Add(new MultipartFormFileSection("file", recordData, "recordData.wav", "multipart/form-data"));

3. WhisperAPIからレスポンスを受け取る

  • 特にありません。
  • WhisperAPIは、直前とのつながりなどはないため、シンプルなレスポンス(textだけのjson)となっています。

4,5. ChatGPT APIにリクエストを送る/レスポンスを受け取る

  • 特筆すべき点は2つあります。
    1. メッセージの履歴を毎回キャッシュし、リクエストの度に全てを載せて送っていること
ChatGPTConnection.cs

// フィールドにキャッシュするためのリストがあります。
private readonly List<ChatGPTMessageModel> _messageList = new();

// リクエストを送る前
// 自分の打った文を、role="user"としてリストに追加しています。
_messageList.Add(new ChatGPTMessageModel()
{
    role = "user",
    content = userMessage
});

// レスポンスが返ってきた後
// messageの中身を、リストに保存していることが分かります。
// roleはAIを意味する"assistant"です。
var responseObject = JsonUtility.FromJson<ChatGPTResponseModel>(responseString);
_messageList.Add(responseObject.choices[0].message);
    1. 最初に、systemとしてAIの人格の設定が可能です
ChatGPTConnection
_messageList.Add(new ChatGPTMessageModel()
{
    role = "system",
    content = "あなたは僕の職場の上司の女性です。仲良しなので、敬語ではなく、砕けた口調で返答してください。返答は短めにすること。"
});

6. viewに表示する

  • 特になし

今後

  • アバターが読み上げて発話してくれるようにしたい。リップシンクなどもいれたい

参考

追記

実装

Profile

SystemProfile.cs
using UnityEngine;

namespace ChatGPTAPI.Config
{
    [CreateAssetMenu(fileName = "SystemProfile", menuName = "ChatGPTAPI/SystemProfile", order = 1)]
    public class SystemProfile : ScriptableObject
    { 
        [TextArea] public string systemContent;
    }
}
UserProfile.cs
using UnityEngine;

namespace ChatGPTAPI.Config
{
    [CreateAssetMenu(fileName = "UserProfile", menuName = "ChatGPTAPI/UserProfile", order = 2)]
    public class UserProfile : ScriptableObject
    {
        public string nickName;
        public string age;
        public string sex;
        public string birthPlace;
        public string job;
        public string currentTask;
        public string hobby;
        public string favoriteFood;
        public string favoriteColor;
        public string favoriteAnimal;
        public string favoriteSeason;
        public string favoriteGame;
        public string favoriteMovie;
        public string favoriteMusic;
        public string favoriteBook;
        public string favoritePlace;
    }
}
  • これでScriptableObjectを作成
  • ChatGPTのAPIをつかさどるクラスに渡し、messageとしておく。
ChatGPTConnection.cs

...

~~ コンストラクタなど ~~

        public void AddSystemProfile(SystemProfile profile)
        {
            var model = new ChatGPTMessageModel()
            {
                role = Role.system.ToString(),
                content = profile.systemContent
            };
            AddMessage(model);
        }
        
        public void AddUserProfile(UserProfile profile)
        {
            var model = new ChatGPTMessageModel()
            {
                role = Role.user.ToString(),
                content = $"自己紹介をします!私の名前は{profile.nickName}です。" +
                          $"年齢は{profile.age}歳です。" +
                          $"性別は{profile.sex}です。" +
                          $"生まれた場所は{profile.birthPlace}です。" +
                          $"仕事は{profile.job}です。" + 
                          $"今やっているタスクは{profile.currentTask}です。" +
                          $"趣味は{profile.hobby}です。" +
                          $"好きな食べ物は{profile.favoriteFood}です。" + 
                          $"好きな色は{profile.favoriteColor}です。" +
                          $"好きな動物は{profile.favoriteAnimal}です。" +
                          $"好きな季節は{profile.favoriteSeason}です。" +
                          $"好きなゲームは{profile.favoriteGame}です。" +
                          $"好きな映画は{profile.favoriteMovie}です。" +
                          $"好きな音楽は{profile.favoriteMusic}です。" +
                          $"好きな本は{profile.favoriteBook}です。" +
                          $"好きな場所は{profile.favoritePlace}です。"
            };
            AddMessage(model);
        }

        private void AddMessage(ChatGPTMessageModel model)
        {
            _messageList.Add(model);
        }
  • Scene内のManagerで[SerializeField]でScriptableObjectを受け取り、ChatGPTConnectionのコンストラクト後に渡すなどする

結果

  • 以下のように、自分のプロフィール情報を汲み取った回答をしてくれる
Flamers Tech Blog

Discussion