Gemini 3.8 TTSが指示まで読み上げるときはspeech_metadataを確認
Gemini 3.8 Flash TTSの台本と話し方の指示を分離し、Interactions APIへの移行と音声応答の確認手順を解説します。
Gemini 3.8 Flash TTSでは、発話する言葉は台本へ、継続的な話し方の指示は音声メタデータへ分けます。 文字どおりの台本として扱われるテキストに「落ち着いて話して」と書くと、その指示自体が読み上げられる可能性があります。Gemini 3.8 Flash TTSの移行ガイドは台本と話し方のメタデータを明確に区別しています。
Googleは2026年9月22日のAPIリリースノートで新しいTTSモデルを発表しました。本記事はInteractions REST APIの公式資料に沿った移行例です。リクエスト構造とデコード処理はローカルで確認できますが、実際の音声を聴く試験、音質の向上、このエンドポイントの現在のOfox対応を主張するものではありません。
発話内容と話し方を分ける
| 情報 | この例での配置先 |
|---|---|
| 聞き手に届けたい言葉 | textコンテンツのtextフィールド |
| 落ち着いて明瞭に、など継続する話し方 | speech_metadata注釈のstyle |
| 音声の選択 | generation_config.speech_config |
| 音声出力の指定 | response_format |
短い指示でも分けておけば、台本との混同を避け、リクエストを確認しやすくなります。ただし登場人物が「落ち着いて話して」と言う台本なら、その言葉は聞かせる内容なので台本に含めます。
モデル文書には特定の時点に入れる発声イベントの説明もあります。以前のプロンプト用タグや任意の注釈がすべて使えると考えず、選んだモデルとAPIの現行リファレンスを確認してください。
旧ナレーション要求をフィールドごとに移す
旧プロンプトを一つの文字列として移すのではなく、確定した読み上げ原稿から始めます。たとえば旧入力が「Read warmly as a station announcer: The next train leaves at noon.」なら、発話させるのは後半の一文だけです。温かい駅アナウンス風の指示は style に置き、原稿から外します。これは編集例であり、音声生成の結果ではありません。
最初の要求では声と文を変えず、旧要求を別ファイルに保存して、次節の Interactions 要求を作ります。GenerateContent の contents、parts、speech_metadata の位置や応答処理を持ち込まないでください。同じような名前でも JSON 内の階層は異なります。
現行資料では継続的なスタイルと瞬間的な発声イベントを区別します。文全体をささやく指示はメタデータへ、短い <sigh> は実際にため息を入れたい位置の原稿に残せます。すべての指示を独自タグに変えないでください。確定した台詞に指示のような語句が含まれる場合は、そのまま本文に残します。
二人の会話なら、ターン番号、正確な台詞、speaker ID、style、設定する声を表にします。全ターンで設定と一致する speaker を明示します。原稿中の Speaker 1: は構造化 speaker フィールドの代わりにはならず、発話される可能性があります。長い会話の前に対応を確定してください。一人用の声配列をコピーするだけでは複数話者の設定は完成しません。
リクエストから応答まで同じAPI形式を使う
以下はInteractions APIの例です。input配列やannotationsをGenerateContentの本文、または旧SDKのフィールドと混ぜないでください。この形式の出典は公式音声生成ガイドです。
次の内容をrequest.jsonに保存します。英語の短い台本を使った例をそのまま示します。
{
"model": "gemini-3.8-flash-tts",
"input": [{
"type": "user_input",
"content": [{
"type": "text",
"text": "The next train leaves at noon.",
"annotations": [{
"type": "speech_metadata",
"style": "calm and clear"
}]
}]
}],
"response_format": {"type": "audio"},
"generation_config": {
"speech_config": [{"voice": "Kore"}]
}
}
権限のある直接Googleアカウントでのリクエストは次のとおりです。
curl --fail-with-body --silent --show-error \
'https://generativelanguage.googleapis.com/v1beta/interactions' \
-H "x-goog-api-key: $GEMINI_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary @request.json > response.json
自分のキーを環境変数から安全に渡してください。このコマンドは実際のAPIリクエストを送り、利用料金が発生する場合があります。ローカルでJSONを解析できても、モデルへのアクセス権や提供者による受理は確認できません。
これは一人の話者の例です。公式の複数話者設定は構造が異なります。上のspeech_config配列を独自の会話スキーマに作り変えないでください。各発話の話者は設定済みの話者と一致させる必要があります。
応答を確認してから音声をデコードする
response.jsonに保存されたHTTPエラーは音声ではありません。base64をデコードする前にHTTP結果と応答構造を確認します。InteractionsのREST応答ではmodel_outputステップとそのcontentブロックを調べます。SDKの便利なプロパティであるoutput_audioが生のJSONにもあるとは限りません。
堅牢なデコーダーでは次を確認します。
- エラー応答を音声ファイルとして書き出さない。
- model_outputの音声コンテンツを選び、テキストやツール内容は除く。
- 返されたMIMEタイプを確認してから拡張子を決める。
- base64をデコードし、元のコンテナを保持する。
移行ガイドによると非ストリーミング応答の既定出力はWAVです。古い生PCMの例からWAVヘッダーを無条件に追加しないでください。既に正しいWAVに二重のヘッダーを付けると破損する場合があります。別形式を指定した場合も、拡張子だけを変えるのではなく実際のMIMEタイプとコンテナに従います。
小さなデコーダーで確認済みの WAV を保存する
この非ストリーミング応答の例では、response_format を次に変更して WAV を明示します。
{"type":"audio","mime_type":"audio/wav"}
Interactions の応答リファレンスに step/content 構造があります。次を response.json と同じフォルダーの decode_tts.py に保存し、python3 decode_tts.py を実行します。Python 標準ライブラリーだけを使用し、ネットワーク要求は送りません。
import base64
import io
import json
import wave
from pathlib import Path
body = json.loads(Path('response.json').read_text())
if body.get('error') or body.get('status') != 'completed':
raise SystemExit('Interaction failed or is not completed; inspect response.json')
blocks = [
item
for step in body.get('steps', [])
if step.get('type') == 'model_output'
for item in step.get('content', [])
if item.get('type') == 'audio'
]
if len(blocks) != 1:
raise SystemExit('Expected one audio block; inspect the response before combining audio')
audio = blocks[0]
if audio.get('mime_type') != 'audio/wav' or not isinstance(audio.get('data'), str):
raise SystemExit('Expected inline audio/wav data; inspect MIME type and delivery mode')
payload = base64.b64decode(audio['data'], validate=True)
if payload[:4] != b'RIFF' or payload[8:12] != b'WAVE':
raise SystemExit('Payload is not a RIFF/WAVE file')
with wave.open(io.BytesIO(payload), 'rb') as wav:
frames = wav.getnframes()
rate = wav.getframerate()
if frames == 0 or rate == 0:
raise SystemExit('Audio contains no usable frames')
print({'channels': wav.getnchannels(), 'sample_rate': rate,
'duration_seconds': frames / rate})
Path('speech.wav').write_bytes(payload)
print('Saved speech.wav')
期待されるローカル出力はメタデータと Saved speech.wav です。長さとサンプルレートは実ファイルから取得し、固定値は約束しません。音声ブロックが複数、処理が未完了、URI 配信、別のエンコーディングの場合は停止します。単一結果の WAV 専用で、汎用ストリーミングクライアントではありません。不正な base64 や非対応 WAV もエラーにし、誤解を招くファイルを黙って保存しません。
このデコーダーは、ローカル生成した PCM WAV と不正データ・エラー応答で確認しました。確認できるのは解析と拒否の処理だけであり、アカウントの権限、Google の実応答、発音、音質ではありません。
コンテナーの問題と発話内容の問題を分ける
speech.wav を開けないなら、原稿を変える前に HTTP ステータス、MIME、バイト列を調べます。WAV に二重のヘッダーを付けても台詞の問題は直りません。逆にメタデータが正しく再生可能でも、指定したすべての語を読んだ証拠にはなりません。
| 症状 | 確認と修正 |
|---|---|
| 演出指示まで読む | 確定原稿と比較し、不要な指示を本文から style へ移す |
| 話者ラベルを読む | 本文からラベルを外し、設定と一致する speaker メタデータを使う |
| JSON エラーを音声保存 | HTTP/interaction エラーで停止し、エラーオブジェクトをデコードしない |
| WAV が壊れる、雑音になる | コンテナーとエンコーディングを確認し、既に WAV なら旧 PCM 用ヘッダー処理を除く |
| 無音または途中で切れる | 完了状態と終了情報を調べ、文字量・モデル制限を確認してから分割する |
| 会話中に意図せず声が変わる | ターンと speaker の対応を調べ、設定した声を短い一文ずつ試す |
data がない | URI 配信やストリーミングの設定を確認し、その方式の手順で処理する |
ストリーミングは別の実装課題です。チャンクは独立した WAV とは限らず、単純なバイト結合では扱えません。完成済み WAV 同士を結合しても途中に余分なヘッダーが残ります。最初は非ストリーミングのまま移行し、後で配信や連結が必要になったら、資料どおりの PCM 形式とタイミングを明示的に処理します。
最初の移行テストを小さくする
一人の話者、短い台本、一つの話し方の指示から始めます。単語の欠落、余分な指示の読み上げ、発音、意図しない声の変化を聴いて確認します。リクエスト、モデルID、時刻、出力を一緒に保存してください。これらは受け入れ確認の提案であり、本記事で得た実測結果ではありません。
その後に台本を長くしたり話者を追加したりします。一度に変える要素は一つにします。メタデータへの移動と同時に声の変更、台本分割、API切り替えまで行うと原因を絞れません。
ナレーションでは映像に組み込む前に、生成音声と正確な台本を照合します。顔出しなし動画の制作ガイドは全体の工程を扱います。TTS移行はその一段階であり、タイミングや発音、特定の声の利用同意まで保証するものではありません。
動画へ入れる前にナレーションを検収する
長い原稿を生成する前に合格条件を決めます。列車の例なら、承認した一文だけを欠落なく読み、「calm and clear」や話者の接頭辞を加えず、時刻を落とさず、末尾を繰り返さないことです。発音、クリッピング、不要な無音、声の変化は別々に記録します。形式が正しいファイルでも、これらは失敗し得ます。
実用的な移行テストには、中立的な短文、プロジェクトで使う固有名詞や数字を含む一文を用意し、複数話者が必要な製品だけ二ターン会話を追加します。指示をメタデータへ移す間は原稿と声を固定します。要求と出力、試聴メモを保存し、定義した分母で全実行を採点していないなら成功率は発表しません。
合格した音声の実際の長さに画面を合わせます。動画を 15 秒と指定しても TTS が 15 秒になるわけではありません。原稿やタイムラインを意図的に調整し、速度変更後は再度聞いてください。元音声を残せば、編集ミスと生成時の問題を分けられます。これは指示の配置から利用できる素材までを移行する手順であり、音質ベンチマークではありません。
別の提供者を経由する前の確認
OpenAI互換のテキストAPIがあるだけでは、GoogleのInteractions APIや音声メタデータへの対応は分かりません。音声専用ルート、対応モデルID、出力形式を確認します。本記事の直接Google向け例はOfoxの接続設定例ではありません。
モデル選択とAPI形式の移行も分けます。関連するGemini 3.8 Flash-Lite TTSに文字列だけを変更する前に、そのモデルの資料で対応と挙動を確認してください。マルチモーダルAPIの概要(英語)は全体像の参考になりますが、現行の提供者文書に代わるものではありません。
よくある質問
- 話し方の指示まで読まれるのはなぜですか?
- 新しいモデルは入力テキストを文字どおりの台本として扱います。継続的な話し方の指示は台本に混ぜず、文書で指定された音声メタデータへ置いてください。
- このJSONをGenerateContentに使えますか?
- いいえ。この例はInteractionsの入力・注釈フィールドを使います。GenerateContentはリクエスト構造が異なるため、そのAPI用の公式例で全体を統一してください。
- 出力は必ず生PCMですか?
- いいえ。移行ガイドでは非ストリーミング応答の既定値はWAVです。実際の応答形式を確認し、WAVヘッダーを二重に追加しないでください。


