こんにちは。
MLエンジニアのふるです。
今回は、Live APIの本番運用について、簡潔なサマリを書いていこうと思います。
「明日10時にミーティング入れて」──スマホに話しかけると、AIが「明日10時にミーティングを登録しますね」と答えながら予定を作成する。そんなアプリを Flutter × Gemini Live API で作り、本番運用しています。
この記事では、個人開発アプリ TalkMane(予定管理×音声AIコーチ)の実装を通じて得た、Gemini Live APIのリアルタイム音声通信の知見を共有します。
AIサマリー
作ったもの
TalkMane は「毎日の予定確認を音声で3分で済ませる」音声AIコーチアプリです。
- 音声で予定の確認・登録・更新・削除ができる
- AIが会話の中でFunction Callingを使い、カレンダーを直接操作する
- 9つのFunctionを実装(get_today_schedule, request_create_occurrence 等)
- Flutter(iOS/Android)+ Supabase + Gemini Live API
技術的には、WebSocketベースのリアルタイム双方向音声ストリーミングをFlutterで実装しています。音声入力→テキスト理解→音声出力→Function Callingが1本のWebSocket接続で動作します。
なぜGemini Live APIを選んだか
リアルタイム音声AIには、OpenAI Realtime APIという選択肢もあります。最大の差はコストです。
| Gemini Live API | OpenAI Realtime API | |
|---|---|---|
| 入力 | $0.30/1M tokens | $100/1M tokens |
| 出力 | $1.20/1M tokens | $200/1M tokens |
| フリーティア | あり(1500 RPD) | なし |
Geminiの入力トークン単価はOpenAIの約1/300。個人開発でリアルタイム音声を実装するなら、フリーティアのあるGeminiが現実的な選択肢でした。
一方で、Firebase AI Logic SDKは使わず Ephemeral Token + WebSocket直接接続 を選びました。WebSocketのライフサイクル制御、Function Callingの応答タイミング制御、PCM音声データの低レベル管理が必要だったためです。
アーキテクチャ
Flutter App
├── AudioRecorder (PCM 24kHz)
├── VoiceSessionNotifier (7状態管理, Riverpod)
├── AudioPlayer (PCM 24kHz)
└── GeminiLiveClient (WebSocket JSON Protocol)
│ wss://
▼
Gemini Live API (BidiGenerateContent)
Supabase Edge Function: issue-ephemeral-token
→ APIキーをサーバーに保持 → 10分有効のトークンを発行
音声セッション全体を 7つの状態(idle / connecting / connected / listening / processing / speaking / error)で管理しています。この状態設計に至るまでに数十のバグ修正を経ました。
本番運用で得た3つの驚き
1. WebSocket切断は3層防御が必要だった
disconnect() を呼んでも、既にキューに入ったコールバックは実行される。セッション終了後に音声が再生され続ける問題に、3コミットを経てたどり着いた解決策が「元栓パターン」「世代カウンタパターン」です。
_isEnding = true; // 第1層: 新規処理をブロック
final client = _client;
_client = null; // 第2層: 参照をnull化
final gen = _sessionGeneration; // 第3層: 世代カウンタで無効化
await client?.disconnect();
2. iOS AudioSessionには4つの罠がある
Live APIの実装が終わったと思いきや.....iOS依存の不具合に遭遇...。iOSの音声周りは「動くはず」が「動かない」の連続でした。これがネイティブ実装の難しさ。
-
AVAudioEngine起動クラッシュ →
setActive(true)のリトライが必要 -
録音後にAI音声が小さくなる →
playAndRecordがイヤピースにルーティングする仕様 -
サイレントスイッチ対応の警告バナーを実装 →
playAndRecordはサイレントスイッチを無視する仕様だった(14分後に削除) - 録音/再生の切り替えで遅延 → セッション開始時に1回だけ設定して固定
3. 手動VADを試みて4コミットで全て戻した
Gemini Live APIの自動VADを無効化して手動制御に切り替えたところ、4コミットを経て全て元に戻すことになりました。
コミット1: 自動VADを無効化、手動制御に切り替え
コミット2: audioStreamEndだけでなくturnCompleteも必要と判明
コミット3: 正しいシグナル(activityEnd)に修正
コミット4: Geminiが応答しない → 全て元に戻す
自動VAD + パラメータチューニング(prefixPaddingMs: 300, silenceDurationMs: 600, END_SENSITIVITY_LOW)が現時点で最も安定する構成ですが、もしかすると意外とマイクボタンを外しちゃっても良いのかもしれません....。
Zenn連載で深掘りしていきます
この記事で紹介した内容は、Zennで 全5編の連載 として詳しく解説していく予定です。
記事投稿時にリンクを貼り付けていくので、是非みていただけると幸いです。
- 第1編:WebSocket直接接続とEphemeral Tokenの設計 ── セットアップメッセージの全パラメータ解説
- 第2編:音声入出力の実装とiOS AudioSessionの4つの罠 ── PCM 24kHzパイプラインの実装
- 第3編:複数の状態遷移で管理するリアルタイム音声セッション ── 名前付き状態管理パターン
- 第4編:Function Callingで音声から予定を管理する ── NON_BLOCKING設計とsealed class
- 第5編:本番運用で学んだ12の教訓とSentryモニタリング ── 実装履歴から読み解く
各記事はコード付きで独立して読める構成にしています。Flutter × Gemini Live APIの日本語実装記事はほぼ存在しないので、同じ領域に挑戦する方の参考になれば幸いです。
参考記事
トークマネプロモーション動画
トークマネ技術記事関連
トークマネプロモーションURL
