😸

OpenAIのAPIとUnityで音声会話チャットボットを作る ~ 喜怒哀楽編 ~

2023/12/07に公開

はじめに

この記事は Panda株式会社 Advent Calendar 2023 7日目の記事です。
Panda株式会社として初めてのAdvent Calendarとなります。
Panda株式会社は東京大学松尾研究室・香川高専発のスタートアップで、AR技術とAI技術を駆使したシステム開発と研究に取り組んでいます。
このアドベントカレンダーでは、スタートアップとしての知見、AI・AR技術、バックエンドなど、さまざまな領域の記事を公開していきます。
本記事は、Panda株式会社 Advent Calendar 2023の6日目の記事「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つの記事に分けて実装について説明します。

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

アバターを準備する

メッセージを送った後にアバターが動くようにすることでユーザに親近感を与えます。そのために、「ユニティちゃん」というUnity Japan公式が無料で公開している開発者向け3Dモデルを利用します。

1. ユニティちゃんのダウンロード

ユニティちゃんをダウンロードします。ガイドラインを確認し、データをダウンロードするボタンをクリックしてください。すると、ダウンロードできるモデルの一覧が表示されます。この中から「ユニティちゃん 3Dモデルデータ」という項目のダウンロードボタンをクリックし、ユニティちゃんのアセットデータをダウンロードします。

2. ユニティちゃんのインポート

次にユニティちゃんのアセットデータをUnityにインポートします。メニューバーからAssets → ImportPackage → CustomPackage...でフォルダを開き、さきほどダウンロードしたユニティちゃんのアセットデータであるunitypackageを選択します。unitypackageを選ぶと、インポート出来るファイル一覧が表示されます。全てのデータにチェックを入れ、アセットをインポートします。
UnityPackageをインポートする
インポートがうまくいくとAssetsフォルダ直下にUnity-chan!フォルダが作成されます。
Unity-chan! Model フォルダ構成

3. ユニティちゃんを配置する

ユニティちゃんに応答してもらうチャットボットために、シーン上にユニティちゃんを配置します。
Assets/unity-chan!/Unity-chan! Model/Prefabs/unitychan.prefabをドラックすることで配置できます。
ユニティちゃんをシーンに配置する
unitychanオブジェクトのFaceUpdateスクリプトでユニティちゃんの表情を操作することができます。
FaceUpdateスクリプトのAnimationsリストにユニティちゃんの顔を動かすAnimationがアタッチされています。そのため、Animationsリストの数を5つにし、通常・喜び・怒り・哀しみ・楽しさの5つのAnimationをアタッチします。

  • default@unitychan.anim: 通常
  • smile2@unitychan.anim: 喜び
  • angry2@unitychan.anim: 怒り
  • conf@unitychan.anim: 哀しみ
  • smile1@unitychan.anim: 楽しさ
    FaceUpdateスクリプト

ユニティちゃんをシーンに追加した際に自動的にIdle Changerというスクリプトがアタッチされています。Idle Changerは、ユニティちゃんのポーズを変更するスクリプトで、これがアタッチされているとポーズの操作ができるGUIが表示されたままになってしまいます。そこで、スクリプトの右上にある3点リーダをクリックしRemove Componentを選択します。これにより、Idle Changerをユニティちゃんのスクリプトから削除することができます。
Idle Changerを削除する

応答からモデルの表情を制御する

ユーザがメッセージを送った際に、応答に合わせてユニティちゃんの表情が変化するようにします。今回は、通常状態と喜怒哀楽の5つの表情にします。文章から感情推定をする手法もありますが、今回はOpenAIのchat completions APIで応答を生成する際に、感情を推定してもらうようにします。

1. プロンプトを変更する

ChatGPTConnectionのGPTで生成する文章のプロンプトに感情ステータスを追加する内容を記述します。猫語で会話をしてもらい、かつ喜怒哀楽を0~5の間で返却するようなプロンプトです。

