9
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?

AIの会話に、心地いい「間」を。 ── Gemini Live APIでAI同士のポッドキャストを作った話

9
Posted at

こんにちは!
KDDIアイレットの取り組みとして6月22日〜7月3日の期間で開催中の「Google Cloud Next '26 / Google I/O やってみた系ブログリレー」、本日は10日目の投稿です。
今回は「Gemini Live API」での検証をお届けします!

前回の記事はこちらです。

はじめに

会話において「間」はとても重要です。

  • 漫才師が絶妙なタイミングでツッコむ
  • ジャズミュージシャンが相手の音に応じて次の音を選ぶ

会話のリズムや心地よさは、この「間」の取り方で大きく変わります。返答が遅すぎれば沈黙になりますし、早すぎれば発言が被ってしまいます。

余談ですが、個人的にはアニメ「物語シリーズ」の会話テンポが好きです。

AI との会話でも同じことが言えます。テキストチャットなら画面を眺めながら待てますが、音声会話で待ち時間が長いとうまく会話できている気がしないです。

Google の Gemini Live API はそのあたりがよくできていて、音声がリアルタイムにストリーミングされるので会話のテンポを保ちやすいのが素晴らしいです。

ただ、それでもキューを送ってから音声が返り始めるまでに数秒かかります。この「間」をできるだけ縮めたい、というのが今回の実験の出発点です。

この記事は、その**「間」をどう減らすか**を中心に、2つのAIにポッドキャストをさせた実装を紹介します。

デモ:

Gemini Live API とは

Gemini Live API は、Google の Gemini モデルとリアルタイムの双方向音声対話ができる API です。WebSocket ベースの常時接続で音声をストリーミングし、以下のような特徴があります。

ざっくりでいうと、レスポンスが早くて感情表現が豊かな音声会話ができる API です。

機能 内容
多言語サポート 70 言語で会話可能
割り込み ユーザーはいつでもモデルの応答を中断可能
ツールの使用 関数呼び出しや Google 検索などを統合できる
音声文字変換 入力・出力両方のテキスト書き起こしを提供できる
プロアクティブ音声 モデルが応答するタイミングやコンテキストを制御できる
アフェクティブダイアログ ユーザーの表現に合わせて回答のスタイルとトーンを調整できる
リアルタイム翻訳 70 以上の言語で音声をリアルタイムに翻訳できる

今回の実験

2 つの Gemini Live API セッションをホスト(Mia)とゲスト(Sora)に割り当て、交互に発話させるポッドキャストを作りました。それぞれに役割とシステムプロンプトを与え、ターンごとにキューで話す内容を指示します。エージェントは互いの発話内容を直接知らず、あくまでキューと自分のロール設定だけで話します。

ただ、Live API はキューを受け取ってから音声生成を開始するまでに数秒かかります。これがターンのたびに発生するため、何も対策しないと会話の間が発生します。この「間」をどう詰めるかが今回のポイントです。

構成

バックエンドが 2 つの Live API セッションを管理し、音声チャンクを WebSocket でフロントエンドにストリーミングします。

[Gemini Live API Session A (Mia)] ──┐
                                     ├─ FastAPI ── WebSocket ── React
[Gemini Live API Session B (Sora)] ─┘

バックエンド実装

エージェントへのプロンプト

各エージェントに system_instruction でキャラクターを定義します。system_instruction はいわゆるプロンプトです。今回の登場人物は 2 人です。

Mia(ホスト)

あなたはMiaというポッドキャストのホストです。
ゲストのSoraと「Gemini Live API」について対話形式で解説します。
自然な話し言葉で話題を振ったり、Soraの説明を受けてまとめたりしてください。
日本語で、30秒以内で話してください。

Sora(ゲスト)

あなたはSoraというAIエンジニアのゲストです。
ホストのMiaと「Gemini Live API」を話し言葉で解説します。
技術的な内容を噛み砕いて、自分の言葉で自然に語ってください。
日本語で、30秒以内で話してください。

また、各ターンにキュー(進行指示)をモデルへ送り込みます。

turns = [
    ("A", "ポッドキャストを始めてください。Soraを紹介し、今日のテーマ「Gemini Live API」を導入してください。"),
    ("B", "Miaの紹介を受けて、Gemini Live APIとは何か・普通のAPIとの違いを説明してください。"),
    ("A", "Soraの説明を受けて、リアルタイム音声対話の具体的な使いどころをSoraに質問してください。"),
    ("B", "音声アシスタントや通訳など、Gemini Live APIの実際のユースケースを紹介してください。"),
    ("A", "開発者として実装する際にハマりやすいポイントをSoraに聞いてください。"),
    ("B", "認証・モデル名の指定・音声フォーマット(PCM 24kHz)など、実装での注意点をTips形式で話してください。"),
    ("A", "Soraへの感謝と今日のまとめ、リスナーへの締めの言葉を話してください。"),
]

