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?

OpenAI Realtime API互換サーバーとローカルLLM で Android Automotive OS の音声アシスタントを動かす

0
Last updated at Posted at 2026-08-13

はじめに

前回の記事では、OpenAI Realtime API(WebRTC)を使ってGoogleアシスタントを置き換えるVIA(VoiceInteractionService実装)を作る方法を確認した。

クライアントのサンプルアプリ(VoiceInteractionAppSample)は OpenAIのRealtime APIサーバーにWebRTCで接続し、[発話] → [STT] → [LLM] → [TTS] → [再生] の一連の流れを実現していた。

今回は、OpenAIが提供するバックエンドの代わりに、 Mac上でセルフホストする OpenAI Realtime API互換サーバー (STT・LLM・TTS) へ接続 する。Android Automotive側は音声の入出力と通信を担い、推論はMac側で実行する。

接続先ホスト名をapi.openai.comから OpenAI Realtime API互換サーバー を起動した ローカルMac のアドレスに変更し、設定画面でLOCALモードを選ぶだけで同じクライアントが動作する。

動作確認

今回利用した OpenAI Realtime API互換サーバー のリポジトリ

サンプルのクライアントアプリのリポジトリ

OpenAI Realtime API互換サーバの実現方法

WebRTCサーバーを自作する代わりに、OpenAI Realtime API互換サーバー
として開発されている huggingface/speech-to-speech--mode realtimeをそのまま使用した。

特徴は次の通り

  • WebRTC実装がOpenAI Realtime APIを模倣する設計になっている。データチャネル名oai-events、エンドポイントPOST /v1/realtime/callsが互換
  • session.updateのパースにOpenAI公式Python SDKの型定義(openai.types.realtime.*)をそのまま使用。クライアントが送るsession.audio.input.turn_detectionのネスト形式もそのまま利用可能
  • STT/LLM/TTSはCLIフラグの値で実装が選択可能。バックエンドの差し替えはCLIフラグの変更で実現

動作確認できたのは VoiceInteractionAppSample が使うSDP交換、oai-eventsデータチャネル、session.update、音声入出力、function callingイベントの範囲のみであり、Realtime API全体への互換をチェックしたわけではなく、未対応イベント・エラー応答・複数セッション同時接続などは検証していない。

OpenAI Realtime API互換を模倣するために、セッション認証情報を返すエンドポイントが必要だが、れはspeech-to-speechの範囲外になる。そこで、ローカル検証用に broker.pyという代替を用意した。speech-to-speechのrealtimeサーバー自体は認証チェックをしないため、clientSecretはダミー値を返すだけのHTTPサーバーである。

パイプライン構成とモデル選定

サーバー内部は VAD → STT → LLM → TTS の直列パイプラインで、VAD以外の各段はCLIフラグの値で実装を差し替えられる。今回選んだ実体は次の通り

