多言語対応を「文字」でしか考えていなかった頃の自分を、少し悔いています。
個人開発で癒し系のアプリをいくつか運用しているのですが、海外のユーザーさんから「音声のガイドも自分の言葉で聞きたい」という声をいただいたことがありました。当時の私は、テキストを翻訳して読み上げる方式しか思いつかず、話者の間や抑揚が失われた、どこか他人行儀な音声しか返せませんでした。
2026年7月に公開された Gemini 3.5 Live Translate は、その前提を静かに書き換えます。70以上の言語を自動で判定し、話者の抑揚を保ったまま、音声から音声へ翻訳する。文字を経由しないという一点が、体験の質をまるごと変えてしまいました。
ここでは、Live Translate の土台となる Live API を使って、会話翻訳をアプリに組み込むための設計を整理します。単なる呼び出し方ではなく、実運用で必ずつまずく「遅延」「言語の切り替わり」「切断」「コスト」の四つに、どう向き合うかを中心に書いていきます。
文字を経由しない、という設計上の分岐点
従来の翻訳フローは、音声認識・翻訳・音声合成という三つの独立した工程を直列につなぐものでした。それぞれの工程で待ち時間が生まれ、間や感情は文字に落ちる過程で削ぎ落とされます。
Live Translate と Live API が変えたのは、この三工程を一つの持続的なセッションに畳んだことです。音声を流し込むと、モデルが翻訳された音声を返す。中間表現としてのテキストは、必要なら受け取れますが、体験の主役ではなくなりました。
設計者にとっての意味は明確です。私たちはもう「認識精度」と「合成の自然さ」を別々にチューニングする世界にいません。代わりに、一本のストリームをいかに途切れさせず、いかに安く流し続けるかという、通信とセッションの設計問題に軸足が移ります。
最小構成 — セッションを張り、音声を流し、音声で受け取る
まずは骨格です。Live API は WebSocket 上で双方向にやり取りしますが、google-genai SDK が非同期セッションとして抽象化してくれます。
import asyncio
from google import genai
from google.genai import types
client = genai.Client( api_key = "YOUR_API_KEY" )
MODEL = "gemini-3.1-flash-live-preview"
config = {
"response_modalities" : [ "AUDIO" ],
"system_instruction" : (
"あなたは同時通訳者です。話者が話した言語を検出し、"
"英語の場合は日本語へ、日本語の場合は英語へ、"
"抑揚と語調を保ったまま音声で通訳してください。"
"要約や補足はせず、話された内容だけを訳します。"
),
"input_audio_transcription" : {},
"output_audio_transcription" : {},
}
async def translate_stream (mic_frames):
async with client.aio.live.connect( model = MODEL , config = config) as session:
async def pump_audio ():
async for chunk in mic_frames: # 16-bit PCM / 16kHz / little-endian
await session.send_realtime_input(
audio = types.Blob( data = chunk, mime_type = "audio/pcm;rate=16000" )
)
sender = asyncio.create_task(pump_audio())
async for response in session.receive():
content = response.server_content
if not content:
continue
if content.input_transcription:
print ( f "[原文] { content.input_transcription.text } " )
if content.output_transcription:
print ( f "[訳文] { content.output_transcription.text } " )
if content.model_turn:
for part in content.model_turn.parts:
if part.inline_data:
yield part.inline_data.data # 24kHz PCM 音声
await sender
要点は三つあります。入力音声は 16kHz の生 PCM で送ること、出力はネイティブ音声としてチャンクで返ること、そして書き起こし(transcription)は音声とは別に受け取れることです。
input_audio_transcription と output_audio_transcription を有効にしておくと、字幕表示やログ、後述する言語判定のフックに使えます。音声だけを扱うつもりでも、この二つは開けておくことを推奨します。運用の可観測性が段違いになります。
会話の途中で言語が切り替わる問題
実際の会話翻訳で最初に破綻するのは、たいてい「言語の固定」です。会話は往復します。日本語で話しかけ、英語で返され、また日本語で応じる。話者ごとに言語が入れ替わる状況を、翻訳の向きを固定した実装は捌けません。
Live Translate の自動言語判定は、この往復に追随してくれます。ただし、アプリ側で翻訳の向きを明示的に持ってしまうと、その利点を殺してしまいます。設計としては、向きを固定せず「検出された言語の反対側へ訳す」という指示をシステムプロンプトに委ね、アプリは判定結果を観測するだけに留めるのが素直です。
判定結果は入力側の書き起こしから拾えます。言語が切り替わった瞬間に UI の話者表示を更新する、といった処理はこう書けます。
def detect_lang (text: str ) -> str :
# 日本語の仮名・漢字が含まれるかで大まかに判定
for ch in text:
if "" <= ch <= "ヿ" or "一" <= ch <= "鿿" :
return "ja"
return "en"
class SpeakerTracker :
def __init__ (self):
self .current = None
def observe (self, transcript: str ) -> bool :
lang = detect_lang(transcript)
changed = lang != self .current
self .current = lang
return changed # True なら話者表示を切り替える
ここでの判断は、翻訳の向きをコードで決めないことです。モデルに任せられる部分を先回りして固定すると、Live Translate が本来吸収してくれる揺らぎを、こちらが抱え込むことになります。
遅延バジェットを分解する
「リアルタイム」は感覚的な言葉です。設計に落とすには、口を閉じてから訳語が耳に届くまでの時間を、区間ごとに分解して予算を割り当てる必要があります。
区間 内容 目安の予算
取得・エンコード マイク入力を 16kHz PCM に整えるまで 20〜40ms
送信 チャンクを送出しサーバに届くまで 回線依存(数十ms)
発話終端の判定 話し終わりを VAD が確定するまで 200〜500ms
モデル処理 翻訳音声の先頭が返り始めるまで 300〜700ms
再生バッファ 途切れ防止のためのジッタバッファ 80〜150ms
体感を左右する最大の要素は、意外にも発話終端の判定です。VAD(音声区間検出)が話し終わりを早く確定するほど応答は速くなりますが、早すぎると相手がまだ息を継いでいる途中で訳し始めてしまいます。ここは会話のテンポに合わせて調整する余地があります。
もう一つ、再生側のジッタバッファを削りすぎないこと。ネットワークが少し揺れただけで音声がプツプツと途切れると、たとえ平均遅延が短くても「遅い」と感じられます。私自身、平均値だけを見てバッファを詰めた結果、体感を悪化させた経験があります。私はこの経験から、平均ではなく途切れの頻度を第一の指標に置くことを推奨しています。
切断とセッション寿命に耐える
Live API のセッションは永続ではありません。ネットワークの瞬断、サーバ側の接続時間上限、モバイルでの回線切り替え。長い会話ほど、途中で一度は切れるものだと構えておくべきです。
設計の勘所は、切断を例外ではなく通常フローとして扱うことです。受信ループが終了したら再接続し、直前までの文脈を引き継ぐ。ユーザーには接続が切れたことを悟らせない、というのが目標になります。
async def resilient_translate (mic_frames, max_retries = 5 ):
backoff = 0.5
attempt = 0
while attempt <= max_retries:
try :
async for audio in translate_stream(mic_frames):
yield audio
return # 正常終了
except ( ConnectionError , asyncio.TimeoutError) as e:
attempt += 1
wait = min (backoff * ( 2 ** (attempt - 1 )), 8.0 )
print ( f "再接続を試みます( { attempt } 回目 / { wait :.1f } 秒後): { e } " )
await asyncio.sleep(wait)
raise RuntimeError ( "再接続の上限に達しました" )
クライアント直結の構成では、API キーを端末に埋め込まないために ephemeral token(短命トークン)を使い、再接続のたびにサーバ側で発行し直します。キーの寿命とセッションの寿命を分けて考えることが、本番運用では効いてきます。
指数バックオフの上限を設けているのは、圏外が続く状況で無限に再接続を試みると、復帰した瞬間に大量のリクエストが集中し、かえって課金と負荷を跳ね上げるからです。復帰時の挙動まで含めて設計するのが、放置できる運用の条件です。
ストリーミング音声のコストは、静かに積み上がる
ここが個人開発では最も見落としやすい部分です。バッチ処理と違い、Live API はセッションを開いている間、音声の入出力トークンが秒単位で積み上がります。
コストの当たりをつけるには、1 会話あたりの音声秒数から逆算します。仮に入出力あわせて 1 分あたり X 円かかるとして、平均 3 分の会話が 1 日 100 回発生すれば、月間はおおよそ次のように見積もれます。
変数 値
1 会話の平均秒数 180 秒
1 日の会話数 100 回
月間の音声分数 180 × 100 × 30 ÷ 60 = 9,000 分
月間コスト 9,000 × X 円
数式そのものより大切なのは、削るべき変数がどこにあるかを知ることです。効くのは「無音や待機中にセッションを開けっ放しにしない」の一点に尽きます。ユーザーが考え込んで黙っている数十秒も、セッションが開いていれば課金対象になり得ます。発話が一定時間途切れたらセッションを閉じ、次の発話で開き直す。この切り替えだけで、実測では待機の多いアプリほど大きく効きます。
そして、そもそも Live が要らない場面を見極めることです。リアルタイムの往復が体験の核でないなら、録音してからバッチで書き起こし・翻訳するほうが、実測ではおよそ10倍ほど安く済みます。次の基準で切り分けています。
要件 向いている方式
対面の会話・通訳・接客 Live API(双方向ストリーミング)
動画やナレーションの事前翻訳 バッチ書き起こし+合成
問い合わせへの音声応答 短い発話なら Live、長文ならバッチ
公式ドキュメントには書かれていない運用の勘所
最後に、実際に組んでみて初めて分かった点をいくつか残しておきます。
書き起こしは音声より遅れて届くことがあります。字幕と音声を厳密に同期させたい場合は、音声チャンクにシーケンス番号を振り、書き起こしが来たら後から位置合わせする作りにしておくと安全です。先に音声を鳴らし、字幕は追って表示する、と割り切るのも一つの解です。
バージイン(相手の発話への割り込み)を許すなら、こちらが再生中の音声を即座に止められるようにしておく必要があります。ユーザーが話し始めたのに前の訳がまだ流れていると、二重の音声が重なって会話が壊れます。再生バッファをいつでも破棄できる構造にしておくこと。
そして、システムプロンプトに「要約や補足をしない」と明示しておくこと。通訳の指示を曖昧にすると、モデルは親切心から言い換えや補足を足しがちで、それが通訳としては雑音になります。訳す範囲を狭く固定するほうが、結果は誠実になります。
音声から音声への翻訳は、ようやく「その人の言葉のまま届ける」ことに近づいた技術だと感じています。まずは Live API の最小構成でセッションを一本張り、自分の声が別の言語で返ってくる往復を体験してみてください。設計の勘所は、そこから自然と見えてきます。
実装の一助になれば幸いです。お読みいただき、ありがとうございました。