ChatGPTを使って自分のリアルアバターにしゃべらせました。
この動画はSNS用に編集してるので、こんなにレスポンスは早くないです。
実際は、1度回答をオーディオファイル化・セーブするといった処理をしているので、返答まで5秒から10秒くらいと結構遅いです。
もっといい方法はあると思いますが、今回はここまで。
今度は、TatsuyaGPTを作りました。 #ChatGPT #Unity #Live2D
— たつや (@tatsuya1970) April 10, 2023
※実際にはこんなにレスポンス早くないです。 pic.twitter.com/yZg95RwnDM
雑ですが、備忘録を兼ねてQiitaに残します。
環境・使用ソフト
- OS: Windows10
- Adobe Photoshop 2022
- Unity 2020.3.33f1
- Live2D Cubism Editor 4.2
※OSやバージョンは動作確認済みのもので、これに限りません。
アバター作成
自分の写真をフォトショップで開きます。
一番下のレイヤー(背景)をのっぺらぼうにします。
編集 > コンテンツに応じた塗りつぶし
を使えばスムーズなのっぺらぼうができます。
怖い・・・。
複製した元の写真から、まゆげ、目、鼻、口を切り取り、レイヤーにします。(アニメーションで動かすため)
このまま福笑いアプリを作れそうですねw
PSDで保存します。
Live2D Cubism Editor
Live2D Cubism Editor を立ち上げ、
さっき作ったPSD ファイルを開きます。
眉、目、口を選択し、
「メッシュの自動生成」(下の画像参考)
あとは、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のフォーラムに「1度実行したら反映される」と書いてあります。
そのとおり1度実行(Run)すると、Unityに反映されました。
リップシンクの設定をします。
やり方はここを見てください。
https://docs.live2d.com/cubism-sdk-tutorials/lipsync/
UI
テキスト入力し、ボタンを押したら、後述するスクリプト answer.cs が動く(アバターがChatGPTの回答を話す)ようにします。
アバターのInspector
このとおり必要なコンポーネントを追加してください。
Audio ファイル
アバターが回答する用のオーディオファイルを作成します。
空のオブジェクトを作成し、
「Audio Source」をアタッチしてください。
オーディオファイルはアタッチしなくてもいいです。
(プログラムでオーディオファイルを生成するので)
VOICEVOX
アバターの声は、OSSのテキスト読み上げソフトウェアVOICEVOXを使います。
VOICEVOX をインストール
https://voicevox.hiroshiba.jp/
VOICEVOXのローカルサーバーを立てます。
ここを参考にしました。
https://qiita.com/yamanohappa/items/b75d069e3cb0708d8709
コマンドプロンプトでVOICEVOX のフォルダへ移動
自分のPCのIPアドレスを確認
$ ipconfig
Voicevox をコマンドプロンプトから起動 (ポートは50021)
$ run.exe --host XXX.XXX.X.XX --port 50021
サーバーが立ち上がってるか確認
http://XXX.XXX.X.XX:50021/docs
コード
まず、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クラスで読み上げています。
1度録音、アタッチしてるので処理は遅いです。
なお、OpenAIのAPIキーの取得方法はこちらをご参照ください。
https://qiita.com/tatsuya1970/items/c04cb7b6d07c4cf51007#3-openai
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すれば、リアルアバターと会話できます。
(参考)ChatGPTとの対話によるコーディング
余談になりますが、
このプログラムは以前作成した CatGPT のプログラムをもとに、ChatGPTと対話を重ねながら作りました。
こんな感じです。
まず、CatGPT はコルーティンを使ってたので、まずは、 async/await 形式に直してもらいました。
エラーが出るたびに
対話を続けていきます。
AIも一発で完璧なコードは作れません。
このようなやりとりを4回繰り返すと、async/await 形式 のプログラムに変換できました。
これから、さらに13回やりとりして、VOICEVOX で返答するコードが完成しました。
ChatGPTがなければできませんでした。
ですので、本コードはChatGPTにいわれたとおりに作ったので、細かいところの意味はよく分かってません。
参考サイト