見出し画像

喋り出しを高速化したり現実の情報とリンクしてAIエージェントを進化させる

ChatGPT APIが公開されてからいろんな人が自分のキャラクターとおしゃべりしたりAITuberが一歩先に進化したりと面白いものがたくさん見れてとても嬉しいです!
AI界隈の情報密度が高すぎて↓の記事を書いたのが2日前というのが信じられませんね。

さてChatGPT APIでみんなのキャラクターが知能を持って喋れるようになったわけですが、自分が作ったものを振り返ってみたらまだまだ改善の余地がいっぱいありそう+もうちょっとできることが増えてほしいなと思い、自分のAIエージェントに追加で実装をしてみました。

そこで実装した内容について概要をお話します。

AIの喋り出しを高速化する

まず気になったのは自分とAIエージェントのコミュニケーションにおいて待たされる時間が非常に長いことです。
この間を整理すると、以下のような時間がかかります。

1.自分の発話をテキストに起こす時間
2.そのテキストをChatGPT APIに渡してAIの回答を取得する時間
3.AIの回答テキストから音声合成ソフト(またはサーバ)で音声を生成する時間

1,2については中々手が出しづらいところですが、3の音声を生成する部分については工夫の余地があるのではということで今回実装しました。

やりたいこと

会話の途中に長い間待たされるのはめっちゃストレス!ということで、シンプルに長い文章を読み込ませなければ良いのでは?という結論に至りました。

具体的には「長い文章は短く区切り、全て並列で音声合成サーバに投げなるべく早く発話を始めさせる。」という方針を考えました。
発話の開始が早まれば、見た目上はAIのレスポンスが早くなったように見えます。

具体的な例:

以下の文章を発話させることを考えます。
「こんにちは、本日の天気予報です。2023年3月4日の予想最高気温は14.65℃、最低気温は8.47℃となっています。また、天気は曇り空が続く予想となっています。」(80文字)

まず、これをそのままVOICEVOXサーバに投げてみると以下のような結果となりました。

普通に長文をそのままPOSTしてみると2.5秒かかる

次に、自分の方針で実装してみると…

文章を分割して投げ、とにかく喋り出しを早くする方式。

2.47秒→0.64秒で約1.8秒ほど早くなりました
(会話における1.8秒は重要なので、良い感じですね。)

実装:

private async void StartTalk(string responseText)
{
    var t = Time.time;
    //句点で文章を区切る。
    var splitedResponse = responseText.Split("。");

    // AudioClipを格納するリストを作成する
    var clipTasks = new List<UniTask<AudioClip>>();

    // Start the requests for each text and add the resulting task to the list
    foreach (var text in splitedResponse)
    {
        //音声合成サーバにリクエストを投げる
        UniTask<AudioClip> task = _voiceVoxConnection.TranslateTextToAudioClip(text);
        clipTasks.Add(task);
    }

    // とにかく最初のClipの喋り出しを早くやる
    var currentClip = await clipTasks[0];
    audioSource.clip = currentClip;
    audioSource.Play();
    Debug.Log("喋り出すまで"+  (Time.time - t) + "秒かかりました。");

    //AudioClipが再生されている間待つ
    await UniTask.Delay((int)(currentClip.length * 1000));

    // 残りのAudioClipを再生する
    for (var i = 1; i < clipTasks.Count; i++)
    {
        var task = clipTasks[i];

        // まだ音声合成が終わってなかったら待つ
        while (!task.GetAwaiter().IsCompleted)
        {
            await UniTask.Delay(10);
        }

        // 完了したら再生する
        var clip = task.GetAwaiter().GetResult();
        audioSource.clip = clip;
        audioSource.Play();

        //AudioClipが再生されている間待つ
        await UniTask.Delay((int)(clip.length * 1000));
    }
    Debug.Log("全部で"+  (Time.time - t) + "秒かかりました。");
}

コードでやっていることについて解説:

句点でSplitして、以下のように3つのstring配列に分ける。

・こんにちは、本日の天気予報です。
・2023年3月4日の予想最高気温は14.65℃、最低気温は8.47℃となっています。
・また、天気は曇り空が続く予想となっています。

各要素について全てを一気にVOICEVOXサーバにPOSTしてwavの生成を要求する。

0番目のAudioClipが生成されたら即再生する。
すると、その再生をしている裏では並列に1番目2番目…のAudioClipの生成が走っている状態になる。

