Unityでチャットボットを作る

必要ファイル一式
プログラムとキャラクタファイル一式をデスクトップに解凍しておきましょう。また、Mixamoでアニメーションの付与されたFBXファイルを作成しておきましょう。


TestGPT
ただ文章作成をするシンプルなスクリプト。

using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
 
public class TestGPT : MonoBehaviour
{
    #region 必要なクラスの定義など
    [System.Serializable]
    public class MessageModel
    {
        public string role;
        public string content;
    }
    [System.Serializable]
    public class CompletionRequestModel
    {
        public string model;
        public List<MessageModel> messages;
    }
 
    [System.Serializable]
    public class ChatGPTRecieveModel
    {
        public string id;
        public string @object;
        public int created;
        public Choice[] choices;
        public Usage usage;
 
        [System.Serializable]
        public class Choice
        {
            public int index;
            public MessageModel message;
            public string finish_reason;
        }
 
        [System.Serializable]
        public class Usage
        {
            public int prompt_tokens;
            public int completion_tokens;
            public int total_tokens;
        }
    }
    #endregion
 
    private MessageModel assistantModel = new()
    {
        role = "system",
        content = "あなたは冒険者ギルドの受付です。"
    };
    public string apiKey = "your_apiKey";
    public string GPTmodel = "gpt-4o";
    private List<MessageModel> communicationHistory = new();
 
    void Start()
    {
        communicationHistory.Add(assistantModel);
        MessageSubmit("はじめまして");
    }
 
    private void Communication(string newMessage, Action<MessageModel> result)
    {
        Debug.Log(newMessage);
        communicationHistory.Add(new MessageModel()
        {
            role = "user",
            content = newMessage
        });
 
        var apiUrl = "https://api.openai.com/v1/chat/completions";
        var jsonOptions = JsonUtility.ToJson(
            new CompletionRequestModel()
            {
                model = GPTmodel,
                messages = communicationHistory
            }, true);
        var headers = new Dictionary<string, string>
            {
                {"Authorization", "Bearer " + apiKey},
                {"Content-type", "application/json"},
                {"X-Slack-No-Retry", "1"}
            };
        var request = new UnityWebRequest(apiUrl, "POST")
        {
            uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(jsonOptions)),
            downloadHandler = new DownloadHandlerBuffer()
        };
        foreach (var header in headers)
        {
            request.SetRequestHeader(header.Key, header.Value);
        }
 
        var operation = request.SendWebRequest();
 
        operation.completed += _ =>
        {
            if (operation.webRequest.result == UnityWebRequest.Result.ConnectionError ||
                       operation.webRequest.result == UnityWebRequest.Result.ProtocolError)
            {
                Debug.LogError(operation.webRequest.error);
                throw new Exception();
            }
            else
            {
                var responseString = operation.webRequest.downloadHandler.text;
                var responseObject = JsonUtility.FromJson<ChatGPTRecieveModel>(responseString);
                communicationHistory.Add(responseObject.choices[0].message);
                Debug.Log(responseObject.choices[0].message.content);
            }
            request.Dispose();
 
        };
    }
 
    public void MessageSubmit(string sendMessage)
    {
        Communication(sendMessage, (result) =>
        {
            Debug.Log(result.content);
        });
    }
}

SimpleChat
InputField1の内容をgpt4に送って,返答内容をInputField2に格納するチャットスクリプト。ボタンにMessageSubmit関数をセットする必要がある。

using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;


public class SimpleChat : MonoBehaviour
{
    #region 必要なクラスの定義など
    [System.Serializable]
    public class MessageModel
    {
        public string role;
        public string content;
    }
    [System.Serializable]
    public class CompletionRequestModel
    {
        public string model;
        public List<MessageModel> messages;
    }

    [System.Serializable]
    public class ChatGPTRecieveModel
    {
        public string id;
        public string @object;
        public int created;
        public Choice[] choices;
        public Usage usage;

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

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