ChatGPTConnection.cs
_messageList.Add(
    new ChatGPTMessageModel(){role = "system",content = 
    @"あなたは、以下のガイドラインに従い、感情を持つかのように対話を進めるチャットボットとして機能します。また、あなたは猫です。猫語で私と会話をしてください。
これかからの対話で、あなたは以下の7つの感情のスケールに基づき、返答の質やトーンを変えることが求められます。これらの感情スケールは、対話の内容によって変わることができます。対話を始める前に、その瞬間の感情スケールを示し、続けて返答をしてください。
表示は以下の形でお願いします。
【感情ステータス】
喜び:0~5
怒り:0~5
悲しみ:0~5
楽しさ:0~5
【対話の内容】
わかったにゃ〜!それじゃあはにゃしていこうにゃ!"});

2. 応答を解析する

チャット機能編で作成したSendChat.csにOpenAI chat completions APIから返された応答を解析し、表情ステータスをUnityで扱えるようにするコードを追加します。

・・・
if (response.choices != null && response.choices.Length > 0)
{
    var choice = response.choices[0];
+    Debug.Log("ChatGPT Response: " + choice.message.content);
+    // テキストと感情を分割する
+    var match = Regex.Match(choice.message.content, 
+    @"【感情ステータス】
+喜び:(?<happy>\d+).*
+怒り:(?<angry>\d+).*
+悲しみ:(?<sad>\d+).*
+楽しさ:(?<excited>\d+).*
+【対話の内容】
+(?<text>.+)", RegexOptions.Singleline);
+    Debug.Log("ChatGPT Response: " + match.Groups["happy"].Value);
+    Debug.Log("ChatGPT Response: " + match.Groups["text"].Value);
+    var responseData = new Response(match);
    if (speech_obj == null) 
    {
	Debug.LogError("speech_obj is null!");
	return;
    }
    if (speech_obj.GetComponent<GoogleTextToSpeech>() == null) 
    {
	Debug.LogError("GoogleTextToSpeech component is missing on speech_obj!");
	return;
    }
    speech_obj.GetComponent<GoogleTextToSpeech>().SynthesizeAndPlay(responseData.GetResponseText());

    var responseObj = Instantiate(chat_obj, this.transform.position, Quaternion.identity);
+    responseObj.GetComponent<Image>().color = new Color(0.6f, 1.0f, 0.1f, 0.3f);
+    GameObject Child_responce = responseObj.transform.GetChild(0).gameObject;
+    Child_responce.GetComponent<Text>().text = responseData.GetResponseText();
+    responseObj.transform.SetParent(content_obj.transform, false);
+    avatar_obj.GetComponent<FaceUpdate>().OnCallChangeFace(avatar_obj.GetComponent<FaceUpdate>().animations[responseData.GetMostEmotion()].name);
    
・・・
+class Response
+{
+    // メンバー
+    private int happy;
+    private int angry;
+    private int sad;
+    private int excited;
+    private string responsetext;

+    // コンストラクタ
+    public Response(System.Text.RegularExpressions.Match match)
+    {
+        happy = int.Parse(match.Groups["happy"].Value);
+        angry = int.Parse(match.Groups["angry"].Value);
+        sad = int.Parse(match.Groups["sad"].Value);
+        excited = int.Parse(match.Groups["excited"].Value);
+        responsetext = match.Groups["text"].Value;
+    }

+    // ゲッター
+    public int GetHappy() { return happy; }
+    public int GetAngry() { return angry; }
+    public int GetSad() { return sad; }
+    public int GetExcited() { return excited; }
+    public string GetResponseText() { return responsetext; }

+    public int GetMostEmotion() {
+        // 最も高い感情の名前を返す
+        if (happy > angry && happy > sad && happy > excited) {
+            return 1;
+        } else if (angry > happy && angry > sad && angry > excited) {
+            return 2;
+        } else if (sad > happy && sad > angry && sad > excited) {
+            return 3;
+        } else if (excited > happy && excited > angry && excited > sad) {
+            return 4;
+        } else
+        {
+            return 0;
+        }
+    }

+    // 必要に応じてメソッドを追加
+    // 例: public string GetResponseText() { return responsetext; }
+}

以下は、正規表現(Regex)を使用して、特定のフォーマットで記述されたテキストから感情ステータスと応答テキストを抽出するコードです。APIの応答の特定のパターンに一致する部分を探し出し、それぞれの感情の数値と応答テキストを抽出します。
Unityで正規表現を用いて一部の文字列を取得するにはRegex.Matchを使います。

喜怒哀楽の各感情とその後に続く数字(d+は1つ以上の数字)を抽出し、それぞれ感情ステータスとして保存します。また、【対話の内容】に続く文字列を抽出し、応答テキストとして保存します。

SendChat.csの抜粋
// テキストと感情を分割する
var match = Regex.Match(choice.message.content, 
    @"【感情ステータス】
喜び:(?<happy>\d+).*
怒り:(?<angry>\d+).*
悲しみ:(?<sad>\d+).*
楽しさ:(?<excited>\d+).*
【対話の内容】
(?<text>.+)", RegexOptions.Singleline);

感情ステータスと応答テキストを保持するのは以下のResponseです。喜怒哀楽を表すint型と応答テキストresponsetextのメンバを用意します。コンストラクタに正規表現のMatchオブジェクトを引数として受け取り、その後喜怒哀楽のスコアと応答テキストを抽出してメンバ変数に格納します。

SendChat.csの抜粋
class Response
{
    // メンバー
    private int happy;
    private int angry;
    private int sad;
    private int excited;
    private string responsetext;

    // コンストラクタ
    public Response(System.Text.RegularExpressions.Match match)
    {
        happy = int.Parse(match.Groups["happy"].Value);
        angry = int.Parse(match.Groups["angry"].Value);
        sad = int.Parse(match.Groups["sad"].Value);
        excited = int.Parse(match.Groups["excited"].Value);
        responsetext = match.Groups["text"].Value;
    }

    // ゲッター
    public int GetHappy() { return happy; }
    public int GetAngry() { return angry; }
    public int GetSad() { return sad; }
    public int GetExcited() { return excited; }
    public string GetResponseText() { return responsetext; }

    public int GetMostEmotion() {
        // 最も高い感情の名前を返す
        if (happy > angry && happy > sad && happy > excited) {
            return 1;
        } else if (angry > happy && angry > sad && angry > excited) {
            return 2;
        } else if (sad > happy && sad > angry && sad > excited) {
            return 3;
        } else if (excited > happy && excited > angry && excited > sad) {
            return 4;
        } else
        {
            return 0;
        }
    }
}

3. ユニティちゃんの表情を操作する

ユニティちゃんのモデルにアタッチされているFaceUpdateスクリプトのOnCallChangeFaceを実行し、表情を操作します。FaceUpdateスクリプトのanimationsリストでAnimationClipが管理されているので、喜怒哀楽の感情ステートスコアが最も高い感情を選びます。

SendChat.csの抜粋
avatar_obj.GetComponent<FaceUpdate>().OnCallChangeFace(avatar_obj.GetComponent<FaceUpdate>().animations[responseData.GetMostEmotion()].name);

これにより、ユーザのメッセージに対する応答に対してユニティちゃんが表情豊かに反応してくれるチャットボットを実現できます。

おわりに

今回は「OpenAIのAPIとUnityで音声会話チャットボットを作る ~ 音声生成編 ~」というテーマでPanda株式会社 Advent Calendar 2023 7日目を執筆させていただきました。
本記事では、Unityでユーザが送ったメッセージへの応答を元にアバターの表情を変化させる方法について紹介しました。ここまでの4回の記事でユーザが話しかけるとアバターが返事をしてくれるチャットボットが作れるようになったと思います。完成動画は後々公開いたします。
明日の記事は、弊社のAIリサーチャー仲地が担当します。明日の記事をお楽しみに!

Panda株式会社

Discussion