背景
以下で作ったゲームキャラをAIアバター化して会話してみる試み
今や何番煎じかわかりませんが、やっぱりゲームや漫画のキャラと会話したいよね
作る
構成
こんな感じです。
ローカルにFastAPIで作った簡易APIサーバーと、Dify(Docker)を立てます。
実装
Realtime API
公式ドキュメントのほぼコピペで動きます。
本当はinputが音声でoutputがテキストにしたかったのですが、現在はできないのかも?(実装間違ってただけかもしれませんが)
/session
でmodalities: [ "text" ]
を指定しても話しかけたら音声が返ってきました。
ということで、とりあえずoutputはテキストだけにしたかったので公式サンプルのこの部分を実装しないようにします。
(outputの音声は作られていると思うので無駄なトークンを消費してますが・・・)
音声合成
にじボイスのAPIを呼び出します。
2024年の年末に試した感じだと同時に1件しか音声を作れないようなので(音声を生成中にもう1件投げたらエラーになった)、1件ずつAPIを投げるようにキューイングしながら処理してあげます。
待ち時間軽減のため、ChatGPTの応答内容全文を一気に音声合成するのではなく、ストリーミング的に文節単位で音声合成をした方が良いのですが今回はその部分は未実装です。
ナレッジ検索
Difyを使った簡易実装です。
「ナレッジを作成」からお手製の原作シナリオと設定を書いたテキストを登録してナレッジを作ります。
使えるRerankのモデルを持ってないので単純なハイブリット検索です。
何も考えずにとりあえずデータ突っ込んだだけなの、正直このままだとプロダクションで使える精度ではなさそうという感想です。
Difyで作ったナレッジはAPI公開できます。
表情やモーション
これは以下で開発したものをそのまま利用。
Toolsの設定
Toolsの設定はsession作成時のパラメータで与えることができます。
Toolの呼び出しはresponse.function_call_arguments.done
で処理します。
switch (message.type) {
case "response.function_call_arguments.done":
if (message.name === "retrieve_knowledge") { // Toolの種類を判定
const obj = JSON.parse(message.arguments); // Toolの引数を取得
const result = await fetchKnowledgeContent(obj.query); // ナレッジ取得のAPI呼び出し
dc.send( // 結果を返す
JSON.stringify({
type: "conversation.item.create",
item: {
type: "function_call_output",
call_id: message.call_id,
output: JSON.stringify(result),
},
})
);
dc.send(JSON.stringify({ type: "response.create" })); // 処理完了
結果
表情とモーションのサンプル
RAGのサンプル
気づいたことなど
(2024年1月初旬)
- 弱小OpenAIユーザーなのでRealtime APIのRate limitにすぐに引っかかりました。テストで動かす程度であれば金額自体はたいした額にはならないです。
- OpenAIの音声は日本語がちょいちょい間違っていて、スクリプトとは違うことをけっこう喋る。にじボイスの方が日本語は良かった。
- にじボイスのクレジットはどんどん消費しちゃいます。
- Tool呼び出し中の待ち時間、なんか喋っていて欲しいのだけど無口なことが多い(たまに喋ってくれるけど)。
conversation.item.create
で「実行中」とか送ってみたけど効果あるのか不明だった。 - OpenAIとAOAIとでRealtime APIのイベントオブジェクトの構造が微妙に違う。
Gemini nanoのようなプラットフォーム同梱のSLMで動かせるようになるともっとワクワクしそう。