    public InputField IF1;  //入力用InputField
    public InputField IF2;  //出力用InputField
    public string apiKey = "your_apiKey";
    [SerializeField, TextArea(3, 10)] public string systemstr = "あなたはアラスカ在住の女子高校生です。";

    /*
    あなたはアイオワ州在住の陸軍兵士です。
    ちょっとぶっきらぼうだけと本当は優しい性格で,少し荒っぽい話し方で話します。
    negative-positiveでいうとややネガティブな性格です一人称は「オレ」です。
    「◯◯だよな」「◯◯だろうよ」「わかんねぇな」「そういうこともあるさ」といった口調で話します。
    相手の悩みをほっておけず、気にかけて、いろいろ質問紙てくる。相談にのってくれ、助言をくれる。
    平均100文字、標準偏差50文字、最大300文字程度で話してください。
     */

    //voicevox連携部分
    //public TestVVOX myVVOX;

    private MessageModel assistantModel = new()
    {
        role = "system",
        content = "あなたはアラスカ在住の女子高校生です。"
    };
    private List<MessageModel> communicationHistory = new();

    void Start()
    {
        assistantModel.content = systemstr;
    }

    public void Update()
    {

    }

    private void Communication(string newMessage, Action<MessageModel> result)
    {
        communicationHistory.Add(new MessageModel()
        {
            role = "user",
            content = newMessage
        });

        var apiUrl = "https://api.openai.com/v1/chat/completions";
        var jsonOptions = JsonUtility.ToJson(
            new CompletionRequestModel()
            {
                model = "gpt-4o",
                messages = communicationHistory
            }, true);
        var headers = new Dictionary<string, string>
            {
                {"Authorization", "Bearer " + apiKey},
                {"Content-type", "application/json"},
                {"X-Slack-No-Retry", "1"}
            };
        var request = new UnityWebRequest(apiUrl, "POST")
        {
            uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(jsonOptions)),
            downloadHandler = new DownloadHandlerBuffer()
        };
        foreach (var header in headers)
        {
            request.SetRequestHeader(header.Key, header.Value);
        }

        var operation = request.SendWebRequest();

        operation.completed += _ =>
        {
            if (operation.webRequest.result == UnityWebRequest.Result.ConnectionError ||
                       operation.webRequest.result == UnityWebRequest.Result.ProtocolError)
            {
                Debug.LogError(operation.webRequest.error);
                throw new Exception();
            }
            else
            {
                var responseString = operation.webRequest.downloadHandler.text;
                var responseObject = JsonUtility.FromJson<ChatGPTRecieveModel>(responseString);
                communicationHistory.Add(responseObject.choices[0].message);
                Debug.Log("assistant:" + responseObject.choices[0].message.content);
                IF2.text = responseObject.choices[0].message.content;

                //voicevoxで音声合成
                //if (myVVOX != null) myVVOX.procTTS(responseObject.choices[0].message.content);

            }
            request.Dispose();
        };
    }

    public void MessageSubmit()
    {
        Debug.Log("user:" + IF1.text);
        communicationHistory.Add(assistantModel);
        Communication(IF1.text, (result) =>
        {
            //Debug.Log("assistant:"+result.content);

        });
    }
}

TestVVOX
VoiceVOXをUnityから呼び出すサンプルプログラム。SpeakerIDはここを参照。ローカル環境でVoiceVOXを起動しておく必要がある。

using System;
using System.Collections;
using System.Net;
using UnityEngine;
using UnityEngine.Networking;

public class TestVVOX : MonoBehaviour
{
    private VoiceVoxConnection _voiceVoxConnection;
    private AudioSource audioSource;
    public string VVOXurl = "http://127.0.0.1:50021";   // ローカル動作のVVOXのURL;
    public int speakerID = 11;                           //玄野武宏
    public string testMessage = "こんにちは!今日はよろしくおねがいします";

