見出し画像

STT×TTS×AR×ChatGPT なアプリをサクッと作る

※この記事は2023年5月29日に弊社運営の技術情報サイト「ギャップロ」に掲載した記事です。

はじめに

我々の業界どころかテレビなどでも紹介が増えてきた ChatGPT。
今回はその「対話型AI」の ChatGPT を使ったスマホアプリを作ってみました。

開発環境

ツール

  • Unity 2022.1.19f1

  • Xcode 14.3

Unity Package

追加したパッケージです。
見慣れないパッケージがあると思いますが、それらについては下述していきます。

Packages/manifest.json

"com.unity.xr.arcore": "4.2.7",
"com.unity.xr.arkit": "4.2.7",
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask",
"com.neuecc.unirx": "https://github.com/neuecc/UniRx.git?path=Assets/Plugins/UniRx/Scripts",
"com.mochineko.chatgpt-api": "https://github.com/mochi-neko/ChatGPT-API-unity.git?path=/Assets/Mochineko/ChatGPT_API#0.4.0",
"com.mochineko.chatgpt-api.memory": "https://github.com/mochi-neko/ChatGPT-API-unity.git?path=/Assets/Mochineko/ChatGPT_API.Memory#0.4.0",
"com.mochineko.tiktoken-sharp": "https://github.com/mochi-neko/ChatGPT-API-unity.git?path=/Assets/Mochineko/TiktokenSharp#0.4.0",
"com.unity.nuget.newtonsoft-json": "3.0.2",

作るもの


★クリックで動画を再生★
  1. 目の前に対話相手のアバターが現れる

  2. ユーザがマイクボタンを押下して話しかけ続ける

  3. アバターが文脈を理解した上で返答し続ける

このようにシンプルな内容のアプリですが…

アバターに話しかけてからは色々な事をしていて、大雑把に書くとこのような感じです。

  1. ユーザの声を録音開始

  2. ユーザが話終わる(=マイクボタンを離す)と録音停止

  3. 録音した音声をテキスト変換 (STT)

  4. 変換したテキストを ChatGPT API で送信

  5. ChatGPT API から返答がくる

  6. 返答のテキストを音声に変換 (TTS)

  7. 音声を再生

続いては、この1〜7について細かく解説していきます。
(※ AR部分は大したことをしていないので説明を省きます。)

解説

STT と TTS の実装

今回は STT(speech to text) と TTS(text to speech) の実装のためにこちらを使用しました。

Speech Framework を用いることで iOS のネイティブ機能だけで STT & TTS を実現しています。
(今回は使用していませんが Android も同様のことができるそうです)

ビルド設定

Framework の追加と info.plist へ 必要な設定 があるので PostProcessBuild で自動化しておきます。

[PostProcessBuild]
private static void OnPostProcessBuild(BuildTarget buildTarget, string pathToBuiltProject)
{
    if (buildTarget != BuildTarget.iOS) return;
    
    // Add FrameWork
    var projectPath = PBXProject.GetPBXProjectPath(pathToBuiltProject);
    var project = new PBXProject();
    project.ReadFromString(File.ReadAllText(projectPath));
    var targetGuid = project.TargetGuidByName("UnityFramework");
    
    // 必要な Framework を追加
    project.AddFrameworkToProject(targetGuid, "Speech.framework", false);
    project.AddFrameworkToProject(targetGuid, "AVFoundation.framework", false);
    project.AddBuildProperty(targetGuid, "OTHER_LDFLAGS", "-ObjC");
    
    File.WriteAllText(projectPath, project.WriteToString());
    
    // Info.plist
    var plistPath = pathToBuiltProject + "/Info.plist";
    var plist = new PlistDocument();
    plist.ReadFromString(File.ReadAllText(plistPath));

    // 音声認識の許可を設定 ※マイクは Unity Edtior の Player Settings で同様の設定済み
    plist.root.SetString("NSSpeechRecognitionUsageDescription", "This app needs access to Speech Recognition");
    plist.WriteToFile(plistPath);
}

肉声を録音 〜 テキスト変換 (STT)

Speech And Text in Unity iOS and Unity Android はとてもシンプルな構造なので特段説明は不要かと思います。

SpeechButton は今回用のオリジナルのボタン処理クラスです。
OnButtonDownAction, OnButtonUpAction に押した直後、離した直後の処理を設定しています。

public class MyAppManager : MonoBehaviour
{
    /// <summary>
    /// 言語
    /// </summary>
    [SerializeField]
    private string _code; // 日本語なら "ja_JP"

    /// <summary>
    /// マイクボタン
    /// </summary>
    [SerializeField] 
    private SpeechButton _speechButton;

    private void Awake()
    {
        // 録音ボタン押す・離す
        _speechButton.OnButtonDownAction = onSpeechButtonDown;
        _speechButton.OnButtonUpAction = onSpeechButtonUp;
    }

    private void Start()
    {
        // 言語設定、テキスト変換を受け取るコールバックの設定
        SpeechToText.Instance.Setting(_code);
        SpeechToText.Instance.onResultCallback += onSTT;
    }

