Live2DリアルアバターでChatGPTにて会話させてみた。


ChatGPTを使って自分のリアルアバターにしゃべらせました。
この動画はSNS用に編集してるので、こんなにレスポンスは早くないです。

実際は、１度回答をオーディオファイル化・セーブするといった処理をしているので、返答まで5秒から10秒くらいと結構遅いです。
もっといい方法はあると思いますが、今回はここまで。


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">今度は、TatsuyaGPTを作りました。 <a href="https://twitter.com/hashtag/ChatGPT?src=hash&amp;ref_src=twsrc%5Etfw">#ChatGPT</a> <a href="https://twitter.com/hashtag/Unity?src=hash&amp;ref_src=twsrc%5Etfw">#Unity</a> <a href="https://twitter.com/hashtag/Live2D?src=hash&amp;ref_src=twsrc%5Etfw">#Live2D</a> <br>※実際にはこんなにレスポンス早くないです。 <a href="https://t.co/yZg95RwnDM">pic.twitter.com/yZg95RwnDM</a></p>&mdash; たつや (@tatsuya1970) <a href="https://twitter.com/tatsuya1970/status/1645427531229822977?ref_src=twsrc%5Etfw">April 10, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


<br>
雑ですが、備忘録を兼ねてQiitaに残します。

## 環境・使用ソフト