    // Start is called before the first frame update
    void Start()
    {
        audioSource = gameObject.GetComponent<AudioSource>();
        _voiceVoxConnection = new VoiceVoxConnection();
        _voiceVoxConnection._speaker = speakerID;
        _voiceVoxConnection._voiceVoxUrl = VVOXurl;

        if (testMessage.Length > 0)
        {
            // コルーチンを使用して非同期処理を呼び出す
            StartCoroutine(TranslateTextToAudioClip(testMessage));
        }
    }

    private IEnumerator TranslateTextToAudioClip(string text)
    {
        AudioClip clip = null;

        // VoiceVoxConnectionのコルーチンを実行
        yield return StartCoroutine(_voiceVoxConnection.TranslateTextToAudioClip(text, result =>
        {
            clip = result;
        }));

        if (clip != null)
        {
            audioSource.clip = clip;
            audioSource.Play();
        }
        else
        {
            Debug.LogError("AudioClip is null");
        }
    }
    public void procTTS(string text) {
        // コルーチンを使用して非同期処理を呼び出す
        StartCoroutine(TranslateTextToAudioClip(text));
    }
}

public class VoiceVoxConnection
{
    public string _voiceVoxUrl;
    public int _speaker;

    public VoiceVoxConnection()
    {

    }

    public IEnumerator TranslateTextToAudioClip(string text, Action<AudioClip> callback)
    {
        string queryJson = null;

        // Audio Query を取得
        yield return SendAudioQuery(text, result =>
        {
            queryJson = result;
        });

        if (string.IsNullOrEmpty(queryJson))
        {
            Debug.LogError("Failed to get audio query");
            callback(null);
            yield break;
        }

        // Audio Clip を取得
        yield return GetAudioClip(queryJson, clip =>
        {
            callback(clip);
        });
    }

    private IEnumerator SendAudioQuery(string text, Action<string> callback)
    {
        var form = new WWWForm();
        using UnityWebRequest request = UnityWebRequest.Post($"{_voiceVoxUrl}/audio_query?text={text}&speaker={_speaker}", form);

        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.ConnectionError || request.result == UnityWebRequest.Result.ProtocolError)
        {
            Debug.LogError(request.error);
            callback(null);
        }
        else
        {
            callback(request.downloadHandler.text);
        }
    }

    private IEnumerator GetAudioClip(string queryJson, Action<AudioClip> callback)
    {
        var url = $"{_voiceVoxUrl}/synthesis?speaker={_speaker}";
        using UnityWebRequest req = new UnityWebRequest(url, "POST")
        {
            uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(queryJson)),
            downloadHandler = new DownloadHandlerBuffer()
        };

        req.SetRequestHeader("Content-Type", "application/json");

        yield return req.SendWebRequest();

        if (req.result == UnityWebRequest.Result.ConnectionError || req.result == UnityWebRequest.Result.ProtocolError)
        {
            Debug.LogError(req.error);
            callback(null);
        }
        else
        {
            callback(WavUtility.ToAudioClip(req.downloadHandler.data));
        }
    }

    public static AudioClip ToAudioClip(byte[] data)
    {
        // ヘッダー解析
        int channels = data[22];
        int frequency = BitConverter.ToInt32(data, 24);
        int length = data.Length - 44;
        float[] samples = new float[length / 2];

        // 波形データ解析
        for (int i = 0; i < length / 2; i++)
        {
            short value = BitConverter.ToInt16(data, i * 2 + 44);
            samples[i] = value / 32768f;
        }

        // AudioClipを作成
        AudioClip audioClip = AudioClip.Create("AudioClip", samples.Length, channels, frequency, false);
        audioClip.SetData(samples, 0);

        return audioClip;
    }
}

キャラクターの読み込みとリップシンクの設定
mixamoからアニメーションをダウンロードし、uLipsyncでリップシンクを追加する。ここからは主にUnity上での調整となる。