はじめに
Android Automotive OS で Google Assistant を置き換える音声アシスタント(Android では「Voice Interaction App」、略してVIAと呼ぶ)を、ネットワーク接続なしで動かした。マイクへの話しかけから応答の生成、音声での返答まで、すべて端末内で完結する。
今回の音声アシスタントサンプルの特徴は次の通り
- ネットワーク接続必要なし (Android内部で完結)
- Google Assistant の置き換えに対応 (システムのマイクボタンから起動できる)
- 他のアプリをIntentで前面に移動可能
- エコーキャンセラ実装のないAndroid環境(AOSP)でもエコーキャンセラ対応
- バージイン対応
- ブラウザを起動して YouTube でコンテンツ検索可能
「YouTubeでマイケル・ジャクソンの動画を検索して」と話しかけて、実際に検索が実行されるところを下記の動画で確認できる。
このアシスタントは、(1)クラウドのOpenAI Realtime APIを使う版 → (2)自前サーバーのローカルLLM(応答を考える処理)を使う版 → (3)今回の完全オンデバイス版、という3段階を経て作った。前の2段階の記事は次の章で簡潔に触れる。
これまでの道のり
ここまでに3つの記事を用意した
1. OpenAIのLLMをバックエンドに採用した音声アシスタント
「Android Automotive OSのGoogle Assistantを置き換える独自の音声アシスタントを作成する方法」では、OpenAI Realtime APIをWebRTC経由で接続し、音声での会話とYouTube検索のFunction calling(LLMが必要に応じてアプリ内の機能を呼び出す仕組み)を実装した
2. ローカルLLMをバックエンドに採用した音声アシスタント
「OpenAI Realtime API互換サーバーとローカルLLMでAndroid Automotive OSの音声アシスタントを動かす」では、クラウドのOpenAIをやめ、Mac上に自前でOpenAI Realtime API互換のサーバーを立て、ローカルLLMで応答を生成するようにした。
3. ネットワーク接続なしで実現する音声アシスタントのためのライブラリ
「ネットワーク接続なしで動作する音声アシスタントを作成するためのライブラリを作った」では、今回使うエコーキャンセラ(スピーカーの音がマイクに回り込むのを打ち消す処理)のライブラリ(local_audio)を作った。
今回はこの3本を土台に、ネットワークを一切使わない音声アシスタントを完成させる
今回のデモ
今回のデモでは、「YouTubeでマイケル・ジャクソンの動画を検索して」と話しかけると、音声認識・応答生成・YouTube検索の実行までが進む。
使っているのはLocal Agentモードで、APIキーもネットワーク接続も要らない設計である。マイクからスピーカーまでの処理は、端末内のSTT(声を文字に変換する処理)・LLM(応答を考える処理)・TTS(文字を声に変換する処理)と、前回記事のエコーキャンセラライブラリだけで完結する。
アーキテクチャ:バックエンドを差し替えられる作り
VoiceInteractionAppSampleは7つのモジュールに分かれている。
viaがAndroid Automotive OSとの接点(CarVoiceInteractionSession)で、sessionが会話の状態管理(ConversationController)を受け持つ。この2つの下にVoiceSessionControllerというインターフェースがあり、ここで会話の相手(バックエンド)を差し替えられるようになっている。
差し替え先は2つある。前々回・前回の記事で使ったrealtime(OpenAI Realtime APIへWebRTCで接続する)と、今回追加されたlocalagent(完全オンデバイス)である。どちらも最終的にはtoolsモジュールを使ってYouTube検索などのFunction呼び出しを実行する。diagnosticsはログや診断を横断的に担当する。
3つの実装、実際は2つのコードパス
3段階と説明してきたが、コードの実体は2つ
前々回(クラウドのOpenAI)と前回(自前サーバー)は、どちらもrealtimeモジュールをそのまま使っている。違うのは接続先のホスト名だけで、WebRTCで接続する処理自体は同じコードが動いている。
今回の完全オンデバイス版だけがlocalagentという別モジュールである。WebRTCも、OpenAI Realtime APIの仕組みも一切使わない、独立した音声パイプラインである。
barge-inをどこで・どうやって判定しているか
AIが話している最中に人間が話しかけて割り込む(barge-in)処理は、この2つのモジュールで判定方法がまったく違う。
Realtime系(前々回・前回)
マイクの音声は、AIが話している間もWebRTCでサーバーへ送られ続ける。音声を検知するVAD(声が出ている区間を判定する処理)はサーバー側にあり、threshold=0.5・silenceDurationMs=500msといった設定値をもとに判定する。検知するとinterrupt_responseというイベントがDataChannel(WebRTCの接続上でテキストデータをやり取りする経路)経由でクライアントに送られ、それを受けてTTSの再生を止める。判定がサーバー側にあるため、マイクからサーバー、サーバーからクライアントへとネットワークを往復する。
Local Agent系(今回)
こちらはVADも判定もすべて端末内で完結する。マイクの音声は前回記事のlocal_audioを通り、TTSの出力を参照信号として使ってエコーが取り除かれる。そのうえでSilero VAD(端末上で動く音声区間検出)が50ms周期で音声の有無を判定し、TurnLogicというロジックが4回連続で陽性(およそ200ms持続)と判定した時点でbarge-in成立とみなす。成立するとttsPlayer.cancel()が同じプロセス内で呼ばれ、通信を挟まずにその場で再生が止まる。AVD(Android仮想デバイス)での実測では、35ms以内に応答が中断していることを確認している。
瞬間的なノイズで誤って割り込み判定にならないよう、4回連続という条件のほかに、3文字未満の発話は認識結果として採用しない、というフィルタも入っている。
両者の違いをまとめると次のようになる。
| Realtime系 | Local Agent系 | |
|---|---|---|
| VADの実行場所 | サーバー側 | 端末内(Silero VAD) |
| 判定から停止までの経路 | ネットワークを往復 | 同一プロセス内 |
| AudioSource | VOICE_COMMUNICATION | VOICE_RECOGNITION |
| エコーキャンセラ | AUTO/HARDWARE/WEBRTCの3方式を選択可 | local_audio(AEC3)固定 |
オンデバイスで動くSTT・LLM・TTSの技術スタック
Local Agentモードが端末内で動かしているのは、STT・LLM・TTS、それにエコーキャンセラの4つである。エコーキャンセラ部分(local_audio)は前回記事の内容そのままなので、ここでは残り3つのフレームワークとモデルについて書く。
LLMにはLiteRT-LM(Googleが提供する、スマホなどの端末上でLLMを動かすための実行環境)を使い、モデルはGemma 4 E2B(gemma-4-E2B-it.litertlmというファイル)を動かしている。EngineConfigでモデルの場所を指定し、Backend.CPU()で実行環境を設定したうえでEngine.initialize()を呼ぶと、数秒から10秒ほどでモデルが読み込まれる。応答の生成(ask())は呼び出し元のスレッドをブロックする作りのため、専用のワーカースレッドから呼び出している。
STTとTTSにはsherpa-onnx(音声認識・音声合成を端末上で動かすための実行環境)を使っている。STTはSenseVoice、TTSはsupertonic-3-jaという日本語モデルの組み合わせで、どちらも前回記事のandroid-local-voice-agentで検証したものと同じ構成である。
ライセンスとフレームワーク
ここで使っているフレームワーク・モデルのライセンスは下記の通り
| 対象 | ライセンス | 商用利用 |
|---|---|---|
| LiteRT-LM | Apache-2.0 | 可 |
| sherpa-onnx(実行環境) | Apache-2.0 | 可 |
| Silero VAD v5 | MIT | 可 |
| SenseVoice(コード) | MIT | 可 |
| SenseVoice(モデル重み) | FunASR Model License | 可(要attribution) |
| Gemma 4 E2B | Gemma Terms of Use / Prohibited Use Policy | 可(条件あり) |
| supertonic-3-ja | OpenRAIL-M | 可(使用制限あり) |
LiteRT-LM・sherpa-onnx・Silero VADは、商用利用や再配布に制限のないライセンスである。
SenseVoiceは少しややこしい。リポジトリのソースコードはMITだが、SenseVoiceSmallのモデル重み自体は別の「FunASR Model License」に従う。この点をIssue #286で開発チームのメンバーに直接確認したところ、「有償のデスクトップソフトにローカル音声認識として組み込む」という今回と同じ用途について、2026年5月に「commercial use is permitted」という回答が付いている。条件はFunASR/SenseVoiceへのattribution(利用元の表示)を行うことである。
GemmaとSupertonic-3-jaは、単純な商用利用の可否だけでは済まない。どちらも独自の使用制限(Gemma Prohibited Use Policy、OpenRAIL-MのAttachment A)を再配布先のユーザーにも継承させる義務があり、ライセンス文書そのものを同梱する必要がある。
今回はモデルの実体をアプリに同梱しているわけではなく、fetch_gemma.shなどのスクリプトでユーザー自身の端末に直接ダウンロードさせる形を取っている。この場合、モデルのコピーを保持・再配布しているのはアプリ側ではなくダウンロード元自身になる。
ただし取得元は3モデルで違う。Gemmaはhuggingface.co/litert-community/gemma-4-E2B-it-litert-lmというGoogle AI Edgeチームが管理する公式チャンネルから直接取得している。一方SenseVoiceとsupertonic-3-jaは、FunAudioLLMやSupertoneの配布元からではなく、k2-fsa/sherpa-onnxのGitHub Releasesが変換・再配布しているONNX版を取得している。
配布経路が変わっても、モデルを使う時点でその利用規約に同意したことになる点は変わらない。スクリプトでユーザー自身に取得させる形でも、Gemma利用規約やOpenRAIL-Mの使用制限は、モデルを使うアプリ側に適用される。
実行環境の準備:Emulatorとモデルの取得
Local Agentモードを使うには、まずRAMとストレージを増やしたAVDを用意しておく必要がある。作成手順は「Android Automotive OS向けEmulatorの作成方法」に記載した。RAMを8192MB(8GB)以上に増加させておくのが良い。
そのうえで、ビルド前に約3GBのモデルとバイナリを取得しておく。
-
fetch_gemma.sh— Gemma 4 E2B(2.6GB) -
fetch_stt_models.sh— SenseVoice -
fetch_supertonic.sh— supertonic-3-ja -
fetch_sherpa_onnx.sh— sherpa-onnxのAAR・ネイティブライブラリ -
fetch_local_audio_engine.sh— 前回記事のlocal_audioエンジン(.so)
これらのスクリプトはホスト側のmodels/にキャッシュしたうえで、adb経由で端末の/data/local/tmp/{llm,stt,tts}/へ配置する。Gemmaのダウンロードは初回だけ時間がかかる。
ストレージにも余裕が要る。/dataパーティションはconfig.iniで16GBと宣言しても、実測では10GB程度しか確保されないことがある。モデル一式を配置したあとの空き容量は、実測で4.9GB程度だった。
メモリの実測値も書いておく。AAOSが起動した直後、VIAアプリもLocal Agentも一切動かしていない状態で、システムだけで約1.7〜2.0GBを消費する。この状態でのAVDの空きRAMは6.2〜6.3GBほど。そこから会話セッションを開始すると、アプリのRSS(実際に使用中のメモリ)は約4GBまで増える。システムの約2GBと合わせると、RAM 8GBのAVDでも残りは2GB程度しかない。
Pixel Tablet (Tangorpro) 実機での実測値
X(VIA停止後)
MemTotal: 7,626,528 kB
MemAvailable: 3,524,492 kB
使用率 = (7,626,528 − 3,524,492) / 7,626,528 = 53.8%
Y(VIA稼働中・モデルロード済み、PID 17090)
MemTotal: 7,626,528 kB
MemAvailable: 575,188 kB
使用率 = (7,626,528 − 575,188) / 7,626,528 = 92.5%
差分(VIAの実効負荷)
| 使用率 | MemAvailable | |
|---|---|---|
| X(停止後) | 53.8% | 3,524,492 kB |
| Y(稼働中) | 92.5% | 575,188 kB |
| 差分 | +38.7 % | −2.81 GB |
VIAアプリ(Gemmaモデルロード込み)が端末全体に与えている実効負荷は約2.81GB
アプリを配布する前に解決が必要なこと
このサンプルをもとに実際のアプリを作るなら、少なくとも次の点を解決しないといけない。
1. モデルの配布方法
今はスクリプト実行とadb pushでモデルを端末に置いているが、Releaseアプリではこれは通用しない。アプリの初回起動時にダウンロードする仕組みと、約3GBという容量をユーザーにどう確保してもらうかの設計が要る。
2. ライセンスの解決
モデルのライセンスは前の章で確認済みだが、GemmaとSupertonic-3-jaの使用制限文書を実際にアプリへ同梱する場合などの調査や、その他のモデルやライブラリに置き換える場合にも慎重に確認する作業が必要になる。
3. 実車両での検証
AVD(エミュレータ)では全音量帯でbarge-inの誤検知0件、35ms以内の割り込みを確認できたが、これはシミュレータの数値でしかない。Pixel Tabletや実際の車載機(OEM HU)で確かめないと、Releaseの品質基準には届かない。「Simulate Insert Headset」をOFFにした環境で発話が検出されない問題も、原因を特定して直す必要がある。
4. Session Broker(認証まわり)の実装
OpenAI Realtime API を利用する場合には、認証まわりはRelease化にあたって解決すべき課題になる。
ソースコード
今回の記事のために作成したソースコードのリポジトリ




