AI活用

Gemini Live APIの使い方|JavaScriptで音声・映像のリアルタイムAIを作る

Gemini Live APIをJavaScriptで使い、短期トークン、WebSocketセッション、PCM音声、映像、文字起こし、割り込み、context圧縮、session resumptionまで解説します。

この記事の目次
  1. 結論:短期トークン・割り込み・再開を最初から設計する
  2. Gemini Live APIで作れるものと制約
  3. 音声・映像・テキストを同じsessionで扱う
  4. 接続時間とcontext上限を分けて考える
  5. 安全な接続構成を作る
  6. バックエンドで短期トークンを発行する
  7. ブラウザからLive sessionへ接続する
  8. 音声と映像を送る
  9. 音声は20〜40msのPCM chunkへする
  10. マイクを一時停止したらstream endを送る
  11. 映像は必要なframeだけ送る
  12. イベントと文字起こしを処理する
  13. messageを種類ごとに分ける
  14. 割り込み時に再生bufferを破棄する
  15. session resumptionと長時間会話
  16. resumption handleを更新する
  17. context圧縮を有効にする
  18. 固有成果物:イベントタイムライン
  19. よくあるエラー
  20. 本番前チェックリスト
  21. まとめ:接続成功だけでなく、10分後と割り込みを試す

Gemini Live APIは、WebSocketを使って音声・映像・テキストを双方向に送受信するリアルタイムAPIです。ブラウザから直接接続する場合は、長期APIキーを埋め込まず、バックエンドが発行した用途・回数・期限付きのephemeral token(短期トークン)を使います。

入力音声はraw 16-bit PCM、標準的には16kHz、little-endianで送り、音声出力は24kHzで返ります。音声は20〜40ミリ秒程度の小さなchunk(小分けデータ)で送り、利用者が話し始めて応答が割り込まれたら、再生待ちのAI音声をすぐ破棄します。

この記事では、JavaScript SDKによる短期トークン発行と接続、イベント処理、音声・映像送信、文字起こし、VAD、session resumption、context window compressionを解説します。モデルIDやpreview状態は変化が速いため、2026年7月20日時点の公式ドキュメントを基準にし、環境変数へ分離します。

情報確認日:2026年7月20日(日本時間)

結論:短期トークン・割り込み・再開を最初から設計する

ブラウザ
  │ 自分のサービスへログイン済み
  ├─ POST /api/gemini-live-token
  ▼
自分のバックエンド
  ├─ 長期GEMINI_API_KEYを保持
  ├─ model・modality・uses・期限を制約
  └─ ephemeral tokenを返す
  ▼
ブラウザ
  ├─ v1alphaでGemini Liveへ接続
  ├─ 16-bit PCM音声 / JPEG画像 / textを送信
  ├─ 24kHz音声 / transcription / eventを受信
  ├─ interruptedで再生bufferを破棄
  └─ goAway・resumption handleで再接続

実用化で外せない項目

  • 標準APIキーはバックエンドだけに置く
  • 短期トークンを1回利用・短い開始期限・固定modelで発行する
  • 入力16kHzと出力24kHzを混同しない
  • AI音声の再生queueを持ち、割り込み時に空にする
  • 接続切替にsession resumptionを使う
  • 長い会話ではcontext圧縮と利用時間上限を設ける
スポンサーリンク

Gemini Live APIで作れるものと制約

音声・映像・テキストを同じsessionで扱う

Live APIは、連続音声、カメラ画像、テキスト、tool responseをsessionへ送れます。映像は動画ファイルをそのまま流すのではなく、JPEGやPNGのframe(静止画)を最大1fps程度で送る方式です。画面共有やカメラ案内では、必要な瞬間だけ画像を送ると通信量とcontext消費を抑えられます。

modality 意味 形式の要点
Audio input 利用者の音声 raw 16-bit PCM、little-endian。16kHzが標準
Audio output モデルの音声 raw 16-bit PCM、24kHz
Video input カメラ・画面の画像 JPEG/PNG frame、最大1fpsを目安
Text 指示・補助入力 realtime inputまたはordered content
Transcription 入力・出力音声の文字化 session configで有効化

