XREAL Air からマイク録音して音データを WAV フォーマットで OpenAI Whisper API に送って文字起こしするメモ

XREAL Air からマイク録音して音データを WAV フォーマットで OpenAI Whisper API に送って文字起こしするメモです。
Unity 単体で Whisper API 連携できているとシンプルにつながる
https://www.1ft-seabass.jp/memo/2023/09/17/unity-with-whisper-api/
こちらの記事で PC の Unity からマイク録音して音データを WAV フォーマットで OpenAI Whisper API に送って文字起こしすることはできています。
- NRSDK を Unity プロジェクトに入れて一通り準備しておく
- XREAL Air は XREAL Light 違って Head Tracking が 3DoF だったりセンサー系に制限があるので意識しておく
このあたりの XREAL Air で動かす準備をしておけば、上記の仕組みが動く Unity プロジェクトで作業を進めることができます。Cube をクリックして録音して、それを WAV フォーマットに変換して、OpenAI Whisper API に送って文字起こしする仕組みはそのまま動きます。
具体的には、
- XREAL Air の録音のターゲットになるマイクデバイス名 Android audio input を狙う
- Plugins/Android/AndroidManifest.xml で RECORD_AUDIO 権限を解放
あたりの対応を加えれば、無事に動きました。
RECORD_AUDIO 権限を解放
Plugins/Android/AndroidManifest.xml で RECORD_AUDIO 権限を解放します。
実際の AndroidManifest.xml です。他の作業もしているので、余計な権限入ってるかもですが、ひとまず RECORD_AUDIO が加わってます。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.unity3d.player" xmlns:tools="http://schemas.android.com/tools" android:installLocation="preferExternal">
<uses-sdk tools:overrideLibrary="com.nreal.glasses_sdk" />
<supports-screens android:smallScreens="true" android:normalScreens="true" android:largeScreens="true" android:xlargeScreens="true" android:anyDensity="true" />
<application android:theme="@style/UnityThemeSelector" android:icon="@mipmap/app_icon" android:label="@string/app_name">
<activity android:name="com.unity3d.player.UnityPlayerActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data android:name="nreal_sdk" android:value="true" />
<meta-data android:name="com.nreal.supportDevices" android:value="NrealLight|NrealAir" />
</application>
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>
Unity で Cube を準備
Unity で以下のように Cube を準備します。ほか EventSystem やカメラに Physics Raycaster を仕込んでいる状態で、クリックすれば動作する状態です。
XREAL Air では EventSystem まわりを実装しておけば、スマホのコントローラーでポインタを当ててタップすると、クリック動作として受け取ってくれます。
プログラム
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)";
// 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
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;
}
// XREAL Air 用のマイクデバイスを割り当て
if (device == recordingTargetMicDeviceNameForXREALAir)
{
Debug.Log($"{recordingTargetMicDeviceNameForXREALAir} serched");
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"));
// 日本語で返答
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("リクエスト開始");
// リクエスト開始
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;
}
}
}
実装として加えた部分です。
// XREAL Air の録音のターゲットになるマイクデバイス名 "Android audio input"
string recordingTargetMicDeviceNameForXREALAir = "Android audio input";
こちらでマイクデバイス名を用意して、
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}");
}
else
{
Debug.Log($"マイク捜索失敗");
}
}
Launch の部分でマイク捜索していて、デバッグしやすいように PC でのデバイスが見つかった処理と XREAL Air のマイクデバイス Android audio input が見つかった処理を並列に書いています。これで PC でも XREAL Air でも両方でマイクの検証が行えます。
// 日本語で返答
formData.Add(new MultipartFormDataSection("language", "ja"));
Whisper API の部分は、日本語で返答するよう language パラメータを ja と伝えて日本語に返すようにお願いしています。
動かしてみる
というわけで XREAL Air で動かした様子です。Debug.Log をログで出す部分は追加実装してます。
実際に Cube をクリックして録音して、それを WAV フォーマットに変換して、OpenAI Whisper API に送って文字起こしこのように日本語で返答を返してくれます。
さらに実装した様子
できたー!XREAL Air のマイクで音声録音したデータを OpenAI API の Whisper API に送って日本語で文字起こしして受け取れました~。これでいろいろな仕組みにつなげられるます! #AR #XREAL pic.twitter.com/Dr5avY1z7b
— Tanaka Seigo (@1ft_seabass) September 12, 2023