Unity からマイク録音して音データを WAV フォーマットで Whisper API に送って文字起こしするメモです。
Whisper API は multipart/form-data でデータを送る
こちらの Unity からマイク録音して音データを WAV フォーマットに保存する ことができたので、あとは OpenAI Whisper API に送ります。
API Reference – OpenAI API の Create transcription にあるように multipart/form-data で送る仕様なので Unity から LINE Notify にスクリーンショットの画像メッセージを送る 知見を使えば multipart/form-data で WAV フォーマットのバイト列データをうまく送ることができました。
このあたり地道に周辺の知見をじっくり固めていたので、トラブルなく送ることができてうれしかったです。
Unity で Cube を準備
Unity で以下のように Cube を準備します。ほか EventSystem やカメラに Physics Raycaster を仕込んでいる状態で、クリックすれば動作する状態です。
プログラム
Cube に以下のように CubeEvent.cs を割り当てます。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Networking; using System; using System.IO; using System.Text; public class CubeEvent : MonoBehaviour, IPointerClickHandler { // マイクの開始・終了管理 bool flagMicRecordStart = false; // マイクデバイスがキャッチできたかどうか bool catchedMicDevice = false; // 現在録音するマイクデバイス名 string currentRecordingMicDeviceName = "null"; // PC の録音のターゲットになるマイクデバイス名 // これはお使いのデバイスで変わります // 完全一致でないと受け取れないので注意 string recordingTargetMicDeviceName = "Krisp Microphone (Krisp Audio)"; // ヘッダーサイズ 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 string OpenAIAPIKey = "apiKey"; 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; } } if (catchedMicDevice) { Debug.Log($"マイク捜索成功"); Debug.Log($"currentRecordingMicDeviceName : {currentRecordingMicDeviceName}"); } else { Debug.Log($"マイク捜索失敗"); } } void Update() { } 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}"); /* // 検証用にファイル保存 // Assets/record.wav に保存されます string pathSaveWav = Path.Combine(Application.dataPath, "record.wav"); // using を使ってメモリ開放を自動で行う using (FileStream currentFileStream = new FileStream(pathSaveWav, FileMode.Create)) { currentFileStream.Write(dataWav, 0, dataWav.Length); Debug.Log($"保存完了 path : {pathSaveWav}"); } */ StartCoroutine(PostAPI()); } } public void OnPointerClick(PointerEventData eventData) { if (catchedMicDevice) { if (flagMicRecordStart) { // Stop flagMicRecordStart = false; Debug.Log($"Mic Record Stop"); RecordStop(); } else { // Start flagMicRecordStart = true; Debug.Log($"Mic Record Start"); RecordStart(); } } } IEnumerator PostAPI() { // 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")); // 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("リクエスト開始"); // リクエスト開始 yield return request.SendWebRequest(); // 結果によって分岐 switch (request.result) { case UnityWebRequest.Result.InProgress: Debug.Log("リクエスト中"); 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("リクエスト成功"); // コンソールに表示 Debug.Log($"responseData: {request.downloadHandler.text}"); break; } } }
今回 Unity からマイク録音して音データを WAV フォーマットに保存するメモ から加わったのは PostAPI の部分です。
// 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")); // 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}");
このあたりで Whisper API に対してデータを送っています。
formData.Add(new MultipartFormDataSection("model", "whisper-1"));
によってWhisper モデルを使うことを API に伝え formData.Add(new MultipartFormFileSection("file", dataWav, "whisper01.wav", "multipart/form-data"));
でWAV データを伝えています。dataWav は WAV フォーマットのバイト列でそのまま入れておけば、MultipartFormFileSection がうまくやってくれます(ありがたい)。
// OpenAIAPIKey string OpenAIAPIKey = "apiKey";
ここで OpenAI の API キーを入力すれば、
// OpenAI 認証は Authorization ヘッダーで Bearer のあとに API トークンを入れる request.SetRequestHeader("Authorization", $"Bearer {OpenAIAPIKey}");
で、API の仕様に合わせて Authorization ヘッダーで Bearer のあとに API トークンを入れて認証が通ります。
動かしてみる
ここまで設定できたら、Play ボタンを押して動かしてみます。
マイク捜索ができたら、Cube をクリックして話して今回のマイクデバイスに向かってしゃべります。Cube をもう一度クリックして録音を終了します。
そして録音が停止すると WAV データを作成して Whisper API にリクエストして文字お越しされたデータが返ってきます。今回は英語で I’m testing now it. と言ったのがうまく返ってきています!