modality(モダリティ)は情報の種類、PCM(Pulse Code Modulation)は音の振幅を数値として並べる非圧縮音声形式、little-endianは1つの数値を下位byteから並べる方式です。sample rateは1秒あたりの音声sample数で、16kHzなら16,000個です。

接続時間とcontext上限を分けて考える

制約 2026-07-20時点の目安 対策
WebSocket接続 約10分でresetされる場合がある goAwayとsession resumptionを処理
音声のみ・圧縮なし 約15分 context window compressionを有効化
音声+映像・圧縮なし 約2分 画像頻度を下げ、contextを圧縮
短期tokenでsession開始 既定で発行後1分 接続直前に発行
短期tokenで送信 既定で30分 アプリ上限を別に設定
resumption handle 発行後2時間有効 最新handleだけを短期保存

context window(コンテキストウィンドウ)は、モデルが同時に参照できる会話・音声・画像の範囲です。接続が切れる時間と、contextがいっぱいになる時間は別です。compression(圧縮)は古い会話を要約・縮約して新しい入力の余地を作る機能です。

Preview機能の確認:ephemeral tokenやLiveモデルにはpreviewの機能が含まれます。利用地域、モデル名、API version、制限は公開前に公式ページで再確認し、productionのSLAとして無条件に扱わないでください。

安全な接続構成を作る

バックエンドで短期トークンを発行する

標準APIキーを使うバックエンド側へ @google/genai を入れます。例ではExpressを使います。

npm install @google/genai express
export GEMINI_API_KEY="your_api_key_here"
export GEMINI_LIVE_MODEL="gemini-3.1-flash-live-preview"
import express from "express";
import { GoogleGenAI } from "@google/genai";

const app = express();
const model = process.env.GEMINI_LIVE_MODEL;

if (!process.env.GEMINI_API_KEY || !model) {
  throw new Error("Required environment variables are missing");
}

const ai = new GoogleGenAI({
  apiKey: process.env.GEMINI_API_KEY,
  httpOptions: { apiVersion: "v1alpha" },
});

app.post("/api/gemini-live-token", async (request, response) => {
  // 本番ではここで自サービスのlogin、回数、同時接続を確認
  const now = Date.now();

  const token = await ai.authTokens.create({
    config: {
      uses: 1,
      newSessionExpireTime:
        new Date(now + 60 * 1000).toISOString(),
      expireTime:
        new Date(now + 30 * 60 * 1000).toISOString(),
      liveConnectConstraints: {
        model,
        config: {
          responseModalities: ["AUDIO"],
          sessionResumption: {},
        },
      },
      httpOptions: { apiVersion: "v1alpha" },
    },
  });

  response.set("Cache-Control", "no-store");
  response.json({
    token: token.name,
    model,
  });
});

app.listen(3000);

uses: 1 は新しいsessionの開始を1回に制限します。liveConnectConstraints でmodelやresponse modalityを固定すると、ブラウザが別の高コスト設定へ自由に変える範囲を減らせます。

短期tokenは「公開してよいkey」ではない:漏れたtokenは期限内に使われる可能性があります。HTTPS、login、CSRF対策、no-store、origin制限、発行回数上限を設け、URLやアクセスログへ含めません。

ブラウザからLive sessionへ接続する

import {
  GoogleGenAI,
  Modality,
} from "@google/genai";

let session = null;
let latestResumptionHandle = null;
const audioQueue = [];

async function connectLive() {
  const tokenResponse = await fetch("/api/gemini-live-token", {
    method: "POST",
    credentials: "same-origin",
  });

  if (!tokenResponse.ok) {
    throw new Error("Token request failed");
  }

  const { token, model } = await tokenResponse.json();
  const ai = new GoogleGenAI({
    apiKey: token,
    httpOptions: { apiVersion: "v1alpha" },
  });

  session = await ai.live.connect({
    model,
    config: {
      responseModalities: [Modality.AUDIO],
      inputAudioTranscription: {},
      outputAudioTranscription: {},
      sessionResumption: latestResumptionHandle
        ? { handle: latestResumptionHandle }
        : {},
      contextWindowCompression: {
        slidingWindow: {},
      },
    },
    callbacks: {
      onopen: () => updateState("connected"),
      onmessage: handleLiveMessage,
      onerror: (event) => {
        console.error(event.message);
        updateState("error");
      },
      onclose: (event) => {
        console.log("closed", event.reason);
        updateState("disconnected");
      },
    },
  });
}

