0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【VisionPro】Groq を用いてAppleVisionPro でSpeechToText を実装した話【Groq】

Posted at

Graffity でUnityエンジニアをしているcovaです。

今回はR&DチームでAppleVisionPro にてSpeechToText (音声の文字起こし)を行うためにLLMベースのAIであるGroq を用いて実装してみました。

実装環境

項目 version
Unity 6000.0.47f1
visionOS 2.4
PolySpatial 2.1.2
Unity-Groq v0.24.0

STEP1. Groq について

Groq は groq 社が提供しているLLMベースのAI基盤です

基本無料でAPI key を作成でき、PythonSDK とREST APIを用意してあります。

groq 側の準備

STEP2. Unity でGroq を利用するには

Unityではcurlの代わりにUnityWebRequestを使えばいいのですが、ヘッダの設定やら色々行わないとうまく疎通できません。
そこで今回はGroq のAPIを簡単に呼べる Unity-Groq を使って実装を簡略化しました。

(手前味噌ですが、弊社Graffityから出しているOSSになります)

導入自体は日本語DocumentのInstallation通りに行います。

Groq のSpeechToText API

groq 側は入力として .wavファイルを渡せばレスポンスとして文字起こし後のテキストを返却してくれます。

Graffity.Groq.Speech.Transcription class がSpeechToText のAPIラッパーになっているので
こちらを使います。

引数にはwavファイルのファイルパスとAPIKey を設定すればいいので、あとはこのwavファイル本体とファイルパスを用意できれば問題なさそうです

AppleVisionPro のマイク入力

入力の取り方

AppleVisionPro でUnityにおいてマイク入力を受け取るにはMicrophone コンポーネントで行えます。

AvpRecordingDemo.cs

public class AvpRecordingDemo : MonoBehaviour
{
    /// <summary> サンプリングレート。ヘルツ </summary>
    private const int SampleRate = 48000;

    /// <summary> 生成WAVの既定ファイル名 </summary>
    public const string DefaultWavName = "avp_record.wav";

    /// <summary> 出力ビット深度 </summary>
    private const int BitsPerSample = 16;

    /// <summary> モノラルチャンネル数 </summary>
    private const int ChannelCount = 1;


    /// <summary> 録音秒数 </summary>
    [SerializeField]
    private float _recordSeconds = 10f;

    private async UniTask RecordAndTranscribeAsync(CancellationToken token)
    {
        // 0番目のマイクを取得
        var device = Microphone.devices.Length > 0 ? Microphone.devices[0] : null;
        if (string.IsNullOrEmpty(device))
        {
            Debug.LogError("マイクが見つかりません");
            return;
        }
        
        try
        {
            // 録音開始
            var audioClip = Microphone.Start(device, false, Mathf.CeilToInt(_recordSeconds), SampleRate);
            await UniTask.WaitUntil(() => !Microphone.IsRecording(device), cancellationToken: token);

            var wavByteArray = ConvertToWavByteArray(audioClip);
            var savedWavPath = Path.Combine(Application.persistentDataPath, DefaultWavName);

            
            await File.WriteAllBytesAsync(savedWavPath, wavByteArray, cancellationToken:token);
        }
        catch (Exception e)
        {
            Debug.LogError($"WAV保存失敗: {e.Message}");
        }

    }

	/// <summary>
    /// AudioClipを16-bit PCM WAVのバイト配列に変換
    /// </summary>
    private byte[] ConvertToWavByteArray(AudioClip clip)
    {
        Debug.Log($"clip.samples: {clip.samples}, clip.channels: {clip.channels}");

        var samples = new float[clip.samples * clip.channels];
        clip.GetData(samples, 0);

        var pcm = new byte[samples.Length * BytesPerSample];
        for (int i = 0; i < samples.Length; i++)
        {
            // 音声振幅を short に変換(-1.0f~+1.0f → -32768~+32767)
            var s = (short)Mathf.Clamp(samples[i] * Pcm16MaxAmplitude, short.MinValue, short.MaxValue);
            // リトルエンディアン形式で 2バイトに分割して格納(LSB → MSB)
            pcm[i * 2] = (byte)(s & 0xFF);              // 下位の8bit
            pcm[i * 2 + 1] = (byte)((s >> 8) & 0xFF);   // 上位の8bit
        }

        using var ms = new MemoryStream();
        WriteWavHeader(ms, ChannelCount, SampleRate, pcm.Length);
        ms.Write(pcm, 0, pcm.Length);

        var wav = ms.ToArray();
        Debug.Log($"生成したWAVバイト数: {wav.Length}");
        return wav;
    }

    /// <summary>
    /// WAV ヘッダー (RIFF/WAVE) をストリームに書き込む
    /// </summary>
    private void WriteWavHeader(Stream stream, int channels, int sampleRate, int dataLength)
    {
        // 1秒あたりのバイト数
        var byteRate = sampleRate * channels * BytesPerSample;
        // 1フレームのバイト数
        var blockAlign = channels * BytesPerSample;
        using var writer = new BinaryWriter(stream, Encoding.UTF8, true);

        writer.Write(Encoding.UTF8.GetBytes("RIFF"));
        writer.Write(RiffHeaderSize + dataLength);
        writer.Write(Encoding.UTF8.GetBytes("WAVE"));
        writer.Write(Encoding.UTF8.GetBytes("fmt "));
        writer.Write(Subchunk1Size);
        writer.Write((short)AudioFormatPcm);
        writer.Write((short)channels);
        writer.Write(sampleRate);
        writer.Write(byteRate);
        writer.Write((short)blockAlign);
        writer.Write((short)BitsPerSample);

        writer.Write(Encoding.UTF8.GetBytes("data"));
        writer.Write(dataLength);
    }

こんな感じでWavファイルをApplication.persistentDataPath に出力できるScriptを用意します。

Apple Vision Pro 起動直後はマイク入力をうまくとれない場合があるため初回実行は起動直後から数秒待機した後の方が安定します

Groq APIに渡す時は

上のコードだと Path.Combine(Application.persistentDataPath, AvpRecordingDemo.DefaultWavName); に保存しているので、
Transcription インスタンスの SetFilePath に↑のPathを渡してあげればOKです

最終出力

うまくいくと冒頭のようなSpeechToText が行えます

FAQ

Apple なら Apple Intelligence があるからそれでいいのでは?

A. 現状Unity側がすんなり使えるように環境は整備されていません。
そのため、自作でNativePlugin を書かない限りはUnityで使えないため、今回はREST APIで簡単に扱えるGroqを採用しました。

最後に

Graffity ではAppleVisionPro やMeta Quest3 を用いたAR/MRコンテンツを鋭意製作中ですので、開発に興味のある方は是非ご連絡ください

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?