差し替え 選定
VAD 不可 Silero VAD v5 + Smart Turn v3.2
STT mlx-audio-whisper(whisper-large-v3-turbo, MLX、日本語対応)
LLM mlx-lmmlx-community/Qwen3.5-9B-4bit
TTS qwen3(Qwen3-TTS-1.7B-CustomVoice, bf16, mlx-audio, speaker=ono_anna

STTはデフォルトのParakeet-TDTが日本語非対応(--parakeet_tdt_languageのhelpに「25 European languages」と明記されている)なため、mlx-audio-whisperに変更した。

LLMはQwen3-4Bから始めたが、Tool Calling に問題が確認されたため Qwen3.5-9B-4bit に変更している。

TTSの話者はデフォルトのaiden(低めの男性声)から、日本語向けの ono_anna に変更し、量子化も 6bit→bf16 に上げて音質改善を試みた。

バックエンドの切り替え

speech-to-speech 0.2.12では、--stt--llm_backend--ttsの値に応じて各処理の実装が選択される。

今回の構成では、STTにmlx-audio-whisper、LLMにmlx-lm、TTSにqwen3を指定した。

Mac (Apple Silicon) 対応

speech-to-speechには MacOS 向けの実装が既に組み込まれており、推論バックエンドとして MLX を利用可能

セットアップと起動

uvでPythonバージョンと依存関係をロックしている。uv syncコマンドで全依存が.venvにインストールされる。

uv sync

起動はMakefileにまとめてある。

make setup   # uv sync + broker.py(:8787) + speech-to-speech(:8765) を起動、:8765の応答を待つ
make down    # 両方停止
make status  # ヘルスチェック
make logs    # 両方のログをtail

make setupは起動したPIDを.run/に記録し、既に起動中なら再起動せず現在のPIDを表示する。中身は次のコマンドと等価。

uv run python broker.py > .run/broker.log 2>&1 &

uv run speech-to-speech \
  --mode realtime \
  --device mps \
  --stt mlx-audio-whisper \
  --language ja \
  --no_enable_live_transcription \
  --llm_backend mlx-lm \
  --model_name mlx-community/Qwen3.5-9B-4bit \
  --tts qwen3 \
  --qwen3_tts_model_name Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
  --qwen3_tts_speaker ono_anna \
  --qwen3_tts_mlx_quantization bf16 \
  --qwen3_tts_non_streaming_mode True \
  --ws_host 127.0.0.1 --ws_port 8765

STT実装の速度比較

現行採用のmlx-audio-whisper以外に、speech-to-speechが対応する--stt whisper-mlx(実体はlightning-whisper-mlxパッケージ)と、MLXを使わないwhisper.cpp(Homebrew whisper-cppwhisper-cli)を含めた3方式で日本語音声の文字起こし速度を計測した。

計測条件: 短尺はmacOS say -v Kyokoで合成した6.4秒の日本語音声、長尺は手元のYouTube文字起こし系動画音声から切り出した180秒クリップ。各実装は逐次実行(同時実行するとMetalのGPUを取り合い計測が不正確になるため)、各条件1回のみの計測で、複数回実行した分散は取っていない。loadをモデルロード時間、inferを推論時間として分離して計測した。実運用のサーバーはモデルを起動時に一度だけロードして常駐させるため、毎ターン効くのはinferのみになる。

3実装で揃えたのは音声入力とタスク(日本語文字起こし)のみで、モデルは揃っていない。mlx-audio-whisperwhisper.cpplarge-v3-turbolightning-whisper-mlxlarge-v3で、後者はturbo版ではない分だけ元々不利な条件になる。実装単体の速度比較ではなく、「今回試した組み合わせでの結果」として見る必要がある。

stt-speed-comparison.png

実装 モデル load (6.4s) infer (6.4s) infer (180s)
mlx-audio-whisper whisper-large-v3-turbo 3.51s 0.74s 8.18s
whisper.cpp (whisper-cli) large-v3-turbo (ggml) 0.41s 0.90s 10.9s
lightning-whisper-mlx large-v3 0.53s 1.57s 28.3s

今回のMac・このモデル・この音声・各1回の計測では、inferだけで見るとmlx-audio-whisperが短尺・長尺とも最短だった。これが現行構成を維持している根拠になっている。whisper.cppが「合計」で勝って見えたのは、CLIを1発叩くたびにモデルを再ロードするコスト(0.4秒程度)を含んでいたためで、常駐サーバーの実態とは前提が異なる比較だった。lightning-whisper-mlxは短尺では悪くないが、180秒の長尺クリップでinferが28秒(リアルタイム比0.16倍)まで悪化した。batch_size=6前提の実装であることは確認したが、長尺での悪化との因果関係は未特定。今回のサーバーはVAD区切りの短い発話単位でSTTを呼ぶ構成のため実害は小さいと見られるが、積極的に採用する理由もない。

なおlightning-whisper-mlx--stt_model_name未指定時のデフォルトdistil-whisper/distil-large-v3が英語専用モデルで、日本語音声を渡すと英訳もどきの誤認識になる。日本語で使うには--stt_model_name large-v3(多言語モデル)の明示が必要だった。

既知の制約

  • ローカルTTS(Qwen3-TTS)はOpenAI TTSと聴感上の音質差がある、という印象を持った。定量評価はしていない主観的な聞き比べで、評価者・比較手順は記録していない。量子化を6bit→bf16に上げる改善は試したが、それでも差は残っている。speech-to-speechが対応する他の内蔵TTS(chatTTS/facebookMMS/pocket/kokoro)は未試験
  • speech-to-speech 0.2.12--stt mlx-audio-whisperとこのVAD設定の組み合わせでは、--enable_live_transcription(デフォルトtrue)でSTT精度が崩れる事象を確認した。VADが発話の完了を確認する前の最初の約0.88秒だけでSTTを実行しそれを最終結果として確定してしまい、以降の音声を「stale」として破棄する。--no_enable_live_transcriptionを付けるとVADのSpeech soft-endedSmart Turn: completeを待ってから一度だけSTTが走るようになり、正しく認識された
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?