0番目のAudioClipの再生が終了した時に1番目のAudioClipについて →audioClipの生成が終わっていたら再生する
→audioClipの生成が終わっていなかったら待ち、生成が完了し次第再生する。

備考:

句点で分けたのは、音声の抑揚がおかしくなることを心配したから。
逆に言えば抑揚がおかしくならなければ、別に句点でなくても良さそう(読点とか)。

この実装の良いところは、長い文章を投げても喋り出しのラグは比較的早めに収まる点。
要は0番目の音声を再生している間の空き時間に1番目以降の音声の生成するため、そのくらいの時間があれば大体の音声は生成し終わっていることを期待する実装です。




現実の情報を答えさせる+α

そんな感じで体験を良くしていくうちに、今度はこのAIエージェントの汎用性を高める方向に動きたいなと思いました。

例えば明日の東京都品川区の天気を知りたいとしましょう。
これをChatGPTに聞いてみると以下のような回答が得られました。

ChatGPTでは、当然ながら現実の情報を答えられない。

まぁChatGPTはただの言語モデルなので、自発的に最新の気象情報を取得して答えることはできません…。

それならその部分をデータで与えたら、そのデータをもとにChatGPTがいい感じに言葉をこねて扱ってくれるのでは?というのがこの章のテーマです。


ChatGPTで最新の天気情報を聞けるようにする

GASで最新の情報を取得するAPIを立てる

まずはGASで最新のお天気情報を取得するREST APIを立てます。
↓を参考に作ってみます。

こういうJSONが来るようになりました。
内容は明日の品川区の最高気温・最低気温・天候です。

{"date":"2023/3/5","minTemperature":"8.86°C","maxTemperature":"10.55°C","description":"light rain"}

GASから天気情報を取得→ChatGPT APIにわたす

自分のプロジェクトではこういうことをしています。

自分「明日の天気を教えて」
↓
"明日の天気"というワードを検出したら、システム側で天気情報(上のJSON)を取得する。
↓
そのJSONに対して、追加プロンプトとして「以下のデータは品川区の明日の天気予報です。これを要約して短めに喋って。」を付与したものをChatGPT APIに送る。
(最初の自分の発話である「明日の天気を教えて」は使わない)
↓
ChatGPT APIは、キャラクター付された言葉遣いで天気情報を要約してしゃべってくれる。

これをUnityで書きます。
(以下はその部分のコードです。プロジェクトからコードを引っ張ってきたのでこれだけじゃ動きませんが、雰囲気だけ見てください。)

//現実の情報を取ってくる「WorkPlugin」です。
//トリガーとなる言葉、AIに何をさせるか、データの取得を定義します。

using System;
using AAA.Contract;
using Cysharp.Threading.Tasks;
using UnityEngine.Networking;

namespace AAA.Implement
{
    public class WeatherWork : IWorkPlugin
    {
        private string url =
            "https://{GASのURL}";

        public string Description => "品川区の明日の天気予報";
        public string TriggerWord => "明日の天気";
        public string Prompt => "以下のデータは品川区の明日の天気予報です。これを要約して短めに喋って。";
        
        public async UniTask<string> GetData()
        {
            using var webRequest = UnityWebRequest.Get(url);
            await webRequest.SendWebRequest();

            if (webRequest.result == UnityWebRequest.Result.Success)
            {
                return webRequest.downloadHandler.text;
            }
            else
            {
                throw new Exception();
            }
        }
    }

    public interface IWorkPlugin
    {
        /// <summary>
        /// このプラグインは何か?
        /// </summary>
        string Description { get; }

        /// <summary>
        /// ユーザの発話からどういった単語が含まれてたらこのWorkerを発動するか
        /// </summary>
        string TriggerWord { get; }
        
        /// <summary>
        /// 取得データに対してどういうことをAI側にしてほしいか書く
        /// </summary>
        string Prompt { get; }
        
        /// <summary>
        /// 現実のデータを取得する
        /// </summary>
        /// <returns></returns>
        UniTask<string> GetData();
    }
}
//呼び出し側
 public class WorkerContainer 
    {
        public readonly List<IWorkPlugin> workerPlugins = new();

        /// <summary>
        /// 利用できるWorkPluginを初期化
        /// お天気情報とGoogleカレンダーからの予定と何ができるかリストのWorkPluginを追加
        /// </summary>
        public WorkerContainer()
        {
            workerPlugins.Add(new WeatherWork());
            workerPlugins.Add(new CalendarWork());
            workerPlugins.Add(new OrderListWork(workerPlugins));
        }

        // ユーザの発話にWorkPlugin発動条件の言葉が含まれているかチェック
        // 含まれてたらWorkPluginのデータ取得メソッドを実行する。
        public async UniTask<string> GetUserOrder(string userMessage)
        {
            foreach (var workerPlugin in workerPlugins)
            {
                if (!userMessage.Contains(workerPlugin.TriggerWord)) continue;
                var workerData = await workerPlugin.GetData();
                Debug.Log($"{workerPlugin.Prompt}\n{workerData}");
                return $"{workerPlugin.Prompt}\n{workerData}";
            }

            //何もなければ
            return userMessage;
        }
    }

