実践チュートリアル:Gemini Multimodal Live API で低遅延の音声・動画 AI アシスタントを構築する
Gemini Live API は音声の入出力をネイティブでサポートし、ASR や TTS を挟まないエンドツーエンド構成で遅延を 500 ms 以内に抑えられます。本記事では、この API を使ってリアルタイムに会話できる AI アシスタントを組み立てる手順をまとめます。
Gemini Multimodal Live API とは?
まず前提を整理しましょう。従来の Gemini API は、テキストを送ってテキストが返る、というシンプルな流れでした。音声でやり取りしたい場合は、自分で ASR(音声認識)と TTS(音声合成)を挟み、変換のたびにレイテンシが積み上がります。
Gemini Multimodal Live API の違いは、音声の入出力を最初から扱えることです。マイクで拾った音声をそのまま送れ、返ってくるのもオーディオストリーム。中間でフォーマット変換を挟む必要はありません。このエンドツーエンド設計で、遅延を 500 ms 以内に抑えられます。
スマートホームのプロトタイプで試したとき、「リビングのライトを少し暗くして」と言い終わるほぼ同時に応答が返り、相手がプログラムだと忘れさせるほどの滑らかさでした。
現在サポートされているモデルは gemini-2.0-flash-native-audio-preview です。Google は更新が速いので、バージョン名は定期的に確認してください。
アーキテクチャ設計と技術選定
システム構成は、フロントとバックエンドを分けるのがおすすめです。理由は単純で、API キーをフロントに置いてはいけないからです。
データフローは次のとおりです。
[ブラウザ] --WebSocket--> [Python バックエンドプロキシ] --WebSocket--> [Gemini Live API]
| | |
マイク収集 転送 + ビジネスロジック AI 処理
スピーカー再生 VAD 検知 / 割り込み制御 音声生成
ブラウザから直接 Gemini に繋ぐことも技術的には可能ですが、API キーを JavaScript に書くことになり、開発者ツールを開けば誰でも取得できます。私は一度これをやって、翌日の請求で痛い目にしました。
採用した技術スタックは次の表のとおりです。
| レイヤー | 技術 | 用途 |
|---|---|---|
| フロント | 素の JavaScript + Web Audio API | 音声収集・再生、AudioWorklet によるリアルタイム処理 |
| バック | Python 3.9+ + websockets | WebSocket プロキシ、VAD、セッション管理 |
| プロトコル | WebSocket + JSON | Gemini との双方向通信 |
Web Audio API の AudioWorklet は、メインスレッドをブロックせず独立スレッドで音声を処理できるので有用です。実装例は後述します。
WebSocket 接続の確立とセッション管理
まず Gemini への接続方法からです。
Live API の WebSocket エンドポイントは次のとおりです。
wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent?key=YOUR_API_KEY
v1alpha はプレビュー版の印です。本番利用ではインターフェース変更に備えてください。
接続後、最初に Setup メッセージで対話条件を送ります。
import asyncio
import json
import websockets
GEMINI_API_KEY = "your-api-key-here"
GEMINI_WS_URL = (
f"wss://generativelanguage.googleapis.com/ws/"
f"google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContent"
f"?key={GEMINI_API_KEY}"
)
CONFIG = {
"setup": {
"model": "models/gemini-2.0-flash-native-audio-preview",
"generation_config": {
"response_modalities": ["AUDIO"],
"speech_config": {
"voice_config": {
"prebuilt_voice_config": {
"voice_name": "Charon" # 例: Charon, Aoede など
}
}
}
},
"system_instruction": {
"parts": [{"text": "あなたは helpful な AI アシスタントです。簡潔で自然に答えてください。"}]
}
}
}
async def connect():
async with websockets.connect(GEMINI_WS_URL) as ws:
# setup 設定を送信
await ws.send(json.dumps(CONFIG))
# setup complete を待つ
response = await ws.recv()
data = json.loads(response)
if "setupComplete" in data:
print("✅ 接続成功。対話を開始できます")
return ws
else:
raise Exception(f"Setup 失敗: {data}")
注目ポイントは次の 2 点です。
response_modalities:["AUDIO"]なら音声のみ。テキストも欲しければ["AUDIO", "TEXT"]voice_name: プリセット音色のひとつ。私は落ち着いたCharonが好みです
切断後の再接続は、指数バックオフが安全です。いきなり連打するとサービスに負荷がかかります。
async def connect_with_retry(max_retries=5):
for attempt in range(max_retries):
try:
return await connect()
except Exception as e:
wait_time = min(2 ** attempt, 30) # 最大 30 秒
print(f"接続失敗 ({e})。{wait_time} 秒後に再試行...")
await asyncio.sleep(wait_time)
raise Exception("再試行上限に達しました")
16kHz PCM オーディオストリームの収集と転送
接続できたら、音声の取得と送信です。
16kHz を選ぶ理由は、人の声の主要帯域(男性は低め、女性は高めでおおよそ 85〜255 Hz)に対し、ナイキストの定理では 8kHz でも理論上は足ります。ただしディテールを残すなら 16kHz が音質とデータ量のバランス点(sweet spot)になります。Gemini 公式もこのレートを推奨しています。
フロントの収集コード例です。
class AudioRecorder {
constructor() {
this.sampleRate = 16000;
this.bufferSize = 1024;
this.audioContext = null;
this.workletNode = null;
this.stream = null;
this.onAudioData = null; // コールバック
}
async start() {
// マイク権限
this.stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true
}
});
// サンプルレートを 16kHz に固定
this.audioContext = new AudioContext({
sampleRate: 16000
});
await this.audioContext.audioWorklet.addModule('pcm-processor.js');
const source = this.audioContext.createMediaStreamSource(this.stream);
this.workletNode = new AudioWorkletNode(this.audioContext, 'pcm-processor');
this.workletNode.port.onmessage = (event) => {
const float32Data = event.data;
const int16Data = this.float32ToInt16(float32Data);
const base64Data = btoa(String.fromCharCode(...new Uint8Array(int16Data.buffer)));
if (this.onAudioData) {
this.onAudioData(base64Data);
}
};
source.connect(this.workletNode);
console.log('🎤 音声収集を開始');
}
float32ToInt16(float32Array) {
const int16Array = new Int16Array(float32Array.length);
for (let i = 0; i < float32Array.length; i++) {
const s = Math.max(-1, Math.min(1, float32Array[i]));
int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
}
return int16Array;
}
stop() {
if (this.workletNode) {
this.workletNode.disconnect();
}
if (this.audioContext) {
this.audioContext.close();
}
if (this.stream) {
this.stream.getTracks().forEach(track => track.stop());
}
console.log('🛑 音声収集を停止');
}
}
AudioWorklet 用の pcm-processor.js です。
// pcm-processor.js
class PCMProcessor extends AudioWorkletProcessor {
process(inputs, outputs, parameters) {
const input = inputs[0];
if (input && input[0]) {
this.port.postMessage(input[0].slice());
}
return true;
}
}
registerProcessor('pcm-processor', PCMProcessor);
バックエンドは受け取ったデータを Gemini へ転送します。
async def send_audio(ws, base64_pcm_data):
"""オーディオを Gemini へ送信"""
message = {
"realtime_input": {
"media_chunks": [{
"mime_type": "audio/pcm;rate=16000",
"data": base64_pcm_data
}]
}
}
await ws.send(json.dumps(message))
注意点として、一部ブラウザの getUserMedia は sampleRate を無視し、44.1kHz や 48kHz で返すことがあります。AudioContext 側でリサンプリングするか、audiobuffer-to-wav などのライブラリで揃えると安心です。
VAD(音声活動検知)の実装
無音のまま送り続けると、帯域もコストも無駄になります。ここで VAD(Voice Activity Detection)が役立ちます。
役割は単純で、「人が話しているか」を判定し、話しているときだけ送ることです。
Google の WebRTC VAD は軽量で速く、Python では webrtcvad が使えます。
import webrtcvad
import collections
import numpy as np
class VADProcessor:
def __init__(self, aggressiveness=2, frame_duration_ms=20):
"""
aggressiveness: 0-3。高いほど厳格
frame_duration_ms: 10, 20, 30 のいずれか
"""
self.vad = webrtcvad.Vad(aggressiveness)
self.frame_duration_ms = frame_duration_ms
self.sample_rate = 16000
self.ring_buffer = collections.deque(maxlen=30) # 600 ms
self.triggered = False
def process_frame(self, pcm_bytes):
"""1 フレームを処理し、送信すべきか返す"""
is_speech = self.vad.is_speech(pcm_bytes, self.sample_rate)
if not self.triggered:
self.ring_buffer.append((pcm_bytes, is_speech))
num_voiced = sum(1 for _, speech in self.ring_buffer if speech)
if num_voiced > 0.9 * self.ring_buffer.maxlen:
self.triggered = True
return b''.join([f for f, _ in self.ring_buffer])
return None
else:
if is_speech:
self.ring_buffer.append((pcm_bytes, True))
return pcm_bytes
else:
self.ring_buffer.append((pcm_bytes, False))
num_unvoiced = sum(1 for _, speech in self.ring_buffer if not speech)
if num_unvoiced > 0.9 * self.ring_buffer.maxlen:
self.triggered = False
self.ring_buffer.clear()
return pcm_bytes
利用例です。
vad = VADProcessor(aggressiveness=2)
async def handle_client_audio(websocket, gemini_ws):
async for message in websocket:
data = json.loads(message)
if 'audio' in data:
pcm_bytes = base64.b64decode(data['audio'])
result = vad.process_frame(pcm_bytes)
if result:
await send_audio(gemini_ws, base64.b64encode(result).decode())
aggressiveness はまず 2 から。低すぎるとノイズを音声と誤認し、高すぎると小声を取りこぼします。
webrtcvad が使えない環境では、RMS エネルギーによる簡易検知も選択肢です。
// フロントの代替: RMS ベース
function detectVoiceActivity(audioData, threshold = 0.015) {
const sum = audioData.reduce((acc, val) => acc + val * val, 0);
const rms = Math.sqrt(sum / audioData.length);
return rms > threshold;
}
Barge-in(割り込み)の実装
AI が長く話しているあいだ、こちらの言葉を挟めない——そんな経験はありませんか。
Barge-in は、AI 発話中にユーザーが話し始めたら、AI がすぐ止まって聞き直す機能です。Gemini Live API はネイティブで対応しており、自動活動検知を有効にするだけで使えます。
CONFIG = {
"setup": {
"model": "models/gemini-2.0-flash-native-audio-preview",
"generation_config": {
"response_modalities": ["AUDIO"],
},
"realtime_input_config": {
"automatic_activity_detection": {
"disabled": False,
"start_of_speech_sensitivity": "START_SENSITIVITY_HIGH",
"end_of_speech_sensitivity": "END_SENSITIVITY_LOW"
}
}
}
}
sensitivity の意味は次のとおりです。
start_of_speech_sensitivityをHIGHにすると、話し始めに敏感になり割り込みが起きやすいend_of_speech_sensitivityをLOWにすると、本当に話し終えたかを慎重に判断し、誤検知を減らせる
クライアントは interrupted を受け取ったら再生を止めます。
class GeminiClient {
constructor() {
this.audioQueue = [];
this.isPlaying = false;
this.currentSource = null;
}
async handleMessage(event) {
const message = JSON.parse(event.data);
if (message.server_content?.interrupted) {
console.log('⚡ 割り込み。再生を停止');
this.stopPlayback();
return;
}
if (message.server_content?.model_turn) {
const parts = message.server_content.model_turn.parts;
for (const part of parts) {
if (part.inline_data?.mime_type.startsWith('audio/')) {
const audioData = base64ToArrayBuffer(part.inline_data.data);
this.queueAudio(audioData);
}
}
}
}
stopPlayback() {
this.audioQueue = [];
this.isPlaying = false;
if (this.currentSource) {
try {
this.currentSource.stop();
} catch (e) {
// すでに停止済みの場合あり
}
this.currentSource = null;
}
}
async queueAudio(audioData) {
this.audioQueue.push(audioData);
if (!this.isPlaying) {
this.playNext();
}
}
async playNext() {
if (this.audioQueue.length === 0) {
this.isPlaying = false;
return;
}
this.isPlaying = true;
const audioData = this.audioQueue.shift();
const audioBuffer = await this.audioContext.decodeAudioData(audioData.slice());
this.currentSource = this.audioContext.createBufferSource();
this.currentSource.buffer = audioBuffer;
this.currentSource.connect(this.audioContext.destination);
this.currentSource.onended = () => {
this.playNext();
};
this.currentSource.start();
}
}
stop() は再生完了後に呼ぶと例外になることがあるので、try-catch で囲んでおくとコンソールが荒れません。
パフォーマンス最適化と遅延制御
遅延の主な要因は次の 3 つです。
- ネットワーク:ブラウザ → サーバー → Gemini の往復
- エンコード/デコード:PCM は無損失なのでコストは小さい
- バッファ:滑らかな再生のための蓄積
対策の要点です。
バッファを浅くする
再生バッファは 100〜200 ms 程度で十分なことが多いです。
const audioContext = new AudioContext({
sampleRate: 16000,
latencyHint: 'interactive' // 低遅延モード
});
アダプティブバッファ
ジッターが大きいときはバッファを少し増やし、安定時は減らす、という制御が有効です。
エコーキャンセル
スピーカー利用時は AI の声がマイクに入りループしやすいです。getUserMedia のオプションを有効にしてください。
navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
})
メトリクス監視
Performance API で計測します。
class LatencyMonitor {
constructor() {
this.metrics = [];
}
recordSendTime() {
this.lastSendTime = performance.now();
}
recordReceiveTime() {
const latency = performance.now() - this.lastSendTime;
this.metrics.push(latency);
if (this.metrics.length > 100) {
this.metrics.shift();
}
const avg = this.metrics.reduce((a, b) => a + b, 0) / this.metrics.length;
console.log(`📊 平均遅延: ${avg.toFixed(2)} ms`);
}
}
テスト環境での目安です。
- エンドツーエンド:300〜500 ms(ネットワーク依存)
- 初回応答:200〜400 ms
- 連続対話:150〜300 ms
これより明らかに遅い場合は、次を確認してください。
- WebSocket は WSS か(HTTP よりオーバーヘッドが増える)
- サーバーは Google のデータセンターに近いか
- VAD のフレーム長が長すぎないか
- フロントの再生バッファが大きすぎないか
Chrome などはユーザー操作なしの自動再生を制限するため、「対話を開始」ボタンで AudioContext を起動するのが無難です。
まとめ
概念の整理から、アーキテクチャ、WebSocket、音声収集、VAD、Barge-in、遅延最適化まで、Gemini Live API でリアルタイム音声アシスタントを作る流れを一通り見てきました。実装でつまずきやすい点も、できるだけ具体的に書いてあります。
この分野はまだ速く進化しており、API も更新が続きます。ただ、ここで紹介した分離アーキテクチャとストリーミングの基本は、しばらく使える土台になるはずです。私のプロジェクトでも数ヶ月、安定して動いています。
開発中に詰まったら、コミュニティで相談するのも手です。一人で抱え込むより、議論した方が早く前に進めます。
FAQ
なぜフロントエンドとバックエンドを分離した構成が必要なのですか?
サンプリングレートに 16kHz を選ぶ理由は?
VAD の aggressiveness パラメータはどう調整する?
Barge-in 機能は追加開発が必要?
3分で読めます · 公開日: 2026年2月27日 · 更新日: 2026年6月1日
関連記事
NotebookLM 実践ガイド:400 本の研究文献を対話型の「デジタル脳」に変える
NotebookLM 実践ガイド:400 本の研究文献を対話型の「デジタル脳」に変える
AI の「魂」を覗く:Gemini 3.1 の思考チェーン(CoT)漏洩でコードロジックをデバッグする
AI の「魂」を覗く:Gemini 3.1 の思考チェーン(CoT)漏洩でコードロジックをデバッグする
AI SEO 自動化の実践:NotebookLM + Gemini 3 でコンテンツ制作工場を構築する
コメント
GitHubアカウントでログインしてコメントできます