constrained tokenで固定した設定と、browser側のsession configを矛盾させないようにします。SDKの型やフィールド名は更新されるため、導入時の @google/genai をlockfileで固定し、公式サンプルと型チェックを行います。

音声と映像を送る

音声は20〜40msのPCM chunkへする

16kHz・mono・16-bit PCMでは、20msが320sample、40msが640sampleです。1秒分をまとめて送ると、発話検出と割り込みが遅れます。次は、16kHzへresample済みの Float32Array をPCM16のBase64へ変換する関数です。

function float32ToPcm16Base64(samples) {
  const buffer = new ArrayBuffer(samples.length * 2);
  const view = new DataView(buffer);

  for (let index = 0; index < samples.length; index += 1) {
    const clamped = Math.max(-1, Math.min(1, samples[index]));
    const value = clamped < 0
      ? clamped * 0x8000
      : clamped * 0x7fff;
    view.setInt16(index * 2, value, true);
  }

  const bytes = new Uint8Array(buffer);
  let binary = "";
  for (const byte of bytes) binary += String.fromCharCode(byte);
  return btoa(binary);
}

function sendPcmChunk(samplesAt16Khz) {
  if (!session) throw new Error("Session is not connected");

  session.sendRealtimeInput({
    audio: {
      data: float32ToPcm16Base64(samplesAt16Khz),
      mimeType: "audio/pcm;rate=16000",
    },
  });
}

実際のブラウザマイクは44.1kHzや48kHzの場合があります。AudioWorkletなどで音声処理を別threadへ移し、sample rateを16kHzへ変換して20〜40msごとに送ります。UI threadで重い変換を続けると音切れが起こるため、本番ではworklet、queue上限、遅延計測を用意します。

マイクを一時停止したらstream endを送る

function notifyAudioStreamEnd() {
  session?.sendRealtimeInput({
    audioStreamEnd: true,
  });
}

automatic VADが有効な状態で音声streamを1秒以上止める場合、audioStreamEnd を送り、bufferされた音声を処理させます。automatic VADを無効にして手動制御する場合は、代わりに activityStartactivityEnd を送ります。

映像は必要なframeだけ送る

function sendJpegFrame(base64Jpeg) {
  session?.sendRealtimeInput({
    video: {
      data: base64Jpeg,
      mimeType: "image/jpeg",
    },
  });
}

camera映像を毎frame送るのではなく、最大1fpsを守り、画面が変わったときや利用者が質問したときだけ送る方法を検討します。背景に映る個人情報、通知、住所、他人の顔を送る前に利用目的と同意を確認してください。

イベントと文字起こしを処理する

messageを種類ごとに分ける

function handleLiveMessage(message) {
  const serverContent = message.serverContent;

  if (message.sessionResumptionUpdate?.newHandle) {
    latestResumptionHandle =
      message.sessionResumptionUpdate.newHandle;
  }

  if (message.goAway) {
    scheduleReconnect(message.goAway.timeLeft);
  }

  if (serverContent?.inputTranscription?.text) {
    appendTranscript(
      "user",
      serverContent.inputTranscription.text,
    );
  }

  if (serverContent?.outputTranscription?.text) {
    appendTranscript(
      "assistant",
      serverContent.outputTranscription.text,
    );
  }

  for (const part of serverContent?.modelTurn?.parts ?? []) {
    if (part.inlineData?.data) {
      enqueueOutputAudio(part.inlineData.data, 24000);
    }
  }

  if (serverContent?.interrupted) {
    clearOutputAudioQueue();
    updateState("listening");
  }

  if (serverContent?.turnComplete) {
    updateState("ready");
  }
}

input transcriptionは利用者音声の文字起こし、output transcriptionはモデル音声の文字起こしです。音声と文字は到着順や確定のタイミングが完全には一致しないため、仮表示と確定表示を分けます。

割り込み時に再生bufferを破棄する

利用者が話し始め、serverから interrupted が届いたら、すでにブラウザへ届いている未再生のAI音声を破棄します。API側の生成が止まっても、clientのqueueに音声が残っているとAIが話し続けているように聞こえます。