    /// <summary>
    /// マイクボタン押下直後
    /// </summary>
    private void onSpeechButtonDown()
    {
        if( Application.platform.IsEditor())
            return;
        
        // 喋っていたら止める
        TextToSpeech.Instance.StopSpeak();
        
        // 録音を開始
        SpeechToText.Instance.StartRecording();
    }

    /// <summary>
    /// マイクボタン離した直後
    /// </summary>
    private void onSpeechButtonUp()
    {
        if( Application.platform.IsEditor())
            return;

        // 録音を終了
        SpeechToText.Instance.StopRecording();
    }

    /// <summary>
    /// STT 完了通知
    /// </summary>
    /// <param name="text">変換されたテキスト</param>
    private void onSTT(string text)
    {
        // 録音を終了後暫くして変換されたテキストを取得できる
        Debug.Log(text);
    }
}

返答テキストを音声に変換 (TTS) & 再生

これについても特段説明は不要かと思います。
sendChatAsync() については下述します。

public class MyAppManager : MonoBehaviour
{
    /// <summary>
    /// 言語
    /// </summary>
    [SerializeField]
    private string _code; // 日本語なら "ja_JP"

    /// <summary>
    /// ピッチ 
    /// </summary>
    [SerializeField]
    private float _pitch;  // 1が妥当
    
    /// <summary>
    /// レート
    /// </summary>
    [SerializeField]
    private float _rate; // 1が妥当

    private void Start()
    {
        // 言語、ピッチ、レートの設定
        TextToSpeech.Instance.Setting(_code, _pitch, _rate);
    }

    /// <summary>
    /// 解答する
    /// </summary>
    /// <param name="text">質問テキスト</param>
    private async UniTask answerAsync(string question, CancellationToken cancellationToken)
    {
        string? answer = null;
        
        if (string.IsNullOrEmpty(question?.Trim()))
        {
            answer = "すいません、聞き取れませんでした。もう一度お願いします。";
        }
        else
        {
            // 質問をChatGPTに投げる
            answer = await sendChatAsync(question, cancellationToken);
        }
        
        // 返答テキストを音声に変換にして再生
        if(!cancellationToken.CanBeCanceled)
            TextToSpeech.Instance.StartSpeak(answer);
    }
}

Unity で ChatGPT を使う

今回は Unity で ChatGPT を利用するのに もちねこ さんの ChatGPT-API-unity を使わせてもらいました。

パッケージ設定

上述の開発環境の項の Packages/manifest.json に書かれた com.mochineko.xxx が ChatGPT-API-unity のパッケージ群です。

質問を ChatGPT に投げる

ChatGPT-API-unity もシンプルな構造で大変使いやすいので説明は特にないですね。言い換えれば、こんなに簡単に高度なAIを使えるようになったってことですね。

public class MyAppManager : MonoBehaviour
{
    /// <summary>
    /// 覚えている履歴のサイズ
    /// </summary>
    [SerializeField] private int maxMemoryCount = 20;
    
    /// <summary>
    /// OpenAPI キー
    /// </summary>
    [SerializeField] private string _apiKey = string.Empty;
    
    /// <summary>
    /// システムのロール (人格を設定するなどに)
    /// </summary>
    [SerializeField, TextArea] private string _systemMessage = string.Empty;

    /// <summary>
    /// コネクション
    /// </summary>
    private ChatCompletionAPIConnection? _connection = null;
    
    /// <summary>
    /// 覚えている履歴
    /// </summary>
    private IChatMemory? _memory = null;

    private void Awake()
    {
        // 履歴の箱を作る
        _memory = new FiniteQueueChatMemory(maxMemoryCount);

        // 接続インスタンス生成
        _connection = new ChatCompletionAPIConnection(
            _apiKey,
            _memory,
            _systemMessage);
    }

    /// <summary>
    /// ChatGPT に話しかける
    /// </summary>
    /// <param name="message">質問内容</param>
    /// <param name="cancellationToken">キャンセルトークン</param>
    /// <returns>返答内容</returns>
    private async UniTask<string?> sendChatAsync(string message, CancellationToken cancellationToken)
    {
        string? result = null;

        try
        {
            await UniTask.SwitchToThreadPool();

       // 質問を投げる
            var response = await _connection.CompleteChatAsync(
                message,
                cancellationToken);
            
            result = response.ResultMessage;
        }
        catch (Exception e)
        {
            Debug.LogException(e);
        }
        finally
        {
            await UniTask.SwitchToMainThread(cancellationToken);
        }

        return result;
    }    
}

STT > ChatGPT > TTS

上述までのコードを

肉声をテキスト変換 > ChatGPT に質問として投げる > 返答を音声変換

の流れでまとめるとこうなります。

#nullable enable
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Mochineko.ChatGPT_API;
using Mochineko.ChatGPT_API.Memory;
using TextSpeech;
using Unity.VisualScripting;
using UnityEngine;