プロンプトはsystem_instruction でキャラクターを固定し、ターンごとのキューで話題を誘導します。

セッション設定と音声転送

各エージェントのセッション設定は、キャラクター定義・音声出力モード・声の 3 点で構成します。声を変えることで、聴いたときに話者が区別できます。
voice_name に指定できる声の一覧はこちらで確認できます。

AGENT_A_CONFIG = {
    "response_modalities": ["AUDIO"],
    "system_instruction": "あなたはMiaというポッドキャストのホストです。...",
    "speech_config": {
        "voice_config": {"prebuilt_voice_config": {"voice_name": "Puck"}}
    },
}

AGENT_B_CONFIG = {
    "response_modalities": ["AUDIO"],
    "system_instruction": "あなたはSoraというAIエンジニアのゲストです。...",
    "speech_config": {
        "voice_config": {"prebuilt_voice_config": {"voice_name": "Charon"}}
    },
}

2つのセッションを非同期に確立し、各ターンのキューを送信します。返ってくる音声チャンクは Base64 エンコードして WebSocket 経由でフロントエンドに転送し、ターン完了のシグナルが届いたら次エージェントへ制御を移します。

async with client.aio.live.connect(model=MODEL, config=AGENT_A_CONFIG) as session_a:
    async with client.aio.live.connect(model=MODEL, config=AGENT_B_CONFIG) as session_b:
        for agent, cue in turns:
            session = session_a if agent == "A" else session_b
            await session.send_client_content(
                turns={"role": "user", "parts": [{"text": cue}]},
                turn_complete=True,
            )
            async for response in session.receive():
                if response.data:
                    await ws.send_json({
                        "type": "audio",
                        "agent": agent,
                        "data": base64.b64encode(response.data).decode(),
                    })
                if response.server_content and response.server_content.turn_complete:
                    await ws.send_json({"type": "turn_complete", "agent": agent})
                    break

ターン間の無音を減らす工夫

Live API はキューを受け取ってから音声生成を始めるまでに数秒の「考える時間」がかかります。A が話し終わってから B にキューを送ると、その分だけ無音になります。

解決策:A の最初の音声チャンクが届いた時点で B のキューを先行送信する。

A が話し始めた段階で B はすでに生成を開始しているため、A が話し終わる頃には B の音声が準備できています。実装上は「最初のチャンクが届いたか」フラグを持ち、初回のみ次エージェントへ asyncio.create_task() で非同期にキューを投げます。

改善前:
Agent A: ████████████ | [無音 数秒] | Agent B: ████████████

改善後:
Agent A: ████████████ |      Agent B: ████████████
              ↑ここでBに先行送信

フロントエンド実装

WebSocket でバックエンドに接続し、届くメッセージに応じて音声再生と話者表示を更新します。処理の流れは次の通りです。

① 音声チャンクが届く

Gemini Live API が返す音声は 16bit signed PCM / 24kHz / モノラル の生バイナリです。ブラウザの AudioContext はこの形式を直接扱えないため、変換処理を挟んでから再生します。

② 再生開始時間の設定
変換後は AudioContext で再生しますが、そのまま届いた順に再生するとチャンク間でノイズや途切れが生じます。前のチャンクが終わる時刻を記録しておき、次のチャンクをその時刻から再生開始するよう予約することで、途切れのない連続再生を実現しています。

ハマりポイント

モデルが global になる

genai.Clientlocation を指定しないと、モデルパスが locations/global になります。

client = genai.Client(
    vertexai=True,
    project=os.environ["GOOGLE_CLOUD_PROJECT"],
    location="us-central1",  # リージョンの指定が必要だった
)

話者の正確な特定は難しい

image.png

本当は上の図のように、誰が話しているのかUI表示したかったですが、先行送信の最適化により、A がまだ話している最中に B の音声生成が始まります。そのため「今誰が話しているか」をリアルタイムに正確に把握するのは難しく、今回は実装を諦めました。また次回の機会があればチャレンジしてみようかと思います!

まとめ

先行送信の工夫でターン間の無音はかなり改善できました。完全にゼロにはなりませんが、ポッドキャストとして聴ける程度には詰められています。
さらに縮めるには、エージェントが相手の発話内容をリアルタイムに受け取って先読みする仕組みが必要になりそうですね。引き続き実験を続けていこうと思います!

また、今回のアーキテクチャでは「どのエージェントがどの順番で何を話すか」をあらかじめ台本として用意しておく必要があります。エージェント同士が自由に会話を展開したり話題が脱線したりするような即興性には対応できていないため、自然な雑談に近づけるにはアーキテクチャ自体の見直しが必要そうです。

9
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
9
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?