Gemini 3.5 Transcribeでの会議文字起こし実践:話者分離、タイムスタンプ、カスタム語彙の活用
Gemini 3.5 Transcribeを活用して、45分の会議音声録音から検証済みの議事録や字幕ベースラインを作成するための実践ガイド。適切な設定フラグの選定、長尺音声ファイルの正しい分割方法、Python SDKを用いた単語単位タイムスタンプの統合手法、そして手動での品質検証プロトコルまでを詳細に解説します。
目次

ビジネスミーティングの音声録音を信頼性の高い実務ドキュメントへと変換するには、音声認識パラメータの設計において慎重なアーキテクチャ上の判断が求められます。Gemini 3.5 Transcribe には、すべてを一度に満たす「万能(All-in-one)」なモードは存在しません。高度な編集・正規化(smart)、話者分離(diarization_mode)、単語レベルのタイムスタンプグリッド(timestamp_granularities)、そして業界固有の用語辞書(custom_vocabulary)は、それぞれ機能的に独立した API 機能として提供されており、特定のパラメータの組み合わせにおいてのみ競合します。
どのような処理パイプラインを構築する場合でも、まずは最終的なゴールを明確に定義することから始める必要があります。ざっと目を通すための簡潔で読みやすいテキストが必要なのか、後続の分析パイプラインに渡すための構造化データが必要なのか、あるいは動画編集用の正確なタイムスタンプ配列が必要なのかによって、採用すべきアプローチはまったく異なります。互換性のない排他的なパラメータを同時に渡そうとすると、リクエストが実際に処理される前のスキーマ検証段階でエラーが発生してしまいます。
以下では、技術用語が頻出する2名の話者による45分の会議録音を題材に、エンドツーエンドの実践的なワークフローを解説します。公式ドキュメントに記載された API の制限事項をクリアし、公式 Python SDK を使って2つの分割パートを処理し、初期ドラフトのデータ構造を構築した上で、元の音声と照合して検証・修正を行い、最終的に検証済みのデータを議事録担当者や字幕エディターへ安全に引き渡すまでの具体的な手順を紹介します。
アーキテクチャ上の制約とパラメータの競合
本パイプラインの具体例として、典型的な業務ミーティングのシナリオを想定してみましょう。テックリードのアレクセイ(Alexey)とプロダクトマネージャーのミハイル(Mikhail)が、社内プロジェクト名 DataPulse および CloudForge における移行アーキテクチャとサービスデプロイについて45分間議論しています。会話の中には専門用語や外来語、相づち、割り込み、言い淀みなどが多数含まれています。
Gemini 3.5 Transcribe をシステムに統合する際には、以下の3つの厳密な制約を考慮しなければなりません。
- カスタム語彙と構造化メタデータの非互換性:
custom_vocabularyパラメータ(最大 1,000 語句まで指定可能、実務上の推奨は 100 語句程度まで)は、diarization_modeやtimestamp_granularitiesと同時に指定することはできません。そのため、設計上のトレードオフを選択する必要があります。すなわち、モデルに固有の製品名やブランド名を正確に認識させる(自動的な話者ラベルやタイムスタンプの取得は諦める)か、詳細な話者情報と時間軸アライメントを取得しつつ専門用語の修正は後処理で行うかのどちらかです。 - スマートモードと時間軸アライメントの非互換性:
smartモードでは音声の正規化が適用され、言い淀みやフィラー、言い直しが自動的に除去されるとともに、文法的な構文の再構築が行われます。トークンが削除、結合、または並べ替えられるため、モデルは生成されたテキストを出力音声ストリームの元の時間軸と対応付けることができません。そのため、smartモードでは話者の識別や単語単位のタイムスタンプは利用できません。さらに、このモードが返すのはアノテーションのないプレーンな連続テキスト(output_text)のみであり、アクションアイテムなどを自動抽出するわけではありません。 - 高度なメタデータ取得時における30分の制限: 詳細なアノテーションを含まない標準的な文字起こしリクエストでは、最大 60 分までの音声ファイルを受け付けます。しかし、
diarization_modeやtimestamp_granularitiesを有効にすると、公式ドキュメントで規定されたファイル長の最大値は 30 分 に制限されます。この閾値を超えるファイルを送信することはサポートされた API 仕様の範囲外となるため、実行してはなりません。長尺の音声は、API へ送信する前にクライアント側で分割する必要があります。
45分の録音データの準備と分割
45分の録音ファイルから話者分離と単語単位のタイムスタンプを取得するには、元のファイルを確実に30分未満のチャンクに分割しなければなりません。今回の仮想シナリオでは、録音を以下の2つのセグメントに分割します。
- パート1: 00:00–25:00(計算をわかりやすくするため、正確に 1500.0 秒と仮定)
- パート2: 25:00–45:00(残りの 20 分間、すなわち 1200.0 秒)
ここでの 1500.0 秒という境界は、本例の計算を単純化するために選定したものです。本番環境では、発話の合間にある自然な沈黙や区切りで分割すべきであり、第1セグメントの正確な物理的長さは、ffprobe などの解析ツールを用いてメディアファイルのメタデータから直接取得する必要があります。
音声を分割することにより、連続性に関する2つの重要な課題が生じます。
- 第2セグメントにおけるタイムスタンプのリセット: API は第2セグメントを完全に独立したファイルとして処理するため、内部の単語レベルのオフセットは
0.000sからリセットされて開始します。会議全体の連続したタイムラインを再構築するには、第1セグメントの実際の長さを、第2セグメントから抽出されたすべての単語タイムスタンプへプログラム的に加算しなければなりません。 - 話者ラベルの局所性: モデルは、個々の API 呼び出しごとに独立して
spk_1やspk_2といった話者識別子を割り当てます。第1チャンクでspk_1と指定された話者が、第2チャンクではspk_2に割り当てられる可能性があります。検証を行わずに異なるリクエスト間で同じ技術ラベルをそのまま結合すると、話者の帰属関係が崩壊してしまいます。各チャンクについて、耳による音声のスポットチェックを通じて実際の発言者とマッピングした独立した対応表を用意する必要があります。
タスクに応じた設定バリエーション
文字起こしのパラメータは、generation_config 辞書の transcription_config フィールド内で設定します。以下に、運用のニーズに応じた代表的な基本設定を示します。
話者ラベルやタイムスタンプを必要とせず、文脈の整った正規化済みの議事メモを作成する場合(60分未満の単一ファイル全体に適しています):
generation_config = {
"transcription_config": {
"mode": "smart"
}
}
固有の製品名や社内専門用語が多く含まれ、正確なスペルでの文字起こしを最優先する場合:
generation_config = {
"transcription_config": {
"custom_vocabulary": ["DataPulse", "CloudForge", "ClickHouse", "gRPC"]
}
}
話者ごとの発言記録や字幕用のタイミンググリッドを生成する場合(音声は30分以内の制限に従う必要があります):
generation_config = {
"transcription_config": {
"mode": {
"type": "verbatim",
"diarization_mode": "speaker",
"timestamp_granularities": ["word"],
}
}
}
Python SDKによる完全なパイプライン:2つのパートの処理
このスクリプトは、音声の視聴や手動検証を行う前の段階で実行されます。Files API を介して両セグメントをアップロードし、Interactions API にリクエストを送信して、単語単位のアノテーションを抽出します。そしてタイムスタンプと話者識別子を検証し、第2パートのタイムラインを 1500.0 秒シフトした上で、以下の初期ドラフトファイルを出力します。
meeting_transcript.txt— 発話ターンごとに整理された、UTF-8 形式の会話ログドラフトword_timestamps.json— 字幕の区切り処理などに利用する、連続した単語単位のタイムスタンプ配列ドラフト
このスクリプトによって生成されるファイルは、あくまで未検証のドラフトであり、確定版の成果物ではありません。担当者は必ず音声録音を実際に聞き、技術的な話者トークンと実際の声をマッピングし(話者マッピング)、専門用語の表記を確認し(用語確認)、時間オフセットの整合性を監査(タイムオフセット監査)する必要があります。この監査を終えた後、コード内の設定パラメータを更新してパイプラインを再実行するか、生成されたテキストファイルを直接修正します。このような検証プロトコルを経て初めて、議事録担当者や字幕エディターに最終コピーを引き渡すことができます。
このスクリプトはアナリストや動画編集者向けの中間データ構造を出力するものであり、アクションアイテムやフォーマット済みの .srt 字幕ファイルを自動生成するものではありません。なお、スクリプト内の話者名や 1500.0 秒の境界値は、本仮想シナリオのデモンストレーション用として指定されたものです。
import json
from google import genai
client = genai.Client()
audio_part1 = client.files.upload(file="meeting_part1.mp3")
audio_part2 = client.files.upload(file="meeting_part2.mp3")
transcription_mode_config = {
"transcription_config": {
"mode": {
"type": "verbatim",
"diarization_mode": "speaker",
"timestamp_granularities": ["word"],
}
}
}
interaction_part1 = client.interactions.create(
model="gemini-3.5-transcribe",
input=[
{
"type": "audio",
"uri": audio_part1.uri,
"mime_type": audio_part1.mime_type,
}
],
generation_config=transcription_mode_config,
)
interaction_part2 = client.interactions.create(
model="gemini-3.5-transcribe",
input=[
{
"type": "audio",
"uri": audio_part2.uri,
"mime_type": audio_part2.mime_type,
}
],
generation_config=transcription_mode_config,
)
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
def parse_offset_seconds(offset_val, word_text):
if offset_val is None or offset_val == "":
raise ValueError(f"Отсутствует таймкод для слова '{word_text}'. Требуется проверка аудиозаписи.")
val_str = str(offset_val)
if val_str.endswith("s"):
val_str = val_str[:-1]
try:
return float(val_str)
except ValueError:
raise ValueError(f"Некорректный формат таймкода '{offset_val}' для слова '{word_text}'. Требуется проверка аудиозаписи.")
words_part1 = extract_word_annotations(interaction_part1)
words_part2 = extract_word_annotations(interaction_part2)
if not words_part1 or not words_part2:
raise ValueError("Один из аудиосегментов не содержит пословных аннотаций. Пустой результат не может считаться успешным.")
part1_offset_seconds = 0.0
part2_offset_seconds = 1500.0
manual_mapping_part1 = {
"spk_1": "Алексей",
"spk_2": "Михаил",
}
manual_mapping_part2 = {
"spk_1": "Михаил",
"spk_2": "Алексей",
}
unified_word_stream = []
for word in words_part1:
text = getattr(word, "text", "")
raw_speaker = getattr(word, "speaker", None)
if not raw_speaker or raw_speaker not in manual_mapping_part1:
raise ValueError(f"Неизвестный спикер '{raw_speaker}' в части 1. Требуется ручная верификация по аудио.")
resolved_speaker = manual_mapping_part1[raw_speaker]
word_start = parse_offset_seconds(getattr(word, "start_offset", None), text) + part1_offset_seconds
word_end = parse_offset_seconds(getattr(word, "end_offset", None), text) + part1_offset_seconds
unified_word_stream.append({
"text": text,
"speaker": resolved_speaker,
"start_seconds": word_start,
"end_seconds": word_end,
"part": 1,
})
for word in words_part2:
text = getattr(word, "text", "")
raw_speaker = getattr(word, "speaker", None)
if not raw_speaker or raw_speaker not in manual_mapping_part2:
raise ValueError(f"Неизвестный спикер '{raw_speaker}' в части 2. Требуется ручная верификация по аудио.")
resolved_speaker = manual_mapping_part2[raw_speaker]
word_start = parse_offset_seconds(getattr(word, "start_offset", None), text) + part2_offset_seconds
word_end = parse_offset_seconds(getattr(word, "end_offset", None), text) + part2_offset_seconds
unified_word_stream.append({
"text": text,
"speaker": resolved_speaker,
"start_seconds": word_start,
"end_seconds": word_end,
"part": 2,
})
dialogue_turns = []
current_turn = None
for item in unified_word_stream:
if current_turn is None or current_turn["speaker"] != item["speaker"] or current_turn["part"] != item["part"]:
if current_turn is not None:
dialogue_turns.append(current_turn)
current_turn = {
"part": item["part"],
"speaker": item["speaker"],
"start_seconds": item["start_seconds"],
"end_seconds": item["end_seconds"],
"words": [item["text"]],
}
else:
current_turn["end_seconds"] = item["end_seconds"]
current_turn["words"].append(item["text"])
if current_turn is not None:
dialogue_turns.append(current_turn)
def format_timestamp(seconds):
minutes = int(seconds // 60)
remaining_seconds = seconds % 60
return f"{minutes:02d}:{remaining_seconds:06.3f}"
with open("meeting_transcript.txt", "w", encoding="utf-8") as f_transcript:
for turn in dialogue_turns:
start_str = format_timestamp(turn["start_seconds"])
end_str = format_timestamp(turn["end_seconds"])
speech_text = " ".join(turn["words"])
f_transcript.write(f"[{start_str} - {end_str}] {turn['speaker']}: {speech_text}\n")
with open("word_timestamps.json", "w", encoding="utf-8") as f_json:
json.dump(unified_word_stream, f_json, ensure_ascii=False, indent=2)
単語レベル構造の具体例と点検ポイント
以下は、話者の交代部分における抽出データストリームの構造を示すサンプルです。
注記: このスニペットは返却されるオブジェクトの構造を説明するための例示であり、実際の API 呼び出しログではありません。実際の文字起こし精度は音響環境、マイクの品質、発話の明瞭さによって変動します。
[Строка 1] [spk_1] (0.100s -> 0.420s) Мы
[Строка 2] [spk_1] (0.450s -> 0.810s) переносим
[Строка 3] [spk_1] (0.830s -> 1.250s) ДатаПульс
[Строка 4] [spk_1] (1.300s -> 1.550s) на
[Строка 5] [spk_1] (1.600s -> 2.100s) CloudForge.
[Строка 6] [spk_1] (2.300s -> 2.600s) Да,
[Строка 7] [spk_2] (2.650s -> 2.900s) согласен,
[Строка 8] [spk_2] (2.950s -> 3.400s) э-э-э,
[Строка 9] [spk_2] (3.420s -> 3.900s) логично.
手動レビューにおける必須チェック項目:
- 実参加者へのラベルのマッピング(1〜5行目および7〜9行目):
spk_1やspk_2という識別子は、音声上の出現順に基づく暫定的なトークンにすぎません。エディターは各セグメントの冒頭を聞き、実際の人物との対応関係を特定する必要があります(例:パート1においてspk_1がアレクセイ、spk_2がミハイルであることを確認する)。 - 発話の交代と短い相づち(6行目): 「Да,」という単語が
spk_1のターンに分類されています。実際のテンポの良い会話では、これは聞き手(spk_2)による相づちや短い同意である可能性があります。話者が切り替わる境界部分は、音声による重点的な照合が必要です。 - 未認識ブランドの音訳表記(3行目):
custom_vocabularyを無効にしている場合、英語の専門用語やブランド名がキリル文字などで表音通りに書き起こされることがあります(例:ДатаПульс)。エディターは、これらを本来の正式表記であるDataPulseに修正しなければなりません。 - 無分別な一括置換の禁止: 用語の修正は、文脈を確認しながら個別に行う必要があります。ドキュメント全体に対して無条件の「全置換」を実行すると、発音が類似した一般的な単語や慣用句、引用箇所などを誤って書き換えてしまうリスクがあります。
- 言い淀みとフィラー(8行目):
verbatimモードでは、「э-э-э」のような言い淀みやフィラーも忠実に記録されます。整った議事録を作成する場合はこれらを除去しますが、動画の字幕トラックを作成する際、画面上の口の動きや発話のタイミングと同期させる必要がある場合は、そのタイムスタンプを維持します。
成果物引き渡し前のスポットチェックプロトコル
プログラムによって結合された未加工の生データは、後続の工程に引き渡す前に、以下の3つの検証チェックポイントに基づく重点的な品質監査を必ず通過しなければなりません。
- 話者と発話境界の検証: 話者が交代する2〜3箇所のタイミングについて、前後15〜20秒の音声を聴取します。異なる話者の声が1つの話者ラベルに統合されてしまっていないか、逆に1人の連続した発言が不自然な幽霊ラベルに分断されていないかを確認します。なお、公式ドキュメントによると、3名以上が参加する会議の話者分離は実験的な機能と位置付けられており、より一層綿密な確認が必要です。
- 用語、数値、固有表現の検証:
主要なプロジェクト名(
DataPulse、CloudForge、バージョン番号、予算配分など)をリストアップしたチェックリストを作成します。テキスト内を検索し、不確実な箇所を音声と直接照合します。その際、無分別な一括置換は避けてください。 - セグメント境界におけるタイムライン連続性の検証: パート1の開始1分間と、パート2への移行点(25:00 / 1500.0 秒の境界直後)を比較します。第2セグメントのタイムスタンプがゼロにリセットされることなく、パート1からシームレスに連続していることを確認します。
後続チームへの引き渡しとプロセスの停止条件
スクリプトによって生成された meeting_transcript.txt および word_timestamps.json は、音声の確認前に作成されたものであり、あくまで初期ドラフトとして扱わなければなりません。未加工の状態で後続の担当者へ引き渡すことは厳禁です。必ず手動検証プロトコルを実行し、話者の特定、専門用語、タイムラインのオフセットを音声と照合して修正(コード内のパラメータを更新して再実行するか、ファイルを直接編集)した上で、検証済みのコピーのみを後続フローへリリースしてください。
- 議事録担当者または要約モデル(Note Taker / LLM Summarization)向け: 正確な話者属性と正規化された専門用語が確認された、検証済みの会議トランスクリプトを提供します。これはエグゼクティブサマリーや決定事項ログの信頼できる基礎となりますが、それ自体が自動的にアクションアイテムのリストを構成するわけではありません。
- 動画編集者または字幕スペシャリスト(Caption Editor)向け: 連続したタイムスタンプに紐付けられた、検証済みの単語タイムスタンプ配列を提供します。画面上の表示時間制限、1行あたりの文字数上限、読書速度などのスタイリングルールは、対象の動画プレーヤーや言語に依存するため、その後専用のフォーマットツールを用いて最終的な
.srtや.vtt字幕ファイルを生成します。
プロセス停止条件
以下のいずれかの条件に該当する場合は、ワークフローを直ちに中断し、後続チームへファイルを渡してはなりません。
- 事前分割を行わずに、30分を超える長さの音声を話者分離または単語タイムスタンプ取得のために送信した場合(公式ドキュメントの制限を超過しており、API に送信してはなりません)。
- 音声のスポットチェックや手動レビューを経ることなく、スクリプト実行直後の未加工ドラフトファイル(
meeting_transcript.txtやword_timestamps.json)を後続の利用者に引き渡した場合。 - 音声を聞くことなく、第1セグメントのラベル番号に基づいて第2セグメントの話者ラベルを機械的にマッピングした場合、あるいはデータセット内に未解決の話者識別子が残っている場合。
- 第2セグメントのタイムラインに第1セグメントの物理的な長さが加算されていない場合、または単語タイムスタンプの欠落が検出された場合。
- 文脈の検証を行わずに用語の無分別な全置換を適用した場合、あるいは重要な設定パラメータや数値データが録音音声と照合されずに放置されている場合。