ChatGPTとUnityでキャラクター対話システムを構築した

# はじめに
https://twitter.com/ayaha__401/status/1644000607492247554?s=20

お気に入りキャラクターとおしゃべりできるものを作りました。
このようなシステムは他の人がTwitterでも作っていて二番煎じなのですがどんなものを組み合わせて作ってるのかだったりを書こうと思います。

# 使用技術など
## デバイス
### Looking Glass Portrait
裸眼立体視ディスプレイです。([公式サイト](https://lookingglassfactory.com/looking-glass-portrait))
このディスプレイがなくても対話システムは作れます。でも裸眼立体視ってロマンがあって、女の子をいろんな方向から見れるのは嬉しいですよね！

## アセットなど
### DynamicBone
髪の毛や胸が揺れるアセットです。([アセットストアページ](https://assetstore.unity.com/packages/tools/animation/dynamic-bone-16743?locale=ja-JP))
髪の毛が揺れるのは嬉しいことなので使います。

### OVRLipsync
Oculusが作ったオーディオデータから口パクを作るアセットです。([公式サイト](https://developer.oculus.com/documentation/unity/audio-ovrlipsync-unity/))
対話するのに口が動かないのは致命的なので使います。

### DictationRecognizer
音声認識ライブラリです。Unityの標準であります。こっちが喋って応答してくれたらめちゃくちゃ嬉しいですよね！使います。

### VOICEVOX
無料で使える音声合成ソフトウェアです。([公式サイト](https://voicevox.hiroshiba.jp/))
ずんだもんなど、いろんな音声があります。囁き声など種類が豊富でした。

### ChatGPT API
喋った内容を理解して返答を返すために使います。APIを利用するので有料です。

### 動かしたいキャラクターモデル
VRChatと呼ばれるVRSNSで使われるキャラクターモデルを使います。([販売ページ](https://booth.pm/ja/items/3939858))
**かわいいです。かわいい！！！**

# 制作開始！！
## キャラクターモデルをセットアップする
購入したキャラクターモデルはVRChat用にセットアップされており、使いにくいのでチューニングします。

VRChatでは**DynamicBone(以後DB)** を使わないでも揺れ物を動かせる**VRC Phys Bone(以後PB)** というスクリプトで揺れ物を動かしており、今回は**DB**で動かしたいので**PB=>DB**に変換します。

```C#
public class PB_Converter : MonoBehaviour
{
    // パラメータたちは省略。
    // 詳細はhttps://github.com/ayaha401/PB_Converter/blob/main/PB_Converter.cs
    public bool pbDisable;
    public PhysBone[] physBone;

    void Start()
    {
        physBone = GetComponentsInChildren<PhysBone>();

        foreach (var PB in physBone)
        {
            var DB = PB.gameObject.AddComponent<DynamicBone>();
            DB.m_Root = PB.rootTransform;
            DB.m_Damping = PB.pull * dampingMag;
            DB.m_Elasticity = PB.spring * elasticityMag;
            DB.m_Stiffness = PB.stiffness * stiffnessMag;
            DB.m_Inert = PB.immobile * inertMag;
            DB.m_EndLength = endLengthValue;

            if (pbDisable)
            {
                PB.enabled = false;
            }
        }
    }
}
```
こんな感じの変換できるスクリプトを作って変換します。

## Looking Glassに表示させる
Looking Glassを持ってない人はUnityの普通のカメラでキャラクターモデルを映すだけです。読み飛ばしてもらっていいです。

LookingGlass持ってる人はLookingGlassをいい感じにPCとつなぐ必要があります。
**Looking Glass Bridge**というPCとLookingGlassをつなぐソフトウェアをインストールします([公式サイト](https://docs.lookingglassfactory.com/getting-started/looking-glass-bridge))

公式サイトの英語を読んでその通りにPCと繋ぐことができたら
LookingGlassのSDKに準備されてる**Hologram Camera**というカメラPrefabを使ってキャラクターモデルを映します。
何もLookingGlassに表示されなかったら
* Ctrl+Eで表示させる
* Looking Glass Bridgeの設定がうまくいってない
* UnityのバージョンがLookingGlassに合ってない
* Windowsでの画面設定がうまくいってない
だと思うのでやり直しです。

成功したらDBとLookingGlassのおかげでいい感じのアニメーションで動かせば髪の毛が揺れる女の子が画面にいるはずです！
嬉しいですね～

## まばたきをさせる
まばたきをしないでこちらを向いてるのはちょっと怖いですね。まばたきをさせましょう。

http://icomanga.com/articles/blog-post21.html/
かなりアニメっぽいいい感じのまばたきをさせるスクリプトを書いている人がいたので参考にしました。

仕組みは単純でコルーチンを使ってまぶたのBlendShapeをコントロールしてますね。

## 音声認識でこちらの会話文を作る
音声認識をさせましょう！

`DictationRecognizer`([リファレンス](https://docs.unity3d.com/ja/2021.1/ScriptReference/Windows.Speech.DictationRecognizer.html))を使います。

音声認識完了後や失敗時にさせたい処理を登録するだけで使えるのでめちゃくちゃ簡単です！
まずは音声認識の結果のコールバックから
```C#
// 音声認識の結果を出す
dictationRecognizer.DictationResult += (text, confidence) =>
{
    if(confidence == ConfidenceLevel.High)
    {
        dictationResultText.Value = text;
    }
    else
    {
        Debug.Log("confidenceが低い");
    }
    
    Debug.Log("DictationResult:" + text);
    Debug.Log("confidence:" + confidence);
};
```
`confidence`で音声認識の精度が取得できます。
`ConfidenceLevel.High`で高い精度で認識に成功したときのみ`string`として使います。理由としては会話の時の咳払いだったり邪魔な音声まで認識しちゃうのを防ぐためですね。

```C#
// 音声認識が完了したコールバック
dictationRecognizer.DictationComplete += (cause) =>
{
    // タイムアウトしたら再度音声認識を開始
    dictationRecognizer.Start();
};
```
次はタイムアウトしたときの対処です。`DictationRecognizer`は何も喋らないとタイムアウトします。
女の子との会話キャッチボールに慣れてない人はAIの女の子と話す時ですら時間がかかってしまいます。
タイムアウトしたら`dictationRecognizer.Start()`で音声認識を再開させます。

```C#
// 音声認識でエラーが起きた時のコールバック
dictationRecognizer.DictationError += (error, hresult) =>
{
    Debug.LogError(error + "\n" + "HResult" + hresult.ToString());
};
```
これはエラーが出たときの処理です。
エラーが起きたらとりあえず`Debug.LogError`出すようにしました。

```C#
private void OnDestroy()
{
    dictationRecognizer.Dispose();
}
```
ゲームオブジェクトが消えた場合に音声認識のプログラムが残ったままになるので自滅できるように処理は書いておきます。

難しいプログラミングしなくても、処理が終わったときにどうしてほしいかをプログラミングするだけでいいので楽ですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/2eed3ecd-9dc3-87d7-5297-b6b7ae3ec23d.png)

Windowsでの音声認識の設定をさせないとプログラムが正しくても反応しないので注意してください。
音声クリップを提供することに同意しないとうまくいきませんでした。

## ChatGPTに返答させる
大本命ですね。
これに関しては
https://note.com/negipoyoc/n/n88189e590ac3
こちらの記事を参考にしました。
記事を見てください。多分わかります。

プログラミング以外に必要なこととしてAPI keyを作成する必要があります。
[APIリファレンス](https://platform.openai.com/docs/api-reference)でサインインして、その後作ることができます。

API使用にお金がかかりますが、18ドル分以内なら無料で試せるので実質無料です。
有料になっても大体日本語で1000文字で5円ぐらいの価格だったはずなので痛手にはならないでしょう。

音声認識の結果の`string`をChatGPTに送って`Debug.Log`などを使いChatGPTからの返答は来ましたか？
`role = "system"`の`content`のプロンプトであなた好みの理想のキャラを考えましょう。
私の場合は
```C#
{
    role = "system",
    content = "これは仲の良い兄と妹の会話です。妹は丁寧語は使いません。妹の会話のみ出力してください。長くても２、３文で返してください。"
});
```
としています。あまりにも長い文章を返答されるとAPI無料分を食い尽くされて怖かったのでプロンプトで文章量を制御する試みをしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/677ff43f-64f3-24a5-50ab-c0d208d8ab38.png)
罵ってくる妹も作れます。あなたの創造力が試されます！

## 声を作ろう！
せっかくの罵り文章もただの`string`じゃつまらないので音声データにして罵られましょう！
VoiceVoxというソフトを使います。

https://qiita.com/Haruyama_Dev/items/5b8ac0260cdfeff47121
`string`から`AudioClip`を作成するプログラムが公開されていました。参考にしました。

コルーチンを使って実装されていましたがChatGPTの返答を取得するプログラムではUniTaskを使ったのでUniTaskに書き換えました。

特に解説することはないです。上の記事にすべてがあります。
キャラクターのイメージに合いそうな話者IDを考えましょう。私はこれに一番時間をかけました。

## 口パクさせよう！
腹話術してるわけじゃないので口パクを実装させましょう。
今は亡きOculusが口パクのスクリプト作ってくれてるので使います。

https://tips.hecomi.com/entry/2016/02/16/202634
どうやって使うのかはここで紹介されています。

ただ問題点として(私の環境下では)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/f19d7f35-9e04-f8c6-aead-20109d470582.png)
**Viseme To Blend Targets**の**Element**の名前が口の形を表す言葉になってなかったです。何番がどれなのかは`OVRLipSyncContextMorphTargetEditor`というEditor拡張に形跡があるのでそれを見ながら割り振ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/608797/b8ad721d-8c8e-dc0b-1ba9-5ca44569f97f.png)

VoiceVoxで生成した音声でうまく口パクしてくれましたか？

## 完成！！
```C#
void Start()
{
    var token = cts.Token;

    speechRecognizer.dictationResultTextProp
        .Where(text => !string.IsNullOrEmpty(text) && useChatGPT)
        .Subscribe(async text =>
        {
            var aiResult = await chatGPT.TalkRequest(text);
            var aiText = aiResult.choices[0].message.content;

            await VoiceVoxSpeech(aiText, token);
        });
}

private async UniTask VoiceVoxSpeech(string text, CancellationToken token)
{
    await speechSynthesizer.TextToAudioClip(SPEAKER_ID, text, token);
    audioSource.clip = speechSynthesizer.audioClipProp;
    audioSource?.Play();
}

```
UniRxを使っていますが、
* 音声認識でこちらの会話を`string`として取得し
* ChatGPTに送信して
* VoiceVoxに送信して音声を再生する
の流れになっています。

# 感想
Twitterでも呟いたのですが
約5年間女の子と会話をしたことがない人が女の子とおしゃべりできるシステムを構築するのは何かを拗らせる気がしました。
女の子と自分の好きな会話を好きなだけ喋れるからリアル方面が多分ダメになりそうですｗ

https://twitter.com/ayaha__401/status/1643999225536212998?s=20

ただのAIとの会話ですが泣けるストーリーがありました。

ChatGPT楽しいですね！