- OS： Windows10
- Adobe Photoshop 2022
- Unity 2020.3.33f1
- [Live2D Cubism Editor](https://www.live2d.com/download/cubism/) 4.2



※OSやバージョンは動作確認済みのもので、これに限りません。


## アバター作成
自分の写真をフォトショップで開きます。

[![Image from Gyazo](https://i.gyazo.com/dafb2a97d7e0d2ebc284e2685a25cc9d.png)](https://gyazo.com/dafb2a97d7e0d2ebc284e2685a25cc9d)

レイヤーを複製して残しておきます。
[![Image from Gyazo](https://i.gyazo.com/870feb62ee78c7eb9191ca5f624a8050.png)](https://gyazo.com/870feb62ee78c7eb9191ca5f624a8050)

一番下のレイヤー（背景）をのっぺらぼうにします。

編集　＞　コンテンツに応じた塗りつぶし　
を使えばスムーズなのっぺらぼうができます。
怖い・・・。

[![Image from Gyazo](https://i.gyazo.com/83047328c0edac74b0c9fc485752f0f2.png)](https://gyazo.com/83047328c0edac74b0c9fc485752f0f2)


複製した元の写真から、まゆげ、目、鼻、口を切り取り、レイヤーにします。（アニメーションで動かすため）

[![Image from Gyazo](https://i.gyazo.com/7e6c7e900ad5f79b7541ffbcc201e3e8.png)](https://gyazo.com/7e6c7e900ad5f79b7541ffbcc201e3e8)

このまま福笑いアプリを作れそうですねw

<br>
PSDで保存します。


## Live2D Cubism Editor
 
Live2D Cubism Editor を立ち上げ、
さっき作ったPSD ファイルを開きます。


眉、目、口を選択し、
「メッシュの自動生成」（下の画像参考）

[![Image from Gyazo](https://i.gyazo.com/cd2bc28b43940e23ab7596529c5a70d0.png)](https://gyazo.com/cd2bc28b43940e23ab7596529c5a70d0)

「変化度合小」で
[![Image from Gyazo](https://i.gyazo.com/8c471049668bd45c1a9f7b3d681f571f.png)](https://gyazo.com/8c471049668bd45c1a9f7b3d681f571f)

あとは、Live2Dのチュートリアルを参考に、まばたきや口の開閉などの動きを作ってください。
https://docs.live2d.com/cubism-editor-tutorials/import/



## Unity

詳細はこちら
https://docs.live2d.com/cubism-sdk-tutorials/getting-started/

Unity の Projects > Assets　に
Live2Dで生成した組み込みータをフォルダごと入れます。
LIve2Dで生成したフォルダにあるプレファブをシーンにドラッグ＆ドロップします。

なぜか出てきません。

よく見ると、Inspectoar のMeshが反映されていません。

この[Live2Dのフォーラム](https://forum.live2d.com/discussion/1809/cubismsdkforunity-4-r-1%E3%81%AB%E3%81%8A%E3%81%84%E3%81%A6-unity%E4%B8%8A%E3%81%AEmesh%E3%81%8C%E7%A9%BA%E3%81%A7-%E3%83%A2%E3%83%87%E3%83%AB%E3%81%8C%E8%A1%A8%E7%A4%BA%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84)に「１度実行したら反映される」と書いてあります。


そのとおり1度実行（Run)すると、Unityに反映されました。

[![Image from Gyazo](https://i.gyazo.com/cf084be552e9a468f659fe8194dbff77.png)](https://gyazo.com/cf084be552e9a468f659fe8194dbff77)

リップシンクの設定をします。
やり方はここを見てください。
https://docs.live2d.com/cubism-sdk-tutorials/lipsync/


### UI

テキスト入力し、ボタンを押したら、後述するスクリプト answer.cs が動く（アバターがChatGPTの回答を話す）ようにします。

[![Image from Gyazo](https://i.gyazo.com/2320739705c96e4273d860c17c874f1f.png)](https://gyazo.com/2320739705c96e4273d860c17c874f1f)


### アバターのInspector


このとおり必要なコンポーネントを追加してください。

[![Image from Gyazo](https://i.gyazo.com/aae2f409e3069c0b720d07c5cee01629.png)](https://gyazo.com/aae2f409e3069c0b720d07c5cee01629)



### Audio ファイル

アバターが回答する用のオーディオファイルを作成します。

空のオブジェクトを作成し、
「Audio Source」をアタッチしてください。
オーディオファイルはアタッチしなくてもいいです。
（プログラムでオーディオファイルを生成するので）
[![Image from Gyazo](https://i.gyazo.com/684b45cdb2c912612784c83d11371753.png)](https://gyazo.com/684b45cdb2c912612784c83d11371753)


## VOICEVOX

アバターの声は、OSSのテキスト読み上げソフトウェアVOICEVOXを使います。

VOICEVOX をインストール
https://voicevox.hiroshiba.jp/

VOICEVOXのローカルサーバーを立てます。

ここを参考にしました。
https://qiita.com/yamanohappa/items/b75d069e3cb0708d8709

コマンドプロンプトで　VOICEVOX\vv-engine のフォルダへ移動
※C:\Users\Owner\AppData\Local\Programs\VOICEVOX\vv-engine

自分のPCのIPアドレスを確認
```
$ ipconfig
```

Voicevox　をコマンドプロンプトから起動 （ポートは50021）
```
$ run.exe --host XXX.XXX.X.XX --port 50021
```

サーバー起動
[![Image from Gyazo](https://i.gyazo.com/e300437649c0453db8387cea7687913c.png)](https://gyazo.com/e300437649c0453db8387cea7687913c)

サーバーが立ち上がってるか確認
http://XXX.XXX.X.XX:50021/docs

こういう画面が出たらOK
[![Image from Gyazo](https://i.gyazo.com/bbdedc7bf33e48549c5c4108ed188fbc.png)](https://gyazo.com/bbdedc7bf33e48549c5c4108ed188fbc)


## コード

まず、JSON Object アセットをインストールします。
https://assetstore.unity.com/packages/tools/input-management/json-object-710?locale=ja-JP

以下のGithubから非同期処理のライブラリ 「Unitask」 をインポートします。
https://github.com/Cysharp/UniTask/releases

以下のGithubから、録音・再生するプログラム　WavUtility.cs　をUnityのProjects >　Assetsへ　コピペすします。
https://github.com/deadlyfingers/UnityWav

AIの回答をVoiceBoxにあげてリップシンクで回答するコードはこちらのブログの
クラス VoiceVoxConnection をそのまま使いました。
https://note.com/negipoyoc/n/n081e25f5ee9e

メインのプログラムは以下です。

テキスト入力された言葉をOpenAIのAPIに投げて、その回答を録音し、アバターオブジェクトの Cubism Audio Mouth Input のAudio Inputにアタッチし、VoiceVoxConnectionクラスで読み上げています。

１度録音、アタッチしてるので処理は遅いです。

なお、OpenAIのAPIキーの取得方法はこちらをご参照ください。
https://qiita.com/tatsuya1970/items/c04cb7b6d07c4cf51007#3-openai




``` answer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Networking;
using System;
using Defective.JSON;
using TMPro;
using System.Threading.Tasks;
using System.IO;
using System.Text;

public class answer : MonoBehaviour
{
    public Button button;          // OKボタン
    public GameObject inputField;  //ユーザー入力するフィールド
    AudioSource audioSource;
    public GameObject audioObject;

    [SerializeField]
    TextMeshProUGUI responseText;
    VoiceVoxConnection _voiceVoxConnection;

    //OpenAIのAPIにリクエストするとき必要
    [Serializable]
    public class body
    {
        public string model;
        public Transfers[] messages;
    }

    [Serializable]
    public class Transfers
    {
        public string role;
        public string content;
    }

    public async void OnClick()
    {
        animator = GameObject.Find("man2").GetComponent<Animator>();

        //ユーザーが投稿した内容を取得
        string mes = inputField.GetComponent<Text>().text;

        var url = "https://api.openai.com/v1/chat/completions";

        // ボットの性格を設定
        string chara = "ボットの性格を入れる";

        string itemJson
            = "{ \"model\": \"gpt-3.5-turbo\"," +
            "\"messages\": [" +
            "{ \"role\": \"system\", \"content\": \"" + chara + "\"}," +
            "{ \"role\": \"user\", \"content\": \"" + mes + "\"}]}";

        body item = JsonUtility.FromJson<body>(itemJson);
        string serialisedItemJson = JsonUtility.ToJson(item);
        UnityWebRequest request = new UnityWebRequest(url, "POST");
        byte[] postData = System.Text.Encoding.UTF8.GetBytes(serialisedItemJson);
        request.uploadHandler = new UploadHandlerRaw(postData);
        request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "application/json");
        
        request.SetRequestHeader("Authorization", "Bearer OpenAIのAPIキー");

        var operation = request.SendWebRequest();

        while (!operation.isDone)
        {
            await Task.Delay(10);
        }

        if (request.result != UnityWebRequest.Result.Success)
        {
            Debug.Log(request.error);
        }
        else
        {
            Debug.Log(request.responseCode);

            if (request.responseCode == 200 || request.responseCode == 201)
            {
                string text = request.downloadHandler.text;
                Debug.Log(text);
                JSONObject json = new JSONObject(text);
                JSONObject choices = json.GetField("choices");
                JSONObject message = choices[0].GetField("message");
                string content = message.GetField("content").ToString();
                //Debug.Log(content);
                responseText.text = "考え中・・・";
                string content2 = content.Replace("\"", "");
                _voiceVoxConnection = new VoiceVoxConnection(13);　　//声はここの整数
                // 1-10:女性、11以降は男性 13:渋い男 14:女の子っぽい男の子？ 15:女性　
                
                //録音
                var clip = await _voiceVoxConnection.TranslateTextToAudioClip(content2);
                string filename = "Assets/Audio/sound.wav";
                SaveWav(filename, clip);
      
                // WAVファイルを読み込む
                byte[] fileData = File.ReadAllBytes(filename);
                // WAVファイルのデータ部分をバッファにコピー
                float[] samples = ConvertBytesToFloat(fileData);
                // AudioClipを生成           
                AudioClip clip2 = AudioClip.Create("audio", samples.Length, 1, 44100, false);
                responseText.text = "TatsuyaGPT";
                GameObject.Find("Audio").GetComponent<AudioSource>().clip = clip;
                clip2.SetData(samples, 0);
                audioSource = GameObject.Find("Audio").GetComponent<AudioSource>();
                audioSource.Play();
                await Task.Delay(3000);
            }
            else
            {
                responseText.text = request.responseCode + "エラーだよ！！\nどこかが間違ってるよ。";
                Debug.Log("エラー");
                await Task.Delay(3000);
            }
        }
    }

    //ここから下は全部ChatGPTに作ってもらった
    private void SaveWav(string filename, AudioClip clip)
    {
        // WAVファイルのデータ部分を作成
        byte[] data = CreateWavData(clip);

        // WAVファイルの先頭部分を作成
        byte[] header = CreateWavHeader(clip.frequency, clip.channels, data.Length / 2);

        // WAVファイルの先頭部分とデータ部分を合体
        byte[] fileData = new byte[header.Length + data.Length];
        header.CopyTo(fileData, 0);
        data.CopyTo(fileData, header.Length);

        // WAVファイルを保存
        File.WriteAllBytes(filename, fileData);
    }


    private byte[] CreateWavData(AudioClip clip)
    {
        // AudioClipのデータをバッファにコピー
        float[] samples = new float[clip.samples];
        clip.GetData(samples, 0);

        // ビット深度に応じてデータを変換
        byte[] data;

        // WAVファイルのヘッダーを作成
        byte[] header = CreateWavHeader(clip.channels, clip.frequency, samples.Length);

        // WAVデータを作成
        byte[] wavData = ConvertFloatTo16BitWav(samples);

        // ヘッダーとデータを結合
        data = new byte[header.Length + wavData.Length];
        header.CopyTo(data, 0);
        wavData.CopyTo(data, header.Length);

        return data;
    }


    private byte[] ConvertFloatTo16BitWav(float[] samples)
    {
        int sampleCount = samples.Length;
        byte[] data = new byte[sampleCount * 2];

        int i = 0;
        while (i < sampleCount)
        {
            short value = (short)(samples[i] * 32767f);
            data[i * 2] = (byte)value;
            data[i * 2 + 1] = (byte)(value >> 8);
            i++;
        }

        return data;
    }

    private byte[] CreateWavHeader(int sampleRate, int channelCount, int sampleCount)
    {
        // フォーマットID
        ushort formatId = 1;

        // ブロックサイズ (byte/sample * channel)
        ushort blockSize = (ushort)(2 * channelCount);

        // データレート (byte/second)
        int dataRate = sampleRate * blockSize;

        // サイズ
        int size = 36 + sampleCount * blockSize;

        // ヘッダーを作成
        byte[] header = new byte[44];
        Encoding.ASCII.GetBytes("RIFF").CopyTo(header, 0);
        BitConverter.GetBytes(size).CopyTo(header, 4);
        Encoding.ASCII.GetBytes("WAVE").CopyTo(header, 8);
        Encoding.ASCII.GetBytes("fmt ").CopyTo(header, 12);
        BitConverter.GetBytes(16).CopyTo(header, 16);
        BitConverter.GetBytes(formatId).CopyTo(header, 20);
        BitConverter.GetBytes(channelCount).CopyTo(header, 22);
        BitConverter.GetBytes(sampleRate).CopyTo(header, 24);
        BitConverter.GetBytes(dataRate).CopyTo(header, 28);
        BitConverter.GetBytes(blockSize).CopyTo(header, 32);
        BitConverter.GetBytes(16).CopyTo(header, 34);
        Encoding.ASCII.GetBytes("data").CopyTo(header, 36);
        BitConverter.GetBytes(sampleCount * blockSize).CopyTo(header, 40);

        return header;
    }

    private static float[] ConvertBytesToFloat(byte[] bytes)
    {
        float[] samples = new float[bytes.Length / 2];

        for (int i = 0; i < samples.Length; i++)
        {
            samples[i] = BitConverter.ToInt16(bytes, i * 2) / 32768.0f;
        }

        return samples;
    }

}

```

VOICEVOXのローカルサーバーを立ち上げて、UnityをRUNすれば、リアルアバターと会話できます。

<br>

## （参考）ChatGPTとの対話によるコーディング

余談になりますが、
このプログラムは以前作成した [CatGPT](https://qiita.com/tatsuya1970/items/628b3f7a3479c7b21698) のプログラムをもとに、ChatGPTと対話を重ねながら作りました。

こんな感じです。
まず、CatGPT はコルーティンを使ってたので、まずは、 async/await　形式に直してもらいました。
[![Image from Gyazo](https://i.gyazo.com/d54169006422dd3c6451d909d1e5f298.jpg)](https://gyazo.com/d54169006422dd3c6451d909d1e5f298)

エラーが出るたびに
対話を続けていきます。
AIも一発で完璧なコードは作れません。
[![Image from Gyazo](https://i.gyazo.com/2ac75d56a03146c80fcbbea96559a4b1.png)](https://gyazo.com/2ac75d56a03146c80fcbbea96559a4b1)

[![Image from Gyazo](https://i.gyazo.com/580f014a91f1087fbb9ff08ba2564971.png)](https://gyazo.com/580f014a91f1087fbb9ff08ba2564971)

[![Image from Gyazo](https://i.gyazo.com/83df275aaf27df694ee626e567db500e.png)](https://gyazo.com/83df275aaf27df694ee626e567db500e)

このようなやりとりを4回繰り返すと、async/await　形式　のプログラムに変換できました。

これから、さらに13回やりとりして、VOICEVOX で返答するコードが完成しました。

ChatGPTがなければできませんでした。

ですので、本コードはChatGPTにいわれたとおりに作ったので、細かいところの意味はよく分かってません。








## 参考サイト

https://docs.live2d.com/cubism-editor-tutorials/top/

https://befool.co.jp/blog/aizawa/esa_1597/

https://docs.live2d.com/cubism-editor-tutorials/psd/