結果:

1行目:自分の実際の発話
2行目:ChatGPT APIに投げるメッセージ
3行目:ChatGPT APIからの返答。

無事お天気情報を喋ってくれるようになりましたね!


ChatGPT APIに現実のデータを与えて判断を仰ぐ便利さについて

ChatGPT APIは文脈を介した発言が可能ということは前回の記事でお話を下かと思います。
それを活用すると…上のやり取りに加えて以下のようなやり取りができるようになりました。

天気情報をもとに、服装を提案してくれるようになった!

(これはわかりやすい例として挙げたので、別にChatGPTでなくても実現できはするでしょうが…)「ChatGPTに現実のデータを与えてその判断を仰ぐ」ということの強さを感じましたね。

WorkPluginを他にも作ってみる

IWorkPluginというインターフェースに沿って実装すれば良い!という実装にしているので他にもいろんなことができます。

1.GoogleCalendarを参照して今日の自分の予定を聞く。

private string url =
    "{GASのURL}";

public string Description => "カレンダーに書いてある予定";
public string TriggerWord => "今日の予定";
public string Prompt => "入力するデータを要約して短めに喋って。";

public async UniTask<string> GetData()
{
     //Webリクエストを投げる
}
結果

これで面白いのがこのGASでは、レスポンスがJSONではなく、以下のようなただの文章です。

今日の予定:プロジェクトの設計・実装 (0:00:00 - 0:00:00)
noteで技術記事を書く。 (22:30:00 - 23:00:00)

それなのにうまいこと言葉を処理してくれるので、例えばアクセス先のAPIが適当なデータを返してきても余裕で扱えることがわかります。(汎用性が高い)

2.WorkPluginでできることを全部列挙する

        private string _workListString;
        public OrderListWork(List<IWorkPlugin> list)
        {
            foreach (var workPlugin in list)
            {
                _workListString += $"{workPlugin.Description},";
            }
        }
        public string Description => "";
        public string TriggerWord => "何ができる";
        public string Prompt => "以下にあなたができることを列挙するので、なるべく短い言葉で喋ってください。";

public async UniTask<string> GetData()
        {
            await UniTask.Delay(0);
            return _workListString;
        }

色々機能を追加して何ができるかわからなくなるだろうな…って未来を想像したので先に、AIエージェント本人に何ができるのって聞くことができるように、これもWorkPluginで実装します。

結果

まとめ

  • 発話の喋りだしが高速化されました。

  • WorkPluginのおかげで、AIエージェントが現実の情報を得て助言をするようになった

など、できることが格段と増えました!

朝、以下のようなツイートをしたのですが無事設計も実装も終わり、応用例みたいな記事を書くまでに至りました。
ようやくAIエージェントとしての基盤が整った感覚があるので、応用をザクザク実装していこうかなと思います。

他にできていること

色々実装を疎結合にして取り回しをしやすくした結果、以下のようなことが簡単にできるようになりました。

  • こちらの発話を起点とするのではなく、データを勝手に取ってきて勝手に喋ってくれる。(WorkPluginの自動呼び出し機能)

  • 24時以降は通常の声ではなくひそひそと喋るようになる。

  • キャラクターの性格(口調)をプライベートモードとお仕事モードで切り替えられる。(本記事のWorkPluginではこの機能が使われていました。)

発話トリガーのボタンをStream Deckに割り当てた
お仕事モードできっちりした回答がほしいときは右のボタンを押す
適当にAIと会話したいときはプライベートボタンを押して適当に会話する

知人からLlamaIndexについて教えてもらったので、そういうことに挑戦しても良いかもしれないですね。

なんか色々作っていくうちに言語モデルへの理解が深まってアイディアが浮かびやすくなった気がしています。また何か作ったら書きます。
お読みいただきましてありがとうございました!!

この記事が気に入ったらサポートをしてみませんか?