XREAL Air からマイク録音して音データを Whisper API で文字起こしして ChatGPT API とやり取りするメモです。
ベースとなる文献
Unity からマイク録音して音データを Whisper API で文字起こしして ChatGPT API とやり取りするメモ – 1ft-seabass.jp.MEMO
こちらを元に XREAL Air のマイク認識部分だけ追加して実装できました。
Unity の XREAL Air プロジェクトで Cube と結果テキストを準備
2023/10 現在の情報で進めます。
Unity の XREAL Air プロジェクトで Cube と結果テキストを準備します。
左の Cube を配置して、右に TextMeshPro で TextResult という結果を表示するテキストを用意します。TextResult は日本語を表示するので TextMeshPro で日本語表示可能な対応をしています。
プロジェクトとしてこんな構成です。
Cube に CubeEvent.cs を割り当て
Cube に CubeEvent.cs を割り当てます。
前述のとおり、Unity からマイク録音して音データを Whisper API で文字起こしして ChatGPT API とやり取りするメモ に、XREAL Air の録音のターゲットになるマイクデバイス名 Android audio input にする対応と TextResult という結果を表示するテキストへつどつど状況を表示する対応をしています。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Networking; using System; using System.IO; using System.Text; using TMPro; public class CubeEvent : MonoBehaviour, IPointerClickHandler { // マイクの開始・終了管理 bool flagMicRecordStart = false; // マイクデバイスがキャッチできたかどうか bool catchedMicDevice = false; // 現在録音するマイクデバイス名 string currentRecordingMicDeviceName = "null"; // PC の録音のターゲットになるマイクデバイス名 // これはお使いのデバイスで変わります // 完全一致でないと受け取れないので注意 string recordingTargetMicDeviceName = "Krisp Microphone (Krisp Audio)"; // XREAL Air の録音のターゲットになるマイクデバイス名 "Android audio input" string recordingTargetMicDeviceNameForXREALAir = "Android audio input"; // ヘッダーサイズ int HeaderByteSize = 44; // BitsPerSample int BitsPerSample = 16; // AudioFormat int AudioFormat = 1; // 録音する AudioClip AudioClip recordedAudioClip; // サンプリング周波数 int samplingFrequency = 44100; // 最大録音時間[sec] int maxTimeSeconds = 10; // Wav データ byte[] dataWav; // OpenAIAPIKey // WhisperAPI と ChatGPTAPI で共通 string OpenAIAPIKey = "OpenAIAPIKey"; // Wisper API で受信した JSON データを Unity で扱うデータにする WhisperAPIResponseData ベースクラス [Serializable] public class WhisperAPIResponseData { public string text; } // ChatGPT API で受信した JSON データを Unity で扱うデータにする ResponseData ベースクラス // API仕様 : https://platform.openai.com/docs/api-reference/completions/object [Serializable] public class ResponseData { public string id; public string @object; // object は予約語なので @ を使ってエスケープしています public int created; public List<ResponseDataChoice> choices; public ResponseDataUsage usage; } [Serializable] public class ResponseDataUsage { public int prompt_tokens; public int completion_tokens; public int total_tokens; } [Serializable] public class ResponseDataChoice { public int index; public RequestDataMessages message; public string finish_reason; } // ChatGPT API に送信する Unity データを JSON データ化する RequestData ベースクラス [Serializable] public class RequestData { public string model; public List<RequestDataMessages> messages; } [Serializable] public class RequestDataMessages { public string role; public string content; } void Start() { catchedMicDevice = false; Launch(); } void Launch() { // マイクデバイスを探す foreach (string device in Microphone.devices) { Debug.Log($"Mic device name : {device}"); // PC 用のマイクデバイスを割り当て if (device == recordingTargetMicDeviceName) { Debug.Log($"{recordingTargetMicDeviceName} searched"); currentRecordingMicDeviceName = device; catchedMicDevice = true; } // XREAL Air 用のマイクデバイスを割り当て if (device == recordingTargetMicDeviceNameForXREALAir) { Debug.Log($"{recordingTargetMicDeviceNameForXREALAir} serched"); currentRecordingMicDeviceName = device; catchedMicDevice = true; } } if (catchedMicDevice) { Debug.Log($"マイク捜索成功"); Debug.Log($"currentRecordingMicDeviceName : {currentRecordingMicDeviceName}"); // テキストにお知らせ GameObject.Find("TextResult").GetComponent<TextMeshPro>().SetText($"マイク捜索成功 : {currentRecordingMicDeviceName}"); } else { Debug.Log($"マイク捜索失敗"); } } void Update() { } public void OnPointerClick(PointerEventData eventData) { if (catchedMicDevice) { if (flagMicRecordStart) { // Stop // マイクの録音を開始 flagMicRecordStart = false; Debug.Log($"Mic Record Stop"); // テキストに反映 GameObject.Find("TextResult").GetComponent<TextMeshPro>().SetText($"録音終了"); RecordStop(); } else { // Start // マイクの停止 flagMicRecordStart = true; Debug.Log($"Mic Record Start"); // テキストに反映 GameObject.Find("TextResult").GetComponent<TextMeshPro>().SetText($"録音開始"); RecordStart(); } } } void RecordStart() { // マイクの録音を開始して AudioClip を割り当て recordedAudioClip = Microphone.Start(currentRecordingMicDeviceName, false, maxTimeSeconds, samplingFrequency); } void RecordStop() { // マイクの停止 Microphone.End(currentRecordingMicDeviceName); Debug.Log($"WAV データ作成開始"); // using を使ってメモリ開放を自動で行う using (MemoryStream currentMemoryStream = new MemoryStream()) { // ChunkID RIFF byte[] bufRIFF = Encoding.ASCII.GetBytes("RIFF"); currentMemoryStream.Write(bufRIFF, 0, bufRIFF.Length); // ChunkSize byte[] bufChunkSize = BitConverter.GetBytes((UInt32)(HeaderByteSize + recordedAudioClip.samples * recordedAudioClip.channels * BitsPerSample / 8)); currentMemoryStream.Write(bufChunkSize, 0, bufChunkSize.Length); // Format WAVE byte[] bufFormatWAVE = Encoding.ASCII.GetBytes("WAVE"); currentMemoryStream.Write(bufFormatWAVE, 0, bufFormatWAVE.Length); // Subchunk1ID fmt byte[] bufSubchunk1ID = Encoding.ASCII.GetBytes("fmt "); currentMemoryStream.Write(bufSubchunk1ID, 0, bufSubchunk1ID.Length); // Subchunk1Size (16 for PCM) byte[] bufSubchunk1Size = BitConverter.GetBytes((UInt32)16); currentMemoryStream.Write(bufSubchunk1Size, 0, bufSubchunk1Size.Length); // AudioFormat (PCM=1) byte[] bufAudioFormat = BitConverter.GetBytes((UInt16)AudioFormat); currentMemoryStream.Write(bufAudioFormat, 0, bufAudioFormat.Length); // NumChannels byte[] bufNumChannels = BitConverter.GetBytes((UInt16)recordedAudioClip.channels); currentMemoryStream.Write(bufNumChannels, 0, bufNumChannels.Length); // SampleRate byte[] bufSampleRate = BitConverter.GetBytes((UInt32)recordedAudioClip.frequency); currentMemoryStream.Write(bufSampleRate, 0, bufSampleRate.Length); // ByteRate (=SampleRate * NumChannels * BitsPerSample/8) byte[] bufByteRate = BitConverter.GetBytes((UInt32)(recordedAudioClip.samples * recordedAudioClip.channels * BitsPerSample / 8)); currentMemoryStream.Write(bufByteRate, 0, bufByteRate.Length); // BlockAlign (=NumChannels * BitsPerSample/8) byte[] bufBlockAlign = BitConverter.GetBytes((UInt16)(recordedAudioClip.channels * BitsPerSample / 8)); currentMemoryStream.Write(bufBlockAlign, 0, bufBlockAlign.Length); // BitsPerSample byte[] bufBitsPerSample = BitConverter.GetBytes((UInt16)BitsPerSample); currentMemoryStream.Write(bufBitsPerSample, 0, bufBitsPerSample.Length); // Subchunk2ID data byte[] bufSubchunk2ID = Encoding.ASCII.GetBytes("data"); currentMemoryStream.Write(bufSubchunk2ID, 0, bufSubchunk2ID.Length); // Subchuk2Size byte[] bufSubchuk2Size = BitConverter.GetBytes((UInt32)(recordedAudioClip.samples * recordedAudioClip.channels * BitsPerSample / 8)); currentMemoryStream.Write(bufSubchuk2Size, 0, bufSubchuk2Size.Length); // Data float[] floatData = new float[recordedAudioClip.samples * recordedAudioClip.channels]; recordedAudioClip.GetData(floatData, 0); foreach (float f in floatData) { byte[] bufData = BitConverter.GetBytes((short)(f * short.MaxValue)); currentMemoryStream.Write(bufData, 0, bufData.Length); } Debug.Log($"WAV データ作成完了"); dataWav = currentMemoryStream.ToArray(); Debug.Log($"dataWav.Length {dataWav.Length}"); // まず Wisper API で文字起こし StartCoroutine(PostWhisperAPI()); } } // Wisper API で文字起こし IEnumerator PostWhisperAPI() { // IMultipartFormSection で multipart/form-data のデータとして送れます // https://docs.unity3d.com/ja/2018.4/Manual/UnityWebRequest-SendingForm.html // https://docs.unity3d.com/ja/2019.4/ScriptReference/Networking.IMultipartFormSection.html // https://docs.unity3d.com/ja/2020.3/ScriptReference/Networking.MultipartFormDataSection.html List<IMultipartFormSection> formData = new List<IMultipartFormSection>(); // https://platform.openai.com/docs/api-reference/audio/createTranscription // Whisper モデルを使う formData.Add(new MultipartFormDataSection("model", "whisper-1")); // 日本語で返答 formData.Add(new MultipartFormDataSection("language", "ja")); // WAV データを入れる formData.Add(new MultipartFormFileSection("file", dataWav, "whisper01.wav", "multipart/form-data")); // HTTP リクエストする(POST メソッド) UnityWebRequest を呼び出し // 第 2 引数で上記のフォームデータを割り当てて multipart/form-data のデータとして送ります string urlWhisperAPI = "https://api.openai.com/v1/audio/transcriptions"; UnityWebRequest request = UnityWebRequest.Post(urlWhisperAPI, formData); // OpenAI 認証は Authorization ヘッダーで Bearer のあとに API トークンを入れる request.SetRequestHeader("Authorization", $"Bearer {OpenAIAPIKey}"); // ダウンロード(サーバ→Unity)のハンドラを作成 request.downloadHandler = new DownloadHandlerBuffer(); Debug.Log("WhisperAPI リクエスト開始"); // テキストに反映 GameObject.Find("TextResult").GetComponent<TextMeshPro>().SetText($"WhisperAPI リクエスト開始"); // リクエスト開始 yield return request.SendWebRequest(); // 結果によって分岐 switch (request.result) { case UnityWebRequest.Result.InProgress: Debug.Log("WhisperAPI リクエスト中"); break; case UnityWebRequest.Result.ProtocolError: Debug.Log("ProtocolError"); Debug.Log(request.responseCode); Debug.Log(request.error); break; case UnityWebRequest.Result.ConnectionError: Debug.Log("ConnectionError"); break; case UnityWebRequest.Result.Success: Debug.Log("WhisperAPI リクエスト成功"); // コンソールに表示 Debug.Log($"responseData: {request.downloadHandler.text}"); WhisperAPIResponseData resultResponseWhisperAPI = JsonUtility.FromJson<WhisperAPIResponseData>(request.downloadHandler.text); // テキストにお知らせ GameObject.Find("TextResult").GetComponent<TextMeshPro>().SetText($"自分 : {resultResponseWhisperAPI.text}"); // テキストが起こせたら ChatGPT API に聞く StartCoroutine(PostChatGPT(resultResponseWhisperAPI.text)); break; } } // ChatGPT API IEnumerator PostChatGPT(string text) { // HTTP リクエストする(POST メソッド) UnityWebRequest を呼び出し // リクエスト仕様 : https://platform.openai.com/docs/guides/gpt/chat-completions-api // API仕様 : https://platform.openai.com/docs/api-reference/completions/object UnityWebRequest request = new UnityWebRequest("https://api.openai.com/v1/chat/completions", "POST"); RequestData requestData = new RequestData(); // データを設定 requestData.model = "gpt-3.5-turbo-0613"; RequestDataMessages currentMessage = new RequestDataMessages(); // ロールは user currentMessage.role = "user"; // 実際の質問 currentMessage.content = text; List<RequestDataMessages> currentMessages = new List<RequestDataMessages>(); currentMessages.Add(currentMessage); requestData.messages = currentMessages; Debug.Log($"currentMessages[0].content : {currentMessages[0].content}"); // 送信データを JsonUtility.ToJson で JSON 文字列を作成 // RequestData, RequestDataMessages の構造に基づいて変換してくれる string strJSON = JsonUtility.ToJson(requestData); Debug.Log($"strJSON : {strJSON}"); // 送信データを Encoding.UTF8.GetBytes で byte データ化 byte[] bodyRaw = Encoding.UTF8.GetBytes(strJSON); // アップロード(Unity→サーバ)のハンドラを作成 request.uploadHandler = new UploadHandlerRaw(bodyRaw); // ダウンロード(サーバ→Unity)のハンドラを作成 request.downloadHandler = new DownloadHandlerBuffer(); // JSON で送ると HTTP ヘッダーで宣言する request.SetRequestHeader("Content-Type", "application/json"); // ChatGPT 用の認証を伝える設定 request.SetRequestHeader("Authorization", $"Bearer {OpenAIAPIKey}"); // リクエスト開始 yield return request.SendWebRequest(); Debug.Log("ChatGPT リクエスト..."); // 結果によって分岐 switch (request.result) { case UnityWebRequest.Result.InProgress: Debug.Log("ChatGPT リクエスト中"); break; case UnityWebRequest.Result.ProtocolError: Debug.Log("ProtocolError"); Debug.Log(request.responseCode); Debug.Log(request.error); break; case UnityWebRequest.Result.ConnectionError: Debug.Log("ConnectionError"); break; case UnityWebRequest.Result.Success: Debug.Log("ChatGPT リクエスト成功"); // コンソールに表示 Debug.Log($"responseData: {request.downloadHandler.text}"); ResponseData resultResponse = JsonUtility.FromJson<ResponseData>(request.downloadHandler.text); // 返答 Debug.Log($"resultResponse.choices[0].message : {resultResponse.choices[0].message.content}"); // テキストに反映 GameObject.Find("TextResult").GetComponent<TextMeshPro>().SetText($"自分 : {text}\nChatGPT : {resultResponse.choices[0].message.content}"); break; } } }
ChatGPT の API キーを設定
Cube に CubeEvent.cs 設定できたら、以下の対応をします。
// OpenAIAPIKey // WhisperAPI と ChatGPTAPI で共通 string OpenAIAPIKey = "OpenAIAPIKey";
こちらで WhisperAPI と ChatGPTAPI で共通の API キーを設定します。
動かしてみる
ここまで設定できたら、XREAL Air に書き込んでみて MR モードでアプリを起動します。
まず、マイク捜索が成功します。
マイク捜索ができたら、Cube をクリックして話してしゃべります。Cube をもう一度クリックして録音を終了します。
Whisper API にリクエストして文字起こしをします。
うまく文字起こしができました。
そして、ChatGPT API に質問として送られて返答が返ってきます。
結果テキストで表示させると、こういうデバイス確認はやりやすいですね。