function clearOutputAudioQueue() {
  audioQueue.length = 0;
  stopCurrentlyPlayingAudio();
}

入力音声を大量に先読みせず、outputも必要以上にbufferしません。20〜40msの小さな入力chunkと、短い再生queueの両方を測定します。

session resumptionと長時間会話

resumption handleを更新する

Gemini Liveは接続が永続するとは限りません。serverから新しいresumption handleが届いたら最新値を保持し、goAway通知を受けたら期限内に新しい接続を作ります。handleは認証情報に近い扱いで、ログやURLへ出さず、必要な期間だけ保存します。

sessionResumptionUpdate.newHandle
  ↓ 最新handleへ置換
通常の音声会話
  ↓ goAway.timeLeft
新しいephemeral tokenを取得
  ↓ handle付きでconnect
会話状態を再開
  ↓ 古いWebSocketをclose

context圧縮を有効にする

Live音声は時間とともにcontext tokenが増えます。公式best practicesでは、native audioが概算で毎秒約25token増えると案内されています。長時間会話では contextWindowCompression を有効にし、アプリ側でも会話時間、画像頻度、不要なtool結果を制限します。

圧縮後は古い細部が失われる可能性があります。予約番号、利用者の確認済み選択、重要な同意状態は、会話の記憶だけに頼らず、自分のアプリの検証済みstateとして保持します。

固有成果物:イベントタイムライン

00:00.000 connectを押す
00:00.180 backendへtokenを要求
00:00.520 ephemeral token受領
00:01.160 WebSocket open
00:02.000 16kHz PCM送信開始
00:02.040 input audio chunk #1
00:04.620 input transcription確定
00:04.900 model audio chunk #1(24kHz)
00:07.100 利用者が話し始める
00:07.160 interrupted受信・再生queue破棄
00:09.400 turnComplete
09:20.000 goAway受信
09:20.150 新tokenと最新handleで再接続
09:20.900 resumed

productionでは、token発行、connection open、最初の入力、発話終了、最初の出力音声、割り込み、turn完了、goAway、再開を計測します。音声本文を保存せず、session ID、event種別、elapsed time、error codeだけで調査できる設計を優先します。

よくあるエラー

症状 確認点
tokenが使えない v1alpha、開始1分以内、uses、model制約、時刻ずれ
音声が速い・遅い 入力16kHzと出力24kHz、PCM16、little-endian
応答が遅い chunkが大きすぎないか、入力buffer、VAD、画像頻度
割り込み後もAIが話す interrupted でclient再生queueを破棄したか
約10分で切れる goAway、最新resumption handle、新token、再接続
長時間で終了する context圧縮、音声のみ/映像ありの上限、token期限

本番前チェックリスト

  1. 長期GEMINI_API_KEYがbrowser bundleへ含まれていない
  2. token endpointにlogin、CSRF、rate limit、no-storeがある
  3. tokenのuses、開始期限、送信期限、modelを制約している
  4. SDK、model、v1alphaの組み合わせを型チェックしている
  5. 入力16kHz PCM16と出力24kHzを別処理にしている
  6. 20〜40msの入力chunkと再生queue遅延を測っている
  7. interruptedで未再生audioを直ちに破棄する
  8. goAwayとsession resumptionを実回線でテストしている
  9. context圧縮とアプリ側の時間上限がある
  10. マイク・camera・transcriptの利用目的と保存方針を表示している

通常の単発Gemini APIから始めたい場合はGemini APIをJavaScriptから使う方法、APIキー全般の保管はAI APIキーの安全な管理方法を参照してください。

まとめ:接続成功だけでなく、10分後と割り込みを試す

Gemini Live APIのブラウザ実装では、バックエンドが1回・短期限・固定modelのephemeral tokenを発行し、ブラウザがv1alphaのLive APIへ接続します。音声は16kHz PCM16で小さく送り、24kHzの出力音声をqueue再生し、interrupted でqueueを破棄します。

1往復できただけでは完成ではありません。約10分後の接続切替、圧縮なしのcontext上限、token期限、goAway、resumption handle、マイク停止、回線断をテストし、利用者がいつ聞かれ、いつ送られ、いつ切れたか分かるUIにしてください。

スポンサーリンク