公式ドキュメントの「Transcription modes」の節を読み進めていて、末尾の注記で手が止まりました。smart は timestamp_granularities とも diarization_mode とも併用できない、と書かれています。
この注記の置かれている位置に、少し引っかかりました。話者ダイアライゼーションの節も、単語タイムスタンプの節も、モードの節より 前 にあります。上から順に読むと「話者ラベルが使える」「単語タイムスタンプも使える」「読みやすく整形する smart もある」と受け取ったあとで、最後にその組み合わせが成立しないと知る順序になっています。
私自身、設定値はコードに直書きせず外部ファイルへ切り出す癖があります。個人開発では、あとから自分がどこを触ればよいか分からなくなるのが一番こわいからです。ただ、今回の transcription_config に関しては、その癖がそのまま落とし穴になりました。理由は次の節に書きます。
mode は文字列とオブジェクトの両方を受け取ります
gemini-3.5-transcribe の設定は、generation_config の中の transcription_config に集約されています。ここの mode が少し変わっていて、文字列とオブジェクトのどちらも受け取ります。
from google import genai
client = genai.Client()
audio_file = client.files.upload(file="path/to/sample.mp3")
# 文字列で渡す形(smart)
interaction = client.interactions.create(
model="gemini-3.5-transcribe",
input=[{"type": "audio", "uri": audio_file.uri,
"mime_type": audio_file.mime_type}],
generation_config={
"transcription_config": {"mode": "smart"}
},
)
# オブジェクトで渡す形(verbatim + 話者ラベル + 単語タイムスタンプ)
interaction = client.interactions.create(
model="gemini-3.5-transcribe",
input=[{"type": "audio", "uri": audio_file.uri,
"mime_type": audio_file.mime_type}],
generation_config={
"transcription_config": {
"mode": {
"type": "verbatim",
"diarization_mode": "speaker",
"timestamp_granularities": ["word"],
}
}
},
)同じキーが2つの型を取ります。手で書いている間は気になりません。問題は、設定を JSON や YAML へ外出しして「モードだけ差し替えられるように」した瞬間に起きます。
mode: smart と書いていた行を、話者ラベルを足すためにオブジェクトへ書き換える。逆に「読みやすくしたい」と mode: smart へ戻すと、オブジェクトの中に置いていた diarization_mode が設定ごと消えます。どちらの向きでも、消えたことに気づく手がかりがありません。エラーとして返ってこないからです。
さらに紛らわしいのが、diarization_mode と timestamp_granularities の 置き場所 です。この2つは transcription_config の直下ではなく、mode オブジェクトの中に入ります。custom_vocabulary と language_codes は直下です。階層が一段ずれます。
smart を選んだ時点で外れるもの、verbatim を選んだ時点で背負うもの
両モードで何が使えるのかを、先に一枚で持っておくと迷いません。
| 機能 | verbatim(既定) | smart |
|---|---|---|
話者ダイアライゼーション(diarization_mode) | 使える(最大8話者) | 使えない |
単語タイムスタンプ(timestamp_granularities) | 使える | 使えない |
| フィラー語の除去・整形 | されない(発話どおり) | される |
| 言い直しのインライン解決 | されない | される |
カスタム語彙(custom_vocabulary) | 使える | 使える |
言語コード指定(language_codes) | 使える | 使える |
verbatim 側にも代償があります。単語タイムスタンプを有効にすると、文字起こし全体の精度が落ちる可能性がある、と公式に明記されています。時刻が要らない用途で「念のため」有効にしておく設計は、精度を捨てているだけになります。
エンドポイントによる差も別にあります。非ストリーミングの gemini-3.5-transcribe と、Live API 上の gemini-3.5-transcribe-live では、使える機能が同じではありません。
| 項目 | gemini-3.5-transcribe | gemini-3.5-transcribe-live |
|---|---|---|
| 話者ダイアライゼーション | 対応(3話者以上は実験的) | 非対応 |
| 単語タイムスタンプ | 対応 | 非対応 |
| カスタム語彙 | 最大1,000語 | 最大1,000語 |
| 音声の長さ | 最大1時間 | 1セッション10分 |
「リアルタイムで話者を分けて表示する」画面を先に設計してしまうと、ここで手戻りが出ます。話者ラベルが要るなら、ストリーミングではなくファイル処理側に寄せる判断が先に来ます。
verbatim を選ぶ前に、もう一点だけ知っておきたいことがあります。増えた情報は、思っている場所には返ってきません。文字起こしの本文は従来どおり interaction.output_text に入りますが、話者ラベルと単語の時刻は word_info という注釈として、インタラクションのコンテンツ側にぶら下がります。取り出すには階層をたどる必要があります。
def extract_word_annotations(interaction):
words = []
for step in getattr(interaction, "steps", []) or []:
for content in getattr(step, "content", []) or []:
for annotation in getattr(content, "annotations", []) or []:
if getattr(annotation, "type", None) == "word_info":
words.append(annotation)
return words各注釈は text・speaker・start_offset・end_offset を持ちます。話者ダイアライゼーションを有効にしたまま output_text だけを読んでいると、話者情報のない普通の文字起こしが返ってきたように見えます。そして「設定が効いていない」と結論づけてしまいます。同じ種類の静かな失敗が、ここにもう一度現れます。
送る前に弾く仕組みを、80行だけ書いておきます
組み合わせの制約は覚えておけるほど少なくありません。私は、設定を組み立てた直後に検証する小さな関数を挟むようにしました。API を呼ぶ前に落ちるので、音声のアップロードも課金も発生しません。
"""transcription_config を送信前に検証する。"""
from typing import Any
VERBATIM_ONLY = ("timestamp_granularities", "diarization_mode")
MAX_VOCAB = 1000
RECOMMENDED_VOCAB = 100
def normalize_mode(mode: Any) -> dict:
"""mode は文字列とオブジェクトの両方を取る。辞書へ寄せる。"""
if mode is None:
return {"type": "verbatim"}
if isinstance(mode, str):
return {"type": mode}
if isinstance(mode, dict):
m = dict(mode)
m.setdefault("type", "verbatim")
return m
raise TypeError(f"mode は str か dict です: {type(mode).__name__}")
def validate(tc: dict) -> list[str]:
errors = []
mode = normalize_mode(tc.get("mode"))
mtype = mode.get("type")
if mtype not in ("verbatim", "smart"):
errors.append(f"mode.type が不明です: {mtype!r}")
if mtype == "smart":
for key in VERBATIM_ONLY:
if key in mode or key in tc:
errors.append(f"{key} は smart と併用できません(verbatim 用の設定です)")
for key in VERBATIM_ONLY:
if key in tc:
errors.append(f"{key} は transcription_config 直下ではなく mode の中に置きます")
vocab = tc.get("custom_vocabulary") or []
if len(vocab) > MAX_VOCAB:
errors.append(f"custom_vocabulary が {len(vocab)} 件です(上限 {MAX_VOCAB} 件)")
elif len(vocab) > RECOMMENDED_VOCAB:
errors.append(f"custom_vocabulary が {len(vocab)} 件です({RECOMMENDED_VOCAB} 件までを推奨)")
return errors
def audio_limit_minutes(tc: dict) -> int:
mode = normalize_mode(tc.get("mode"))
heavy = any(k in mode for k in VERBATIM_ONLY)
return 30 if heavy else 60normalize_mode を挟んでいるのは、文字列とオブジェクトの両方が来る前提を、検証側で吸収するためです。呼び出し側に「必ずオブジェクトで書いてください」と強いると、mode: smart と1行で済む書き味が失われます。
手元の6パターンに当てて、実際に出た結果がこちらです。
OK A 既定のまま 上限 60 分
OK B smart だけ 上限 60 分
NG C smart + 話者ラベル 上限 30 分
- diarization_mode は smart と併用できません(verbatim 用の設定です)
OK D verbatim + 話者 + 単語 上限 30 分
NG E 置き場所を間違えた 上限 60 分
- diarization_mode は smart と併用できません(verbatim 用の設定です)
- diarization_mode は transcription_config 直下ではなく mode の中に置きます
NG F 語彙 240 件 上限 60 分
- custom_vocabulary が 240 件です(100 件までを推奨)E のケースが、外部ファイル運用で実際に起きやすい形です。mode を文字列へ戻したときに diarization_mode だけが直下へ取り残される。API はこれを受け取っても止まりません。検証側で2行のメッセージが出たので、原因の場所がその場で分かりました。
F はエラーではなく推奨からの逸脱です。語彙は最大1,000語まで渡せますが、公式の推奨は100語程度までです。固有名詞を思いつく限り詰め込む設計は、上限に届く前に効きが鈍る可能性があります。
機能を有効にすると、扱える音声の長さが半分になります
見落としやすいのが、尺の上限です。gemini-3.5-transcribe は1リクエストあたり最大1時間の音声を受け取ります。ただし 話者ダイアライゼーションか単語タイムスタンプを有効にすると、30分に下がります。
これは設定の互換性より影響が大きい制約です。「50分の対談を、話者ラベル付きで一度に投げる」という設計は、そもそも成立しません。分割して投げ、話者ラベルをチャンクをまたいで突き合わせる工程が要ります。そして分割した各チャンクの spk_1 が、別のチャンクの spk_1 と同一人物である保証はありません。
先ほどの audio_limit_minutes() を検証と一緒に呼んでおくと、この判断を設計段階へ引き上げられます。音源の秒数が分かっているなら、上限を超える組み合わせをその場で弾けます。3話者以上の割り当ては実験的と明記されている点も、対談やインタビューを扱うなら頭に入れておきたいところです。
決める順序は、成果物から逆算します
設定を先に触ると、この記事のような衝突に何度も当たります。順序を逆にすると迷いません。
- 出力として何が必要か を先に決めます。読み物としての議事録か、検索用のインデックスか、話者ごとの発言録か。
- そこから モードが決まります。時刻や話者が要るなら verbatim、要らないなら smart です。両方は取れません。
- モードが決まってから 尺の設計 に降ります。verbatim で機能を足したなら30分が上限です。分割が要るかどうかがここで確定します。
- 最後に カスタム語彙 を積みます。固有名詞や社内用語が多い音源ほど効きます。言語が分かっているなら
language_codesも併せて指定するほうが、語彙の指示も通りやすくなります。
この順序は、設定が増えるほど効いてきます。設定値の互換性をあとから足していく設計がどう壊れるかは、Gemini API 構造化出力スキーマを本番アプリで安全に進化させる設計記録でも別の角度から書いています。エラーとして返ってこない失敗の拾い方については、Gemini API 本番運用ノート — 429・500・503 に静かに耐えるエラーハンドリングとレート制限の設計が近い話題です。
音声を扱う実装をリアルタイム側まで持っていく場合は、電話を受けるAIを本番投入する:Gemini Live API × Twilio Media Streams で構築する電話応答エージェントにストリーミング前提の設計をまとめてあります。話者ラベルが Live 側で使えないことを踏まえた構成が必要になる領域です。
まず手元の設定ファイルを開いて、mode の行と diarization_mode の階層だけ確かめてみてください。ずれていても、いまは何のエラーも出ていないはずです。
小さな確認の話にお付き合いいただき、ありがとうございました。