powered by TechFeed
表示モード
ハウツー

「声でフィルタが動く」を本当に実装する — GeminiのリアルタイムAPIでブラウザのDOMを直接操作する方法

9月20日、Amir Meimandが「Building a Real-Time Voice Co-Browsing Agent with Gemini Live API」と題した記事を公開した。Gemini Live APIを使ってブラウザのDOMをリアルタイムに操作する音声共同ブラウジングエージェントの実装方法を、アーキテクチャ設計からコードの要所まで丁寧に解説した内容だ。

9月20日、Amir Meimandが「Building a Real-Time Voice Co-Browsing Agent with Gemini Live API」と題した記事を公開した。Gemini Live APIを使ってブラウザのDOMをリアルタイムに操作する音声共同ブラウジングエージェントの実装方法を、アーキテクチャ設計からコードの要所まで丁寧に解説した内容だ。


「音声でフィルタを操作する」を本当に実装する

「音声でECサイトを操作できたら便利」というアイデア自体は珍しくない。しかし既存のAIチャットウィジェットの多くは、サイドパネルにテキストで回答を返すだけで、実際の商品カタログのフィルタやスライダーは動かない。ユーザーは結局、画面を切り替えて自分の手でフィルタを設定し直す必要がある。

Amir Meimandが実装したのは、この「チャットとUIの断絶」を解消するシステムだ。Gemini Live APIを使い、ユーザーの音声をリアルタイムで解析してブラウザのDOMを直接書き換える。「5G対応で8万円以下のSamsung端末が欲しい」と話しかけるだけで、フィルタが自動的に設定され、該当商品がハイライト表示される。

完全なコードはGitHubリポジトリで公開されている。


アーキテクチャの核心:PythonとブラウザDOMの「境界問題」

このシステムで最も面白い設計上の課題が、実行環境の境界をどう越えるかだ。

  • Pythonバックエンド(Google Cloud Run上のFastAPI):Gemini Live APIとの双方向WebSocketストリームを管理し、APIキーを安全に保持する。ただし、クライアントのブラウザDOMには直接アクセスできない。
  • ブラウザ(TypeScript):DOMの読み書きができるのはクライアント側のJavaScript/TypeScriptのみ。

ここで重要になるのが、Gemini Live APIが採用するBidirectional Streaming(Bidi Streaming)という通信方式だ。通常のREST APIが「リクエスト→レスポンス」という一問一答型であるのに対し、Bidi StreamingはクライアントとサーバーがWebSocketを介して常時接続を維持し、どちら側からも任意のタイミングでメッセージを送受信できる。音声データのような連続したストリームを低遅延で扱うには不可欠な方式であり、Geminiがツール呼び出し(toolCall)や音声応答を非同期に返せるのもこの仕組みによる。従来のWebSpeech APIや単方向のHTTP SSEと比べて、モデルと環境が双方向に対話できる点がLive APIの本質的な強みだ。

この問題を解決するために、Pythonサーバーを非同期RPCプロキシブリッジとして機能させる設計を採用している。処理の流れは以下の通りだ。

  1. セッション開始時、PythonはGemini Live APIに接続し、ブラウザ側で実行可能なツール(get_screen_contententer_form_datahighlight_elements)を登録する
  2. ブラウザがマイク音声(16kHz PCM)をPythonに送信し、PythonがそれをGeminiへ転送する
  3. GeminiがUI操作が必要と判断すると、構造化されたtoolCall JSONをPythonに返す
  4. PythonがそのtoolCallをブラウザのWebSocket接続に転送する
  5. ブラウザ側のTypeScriptコントローラーがDOMを操作し(チェックボックスのオン/オフ、スライダーの移動等)、ReactやVue等のフレームワークが即座に再レンダリングする
  6. ブラウザがtoolResponseをPythonに返し、PythonがGeminiに渡すことでモデルの処理がアンブロックされ、音声で確認応答が返る

ツール登録のコード

Gemini Live APIのBidi Streamingでは、セッション開始時にツールスキーマをfunctionDeclarationsとして登録する。元記事で特に強調されているのが、キー名はcamelCase必須という仕様だ。snake_caseで記述するとAPIが正しく解釈しないため注意が必要である。

登録するツールはget_screen_content(現在の画面状態を取得)、enter_form_data(フィルタ値を書き込む)、highlight_elements(商品カードをハイライトする)の3種類で構成される。詳細なスキーマ定義はGitHubリポジトリを参照のこと。以下はenter_form_dataの抜粋だ。

{
    "name": "enter_form_data",
    "description": "Modify search and catalog filters on the website (e.g., 5G toggle, brand, min/max price, sort order, keywords).",
    "parameters": {
        "type": "OBJECT",
        "properties": {
            "field_name": {"type": "STRING", "description": "The name of the filter or control."},
            "value": {"type": "STRING", "description": "The value to set."}
        },
        "required": ["field_name", "value"]
    }
}

セッション管理:GeminiLiveBridge

各クライアント接続ごとにGeminiLiveBridgeインスタンスを生成し、APIキーをサーバー側で完結させる設計になっている。

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    api_key = websocket.query_params.get("key") or os.environ.get("GEMINI_API_KEY")
    model_name = "models/gemini-2.5-flash-native-audio-preview-12-2025"
    if not api_key:
        await websocket.close(code=4000, reason="Gemini API Key missing")
        return
    bridge = GeminiLiveBridge(client_ws=websocket, api_key=api_key, model_name=model_name)
    await bridge.run()

使用モデルはgemini-2.5-flash-native-audio-preview-12-2025で、ネイティブ音声入出力に対応している。なお、元記事執筆時点(2025年9月)ではプレビュー版として提供されていたモデルであり、現在の提供状況(GA化の有無等)についてはGoogle AI公式ドキュメントで最新情報を確認されたい。

セットアップペイロードでは、音声出力モード(responseModalities: ["AUDIO"])と使用する音声(voiceName: "Puck")も指定する。Pythonはブラウザ向けとGemini向けの2つのタスクをasyncio.waitで並行実行し、いずれかが終了した時点でもう一方をキャンセルする。


エージェントの動作フロー

エージェントはアクション実行前に必ず現在の画面状態を確認する設計になっている。ページによって挙動が分岐する点も実装上のポイントだ。

  • ホームページ(/index.html:検索フォームへの入力後、エージェントが設定内容を音声で読み上げ、ユーザーの明示的な確認を得てから検索を実行する3ステップ確認プロトコルを採用
  • カタログページ(/search.html:有効なフィルター値をリアルタイムにスクレイピングし、ページリロードなしで即座にフィルタリングを実行。highlight_elementsで商品カードをビジュアルにハイライトする

元記事ではこのほか、実装中にハマりやすいポイントとして以下が挙げられている。**toolResponseをGeminiに返し忘れるとモデルのターンがブロックされたまま次の音声応答が生成されない**という点は特に注意が必要だ。また、get_screen_contentでDOM状態を取得する際、ページの非同期レンダリングが完了する前に読み取ると空の結果が返ることがあるため、取得タイミングの制御も考慮する必要がある。


詳細はBuilding a Real-Time Voice Co-Browsing Agent with Gemini Live APIを参照していただきたい。