最近のAI業界の発展スピードには目を見張るものがありますね。ChatGPTはその名の通り、チャット形式でAIと対話できるのが特徴ですが、APIが発表されるとすぐに、音声認識を使ってAIと声でおしゃべりができるシステムが多数登場しました。たとえば現実世界に3Dキャラクターを召喚できるGateboxは、ChatGPTと連携したシステムのクラウドファンディングを始めました。
ところで私はNPCとか、キャラクターが自律的に動くシステムに興味があります。ChatGPTを使ってAIキャラクター同士が会話したら面白いんじゃないか!? と思い作ってみることにした。
ご注意
ある程度Unityでの制作経験がある方向けに書いています。よくわからない、エラーがでる、といった問題はご自身で解決をお願いします。そんなときこそChatGPTの出番です!
今回使用するシステム
- VOICEVOX(音声合成エンジン。無料)
- ChatGPT (対話型AI。APIは有料)
- Unity 2021.3
今回はUnity上で、ChatGPTのAPIを使って会話文を作成し、音声合成エンジンのVOICEVOXで音声データを作成します。3DモデルはVRM形式のアバターを使用して、発音に合わせて口を連動するようにします。
仕組み
AI同士が会話するシステムは、全体の流れを制御するシステムコントローラーと、アバター(2人)で構成されています。
システムコントローラーがChatGPT APIにリクエストを送信して会話データを取得し、会話文を喋らせるアバターに送ります。アバターはVOICEVOX APIにリクエストを送信して、音声のクエリ(1語単位まで言葉を分解した発音データ)と、それを音声化したデータを取得します。アバターはその音声を再生し、音声のタイミングに合わせて口を動かします。ここまでが1人が喋るまでの流れです。
次はその会話に対する返事の作成です。ChatGPTにはセッションの概念がないので、前回に何の話をしていたのか覚えていません。毎回新しい会話が一度だけ行われるだけで終了します。それではどうやって過去のやりとりをつないで話しているかというと、過去の会話の履歴をその都度送信しているのです。
user: ずんだもん「ラスクちゃんこんにちは?」 ①
assistant: こんにちにゃ!ずんだもんは元気にゃ? ②
user: ずんだもん「元気なのだ。ラスクちゃんは?」 ③
userというのがChatGPTに話しかけている側(ラスクちゃん)で、assistantというのがChatGPT側(ずんだもん)です。これをChatGPT側から見ると、自分(ChatGPT)はずんだもんと会話をしていて、ずんだもんから元気なのか聞かれてる、という状況を把握します。ですのでChatGPTはずんだもんに返事をします。
次はずんだもんが返事をする番です。今度はuserとassistantが入れ替わります。userがラスクちゃんで、assistantがずんだもんです。
assistant: ラスクちゃんこんにちは? ①
user: ラスクちゃん「こんにちにゃ!ずんだもんは元気にゃ?」 ②
assistant: 元気なのだ。ラスクちゃんは? ③
user: ラスクちゃん「オイラはいつも元気にゃ。今なにしてるのにゃ?」 ④
このように、ChatGPTが自分は誰なのか、誰に返事をするのか、という立場を明確にすることで、AI同士の会話を成立させています。
ChatGPTの料金
残念ながらChatGPTのAPIは有料です。従量課金なので、使えば使うほど料金がかかります。こちらに料金表がありますが、2023年4月現在でgpt-3.5-turboが1000トークンあたり0.002ドルになります。日本語だと1文字が1トークン以上になったりするので注意が必要です。Tokenizerで文章を入力すると、トークンの数がわかります。
リクエストを送信するたびに過去の会話履歴も送信するため、トークン数はかなり多くなります。しかも今回作成するものはAI同士が無限に会話を続けるものなので、過去の会話履歴がどんどん膨れ上がってきます。課金額が大きくなることを覚悟しておきましょう。でもAI同士の創作会話に、過去の会話履歴全てを送信する必要はないですね。そこで今回は直近の5件分だけを送信するようにしました。これだけあれば会話をつなげていくには十分です。
ChatGPTではAPIの課金額の上限を設定できるので、万が一プログラムが暴走した場合に備えて、課金額の上限を設定しておいてください。
作ってみよう!
それでは実際に作っていきたいと思います。その前に、VOICEVOXのインストールと、ChatGPTのAPI KEYを取得しておきます。
VOICEVOXのインストールと起動
VOICEVOXをインストールしたら起動します。この画面は使用しないので、最小化してしまって大丈夫です。
そうしたらブラウザを開いて以下のURLにアクセスします。
http://localhost:50021/docs#/
実はVOICEVOXを起動するとWebサーバーも同時に起動します。これはAPIの使い方を説明したドキュメントページです。UnityからはこのAPIに対してアクセスしますので、同じPC上で実行します。異なるPCで実行する場合はIPアドレスを指定すればいいらしいです。(未確認)
ChatGPTのAPI KEYの取得
手順はインターネットで検索してみてください。
API KEYを取得したらメモ帳などで保存しておきます。API KEYが表示されるのは最初の一回だけなので、ここでコピーし忘れるともうわかりません。もし忘れた場合は、削除してもう一度取得しなおせばOKです。
Unityで新規プロジェクトを作成
Unity 2021.3で新規プロジェクトを作成します。この後アバターを配置していきますので、Planeを追加しておいたり、カメラの位置を変更しておくといいと思います。
UniVRMのインポート
UniVRMはVRM形式の3DモデルをUnityで扱うライブラリです。こちらからVRM 0.x用の.unitypackageをダウンロードします。ダウンロードしたらUnityにインポートします。
例: UniVRM-0.109.0_7aff.unitypackage
※VRM 1.0 の方は非対応です。VRM 0.x用を選択してください。
JSONライブラリのインポート
Package Managerを開き、左上の+を押してAdd package from git URL…を選択して com.unity.nuget.newtonsoft-json と入力してAddを押します。
Unityにも標準でJSONが使えるのですが、標準のものは機能に制限があるので、Newtonsoft.Jsonを使用します。
サンプルプログラムのインポート
今回私が作成したプログラムをこちらの[Download]からダウンロードしてインポートします。
インポートが完了すると、Assetの中にAIChatVRMというフォルダが作成されます。プログラムのファイルはこの中の Scripts フォルダに入ってますので、オブジェクトにアタッチする際はここから行います。
ChatGPTのAPI KEYを設定する
AIChatVRM/config/ の中に chatgpt_apikey.txt という空のファイルがあります。このファイルを開いて、先ほど取得したChatGPTのAPI KEYをコピー&ペーストしてください。API KEYを他人に知られると、他人が使った分の料金が自分に請求されてしまいますので、くれぐれもご注意を。
アバターの3Dモデルの用意とインポート
今回使用するのはVRM形式の3Dモデルです。好きなアバターを使いましょう。今回は例として、2人ともずんだもんで試します。東北ずん子ショップさんのBOOTHからずんだもんをダウンロードします。
ファイルを展開すると Zundamon(Human)_VRM_09.vrm という拡張子が.vrmのファイルがあります。AIChatVRMのVRMフォルダの中に「ずんだもん」という新規フォルダを作成して、ずんだもんフォルダの中にVRMファイルをドラッグ&ドロップします。しばらくすると複数のフォルダが作成されます。
水色のアイコンがずんだもんのPrefabです。Sceneに配置しましょう。ほかのアバターを追加したい場合は同様に繰り返します。今回は例として2人ともずんだもんにしました。
わかりやすいように、1人目を-0、2人目を-1に名前を変えました。(プログラムでは0が1番目なので、0が1人目、1が2人目という風にします)もちろん、自分がわかりやすい方法でやって問題ないです。
アバターにプログラムを追加
アバターには喋る機能と、口を動かす機能があります。はじめに、アバターを選択してAdd Componentで、Audio Sourceを追加します。
次に AIChatVRM/Scripts/ の中の BlendshapeController.cs と SpeakController.cs をアバターにアタッチします。ドラッグ&ドロップするだけでOKです。
こんな感じに表示されればOKです。1人目が終わったら、もう一人も同じように行います。
システムコントローラーの作成
システムコントローラーは、会話の進行を制御するメインとなる部分です。ヒエラルキーウィンドウ内に空のGameObjectを作成して、SystemControllerとでも名前を付けておきます。
SystemControllerのGameObjectを作成したら、 AIChatVRM/Scripts/ の中の SystemController.cs と ChatGPTRemote.cs をドラッグ&ドロップでアタッチします。
こんな感じになってればOKです。中の値は設定例なので、好きなように変えてください。
アバターの関連付けを行う
システムコントローラーとアバターの関連付けを行います。ヒエラルキーウィンドウのSystemControllerを選択して、System Controller (Script)のAvater Objectsを広げます。Element 0と1があるので、ヒエラルキーウィンドウ内のずんだもんを、それぞれドラッグ&ドロップします。
声の設定、名前の設定、初期メッセージの設定
続いて声を設定していきます。Avater Speaker IDsのElement 0と1に、それぞれ対応する声のSpeaker IDを設定します。Speaker ID、ずんだもん(あまあま)なら1、四国めたん(ノーマル)なら2、のようになっています。こちらのページでサンプルメッセージが再生できます。
このSpeakerのIDを調べる方法なんですが、一覧ページなどが見当たらなかったので、VOICEVOXのAPIにアクセスして調べることにします。ブラウザに以下のURLにアクセスします。
http://localhost:50021/speakers
生のJSONデータが出てきましたね。このままだと見にくいので、オンラインのJSON Viewerとかを使うと便利です。
次はCharacter Namesに、それぞれのキャラクターの名前を設定します。今回は2人ともずんだもんだとAIが混乱してしまうので、区別するためにちょっと名前を変えてみました。
最後に、First Messageに最初の会話を入れます。何もない状態からだとAIが何を話していいかわからなくなってしまうので、2人目のキャラクター(Zundamon-1)が、1人目のキャラクター(Zundamon-0)に話しかけるシチュエーションの最初の会話を入れてあげます。ですので、次の会話は1人目のキャラクター(Zundamon-0)から始まります。
プレイボタンを押してみよう
ここまでできたらCtrl+Sでシーンを保存して、さっそくプレイボタンを押して再生してみましょう。停止ボタンを押すまで、間違いがなければ、永遠とずんだもんが会話をしてくれるはずです。(課金も永遠と…)
うまく動かない場合は、Consoleにエラーが出てないか確認してみてください。
キャラ設定をカスタマイズしよう
キャラの喋り方や好きなものなどのキャラ設定は、個別に設定できるようになっています。AIChatVRM/config/ を開くと、charactor-*.txt というファイルがあります。0が1人目のキャラクター(Zundamon-0)の設定、1が2人目のキャラクター(Zundamon-1)の設定です。
サンプルファイルではこのように設定しています。
あなたの名前は「ずんだもん1号」です。12歳くらいの元気な男の子です。子供なので敬語は使いません。語尾に「なのだ」「のだ」をつけて話してください。一人称はボクです。好きな食べ物はずんだ餅。ずんだ餅の精霊です。「ずんだアロー」に変身することができる。将来の夢は、ずんだ餅のさらなる普及。ずんだ餅の素晴らしさをアピールしている。趣味はスマホゲーム。Youtubeで動画を見たりアニメを見たりするのが好き。
AIChatVRM/config/character0.txt
普通に文章でキャラの説明を書いているだけです。ほんと、GPTって賢いですね!これを自分の好きなキャラクターの設定に書き換えれば、AIが認識して演じてくれるはずです。
これとは別に system.txt というファイルもあります。これはChatGPTにどんな応答を行ってほしいかを書いた文章です。
あなたは部屋の中にいます。部屋にはあなたを含めて2人います。相手からの会話は”名前「会話」”という形式で来ます。話し言葉で返事をしてください。返事の中に感情パラメーター{xxx}を入れてください。xxx入る値はneutral=通常, joy=嬉しい, sorrow=困惑。使用例「これ何?{sorrow}プレゼントくれるの?{joy}ありがとう!」のように、その感情に関係する言葉の”前に”感情パラメーターを入れてください。返事は50文字程度にまとめてください。
AIChatVRM/config/system.txt
ただ、感情表現についてはまだ不完全で、期待通りにならないことが多いです。一応{joy}は付くのですが、この説明をChatGPTは理解できていない感じです。どう伝えたらAIが理解してくれるのか、まだまだと調整が必要そうです。
お好みでモーション設定を追加
再生ボタンを押すとTポーズのまま微動だにしないと思います。これはアニメーターが設定されていないからです。手っ取り早く試すには、Standard AssetのThirdPersonAnimatorControllerを、アバターのAnimatorのControllerに設定してあげれば、ゆらゆらと揺れるIdleモーションになります。ただ動きが大きいので違うIdleモーションに変えた方がいいかもです。
もうちょっと詳しい話
せっかくなので、工夫したところなど、もうちょっと掘り下げて書いていきたいと思います。
プログラム
システムコントローラー用
・SystemController.cs (会話を制御するメインプログラム)
・ChatGPTRemote.cs (ChatGPT APIにアクセスして結果を取得する)
・InputTopics.cs (外部から新しい話題を提供する)
・Common.cs (共通データ)
アバター用
・SpeakController.cs (アバターを喋らせる)
・BlendshapeController.cs (VRMモデルのブレンドシェイプを制御する)
・VoicevoxLocal.cs (VOICEVOXで音声合成用する)
会話履歴をChatGPT APIに送信する処理
会話履歴はSystemController.csの TalkHistoryList というリスト型の変数に格納しています。これには喋った人、内容、次に喋る人、という3つの情報が格納されています。今は2人なので次に喋る人というのは必ずもう片方の人になりますが、将来的に3人以上で会話した場合に備えてです。
// 会話履歴のリスト
public class TalkHistoryList {
public int avatarFrom; // 喋った人(0または1。-1の場合はsystem扱い)
public string text = null; // 喋った内容
public int avatarNext; // 次に喋ってほしい人(0または1。-1の場合は1つ前と同じにする)
}
会話履歴に会話を追加する場合は addTalkHistory() メソッドを実行します。会話履歴は5回分までしかChatGPTに送信しないので、5件以上になったら古い履歴は自動的に削除されるようになっています。5件という数字は maxHistory の値を変更することで変えられます。
次はこの会話履歴をChatGPTに送信するのですが、キャラ設定などの指示もしなくてはなりません。はじめに、どのようにChatGPTに応答してほしいかを書いた config/system.txt の内容と、キャラ設定を書いた config/character-*.txt の内容を system として作成します。
// 会話履歴を元にChatGPTに送信する会話を作成する
private void createChatGPTMessages() {
if (history.Count >= 0) {
int nextSpeakAvater = history[history.Count - 1].avatarNext;
gpt.initMessageList();
gpt.addMessageList("system", defaultSetting + characterSettings[nextSpeakAvater]); // キャラ設定を最初に定義する
foreach (TalkHistoryList hist in history) {
if (hist.avatarFrom == -1) {
gpt.addMessageList("system", hist.text);
}
else if (hist.avatarFrom == nextSpeakAvater) {
gpt.addMessageList("assistant", hist.text);
}
else {
string otherText = $"{characterNames[hist.avatarFrom]}「{hist.text}」";
gpt.addMessageList("user", otherText);
}
}
}
}
system というのは、当事者(ChatGPTと会話相手)以外から指示を与えることができるものです。明確に会話内容と区別できるので便利ですね。
これに続けて会話履歴より user, assistant, user, assistant, …. と続けたデータをつなげていき、ChatGPTに送信します。
言葉と感情表現の分離
ChatGPTには感情表現の指示もしています。たとえば「ありがとう{fun}嬉しいにゃ。」みたいな応答が返ってきますが、これをそのままVOICEVOXに渡すと「ありがとうエフユーエヌうれしいにゃ」と喋ってしまいます。そこで言葉と表情の情報は分離させる必要があります。以下はChatGPTから返ってきた内容を保管しておくためのキュー型のデータです。
// 発話するキューのクラス
public class SpeakQueue {
public SpeakType type = SpeakType.Speak;
public int avatar; // 発話者
public int? speakerID = null; // VOICEVOXのspeakerのID
public string text = null; // Speakのとき: 喋る内容
public EmoteType emote; // Emoteのとき: 感情フラグ
}
public enum SpeakType {
Speak,
Emote,
Pause,
Fiinsh,
}
// 表情
public enum EmoteType {
Null,
Neutral,
Joy,
Sorrow,
}
キュー型の変数は順番に処理していくのに適したデータ型で、キューに格納された順番通りに喋ったり表情を変えたりします。type が SpeakType.Speak だったら text の内容をVOICEVOX APIに送信して音声合成を行います。type が SpeakType.Emote だったら、emote の内容に応じてアバターのシェイプキーを変更します。
リップシンク
a i u e o の母音の形に合わせて口を動かすと、より自然に喋っているように見えます。VOICEVOXで音声合成を行うには、クエリの作成と音声データの生成という2ステップの処理を行いますが、この最初のクエリを作成した段階で、発音の情報が取得できます。以下はずんだもんが「ずんだ」と喋るときの音声クエリです。
{
"accent_phrases": [
{
"moras": [
{
"text": "ズ",
"consonant": "z",
"consonant_length": 0.1621599644422531,
"vowel": "u",
"vowel_length": 0.1406611055135727,
"pitch": 5.835348129272461
},
{
"text": "ン",
"consonant": null,
"consonant_length": null,
"vowel": "N",
"vowel_length": 0.137544646859169,
"pitch": 6.061563014984131
},
{
"text": "ダ",
"consonant": "d",
"consonant_length": 0.04071526974439621,
"vowel": "a",
"vowel_length": 0.2459341436624527,
"pitch": 5.8365607261657715
}
],
"accent": 1,
"pause_mora": null,
"is_interrogative": false
}
],
"speedScale": 1,
"pitchScale": 0,
"intonationScale": 1,
"volumeScale": 1,
"prePhonemeLength": 0.1,
"postPhonemeLength": 0.1,
"outputSamplingRate": 24000,
"outputStereo": false,
"kana": "ズ'ンダ"
}
vowel というのが母音なので、これに合わせて口を動かせばリップシンクが可能です。一方VOICEVOXで作成される音声データはwave形式のデータで、こちらは文章全体で1つのファイルになっています。vowel_length に時間の情報が入っているので、音声データを再生中に、この時間が経過したら次の口の形に変える、という処理をしています。
シェイプキーの変更
VRMモデルのシェイプキーをどうやって変更するのかわからなくて最初困りました。実行した状態(プレイボタンを押している状態)で、以下のようにすることで制御することができました。
private VRMBlendShapeProxy proxy;
void Start() {
proxy = this.gameObject.GetComponent<VRMBlendShapeProxy>();
}
:
proxy.ImmediatelySetValue(BlendShapeKey.CreateFromPreset(BlendShapePreset.Blink), weight);
proxy.Apply();
最初はアニメーションでまばたきをやっていたのですが、アニメーションでシェイプキーを制御すると、値がずっと上書きされ続けてしまうんですよね。そうするとプログラムから変更しても反映されないので、最終的にはプログラムで行うようになりました。
新しい話題に変えたい
AI同士が考えて喋って面白~いと、と最初は思ってたのですが、しばらく聞いてるとひたすら同じことを喋り続けることがあります。特にキャラ設定の反映が強く、過去の会話からなかなか新しい話題に変わってくれません。そこでUIを作成し、テキストフォームに入力した”新ネタ”を投入できるようにしてみました。
// SystemControllerの会話履歴に入力したテキストを追加する
public void GetInputNewText() {
string text = inputField.text;
Debug.Log("*** 新しい話題を投入: "+text);
string message = $"会話がひと段落したら、新しい話題「{text}」に変えてください。";
syscon.addTalkHistory(-1, message, -1); // role:system で発言する
inputField.text = "";
}
方法としは、『会話がひと段落したら、新しい話題「…」に変えてください。』というメッセージを system として会話履歴に追加しています。こうすると第三者的に会話に割り込むことができるようになります。
あらかじめ画面上にアイコンを配置しておいて、ボタンを押すと新しい話題に切り替わるようにしてもいいですね。ほかにも定期的にネットから自動でトピックスを拾ってきて、新ネタを放り込んであげるといった使い方もできそうです。これをネット配信しながら、視聴者が会話に参加するようなこともできますね。可能性は無限大!
テキストフォームについては AIChatVRM/Scene/ の中にサンプルシーンが入ってますので、そちらを参考にしてみてください。サンプルのシーンを開くとなぜかアバターが表示されないので、削除してもう一度設定してみてください。
実は、、、ChatGPTに教えてもらいました。
これらのプログラム、実はChatGPTに教えてもらいながら作りました。これを作る前はリスト型もキュー型も知らなかったですし、httpリクエストの送信方法も、JSONのパース方法も、コルーチンの使い方もなーんにも知りませんでした。
ChatGPTすごすぎる!何をしたい場合はどうするの?これはどうやって使うの?エラーが出たけど何が原因?ほかに方法はある?こんな感じで作っていきました。ChatGPTの素晴らしいのは、質問に答えてくれるだけでなく、ちゃんと解説までしてくれる点です。これ、完全に学習ツールじゃないですか!マンツーマンの家庭教師ですよ。おかげで苦手なC#も少しは書けるようになりました。
ほんとに、ChatGPTが登場してから開発の仕方ががらっと変わりました。今まではGoogleで検索して、いろんなページを見たりして調べていて(特にUnity関係ってやたら広告が出る鬱陶しいサイトが上位に出るんだよなぁ)、調べたいことが分かっている場合はそれでいいのですが、漠然とした問いの答えを探すのは従来の検索エンジンでは困難でした。ChatGPTは何をしたいのかを理解して回答してくれるので、そこからどんどん掘り下げて問題解決をしてくれます。今ではもうChatGPTが無い世界なんて考えられませんね。とても効率的に作業ができるようになりました。
免責事項
本プログラムの使用によって生じたいかなる損害も作者は一切責任を負いません。自己責任でご使用ください。本プログラムに不具合があっても、作者に修正の義務はありません。不具合などによりAPIへの過剰なアクセスが発生して高額な料金が請求されるリスクがあります。事前に課金額の上限などを設定したうえで、利用状況を常に把握するようにください。
使用に関して
本プログラムの著作権は放棄していません。プログラムを改変してブログ等で公開する場合は、こちらが元になっていることの記載やリンクをしていただけると嬉しいです。また、Youtubeの配信などに使っていただいても結構です。可能であれば、概要欄などにこの記事へのリンクを書いていただけると嬉しいです(強制ではないです)。
URL → https://akibabara.com/blog/6746.html