/// <summary>
/// STT > ChatGPT > TTS をする
/// </summary>
public class MyAppManager : MonoBehaviour
{

    /// <summary>
    /// 言語
    /// </summary>
    [SerializeField]
    private string _code;
    
    /// <summary>
    /// ピッチ
    /// </summary>
    [SerializeField]
    private float _pitch;
    
    /// <summary>
    /// レート
    /// </summary>
    [SerializeField]
    private float _rate;
    
    /// 覚えている履歴のサイズ
    /// </summary>
    [SerializeField] private int maxMemoryCount = 20;
    
    /// <summary>
    /// OpenAPI キー
    /// </summary>
    [SerializeField] private string _apiKey = string.Empty;
    
    /// <summary>
    /// システムのロール (人格を設定するなどに)
    /// </summary>
    [SerializeField, TextArea] private string _systemMessage = string.Empty;
    
    /// <summary>
    /// マイクボタン
    /// </summary>
    [SerializeField] 
    private SpeechButton _speechButton;

    /// <summary>
    /// コネクション
    /// </summary>
    private ChatCompletionAPIConnection? _connection = null;
    
    /// <summary>
    /// 覚えている履歴
    /// </summary>
    private IChatMemory? _memory = null;

    private void Awake()
    {
        // 録音ボタン押す・離す
        _speechButton.OnButtonDownAction = onSpeechButtonDown;
        _speechButton.OnButtonUpAction = onSpeechButtonUp;

        // 履歴の箱を作る
        _memory = new FiniteQueueChatMemory(maxMemoryCount);

        // 接続インスタンス生成
        _connection = new ChatCompletionAPIConnection(
            _apiKey,
            _memory,
            _systemMessage);
    }
    
    private void Start()
    {
     // 言語設定、テキスト変換を受け取るコールバックの設定
        SpeechToText.Instance.Setting(_code);
        SpeechToText.Instance.onResultCallback += onSTT;
        
     // 言語、ピッチ、レートの設定
        TextToSpeech.Instance.Setting(_code, _pitch, _rate);
    }

    /// <summary>
    /// マイクボタン押下直後
    /// </summary>
    private void onSpeechButtonDown()
    {
        if( Application.platform.IsEditor())
            return;
        
        // 喋っていたら止める
        TextToSpeech.Instance.StopSpeak();
        
        // 録音を開始
        SpeechToText.Instance.StartRecording();
    }

    /// <summary>
    /// マイクボタン離した直後
    /// </summary>
    private void onSpeechButtonUp()
    {
        if( Application.platform.IsEditor())
            return;

        // 録音を終了
        SpeechToText.Instance.StopRecording();
        
        // ボタンのインタラクションを無効にする
        _speechButton.Interactable = false;

    }

    /// <summary>
    /// ユーザの音声をテキストに変換後
    /// </summary>
    /// <param name="text"></param>
    private void onSTT(string text)
    {
        answerAsync(text, this.GetCancellationTokenOnDestroy()).Forget();
    }

    /// <summary>
    /// 解答する
    /// </summary>
    /// <param name="text">質問テキスト</param>
    private async UniTask answerAsync(string question, CancellationToken cancellationToken)
    {
        string? answer = null;
        
        if (string.IsNullOrEmpty(question?.Trim()))
        {
            answer = "すいません、聞き取れませんでした。もう一度お願いします。";
        }
        else
        {
            // 質問をChatGPTに投げる
            answer = await sendChatAsync(question, cancellationToken);
        }
        
        //ボタンのインタラクションを有効にする
        _speechButton.Interactable = true;

        // 返答テキストを音声に変換にして再生
        if(!cancellationToken.CanBeCanceled)
            TextToSpeech.Instance.StartSpeak(answer);
    }

    /// <summary>
    /// ChatGPT に話しかける
    /// </summary>
    /// <param name="message">質問内容</param>
    /// <param name="cancellationToken">キャンセルトークン</param>
    /// <returns>返答内容</returns>
    private async UniTask<string?> sendChatAsync(string message, CancellationToken cancellationToken)
    {
        string? result = null;

        try
        {
            await UniTask.SwitchToThreadPool();

       // 質問を投げる
            var response = await _connection.CompleteChatAsync(
                message,
                cancellationToken);
            
            result = response.ResultMessage;
        }
        catch (Exception e)
        {
            Debug.LogException(e);
        }
        finally
        {
            await UniTask.SwitchToMainThread(cancellationToken);
        }

        return result;
    }    
}

適当な GameObject に付けてインスペクターに次のように設定します。

はい、これだけです。
私も実際にやってみて拍子抜けするくらい簡単に出来てしましました。

まとめ

簡単に出来たとは言いましたが、

  • ChatGPT の API ってどうやって使うの?

  • STT/TTS と組み合わてアバターと会話しているっぽくするには?

とかは、実際に自分で実装してみてようやく理解しました。
やってみると簡単です、でもやってみないと実感は得られないと思います。
ぜひ皆さんも ChatGPT で何か作ってみてください!


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