Invite & Earn

How invite rewards work

Share your invite link. When a friend registers through it and tops up, you receive the displayed reward on their subsequent top-ups.

Meeting Transcription in Gemini 3.5 Transcribe: Speakers, Timestamps, and Vocabulary in Practice

A practical guide to transforming a 45-minute meeting recording into a verified transcript or subtitle baseline using Gemini 3.5 Transcribe: selecting configuration flags, properly splitting long audio files, assembling word-level timestamps via the Python SDK, and executing manual quality assurance.

Contents
Meeting Transcription in Gemini 3.5 Transcribe: Speakers, Timestamps, and Vocabulary in Practice

Transforming an audio recording of a business meeting into a dependable work document requires deliberate architectural choices when configuring speech recognition parameters. In Gemini 3.5 Transcribe, there is no universal “all-in-one” mode: deep editorial normalization (smart), speaker separation (diarization_mode), word-level time grids (timestamp_granularities), and domain-specific lexicons (custom_vocabulary) represent mutually isolated API capabilities.

Every processing pipeline must begin by establishing its ultimate objective: do you require a concise, readable block of text for quick reading, a structured foundation for downstream analysis, or an array of exact timestamps for video editing? Attempting to combine mutually exclusive parameters leads to schema validation errors before the request is even processed.

Below is an end-to-end practical walkthrough: how to prepare a 45-minute recording of two speakers filled with technical terminology, navigate documented API limits, process both parts using the official Python SDK, assemble preliminary draft data structures, verify and correct them against the audio recording, and hand off verified copies to a meeting note taker or subtitle editor.


Architectural Constraints and Parameter Conflicts

To illustrate the pipeline, consider a typical working session: Alexey (Tech Lead) and Mikhail (Product Manager) spend 45 minutes discussing migration architecture and the deployment of services under the internal project names DataPulse and CloudForge. The conversation includes technical jargon, loanwords, rapid interruptions, and false starts.

When designing an integration with Gemini 3.5 Transcribe, you must take three strict rules into account:

  • Incompatibility Between Custom Vocabulary and Structural Metadata: The custom_vocabulary parameter (which supports up to 1,000 terms, with a practical recommendation of up to 100) cannot be passed alongside diarization_mode or timestamp_granularities. You must make a design trade-off: either rely on the model to transcribe specialized brand names accurately (forfeiting automatic speaker labels and timestamps), or request detailed speaker and time alignments while verifying specialized terminology during post-processing.
  • Incompatibility Between Smart Mode and Timeline Alignment: The smart mode applies speech normalization: it removes conversational filler words, stutters, and false starts while restructuring grammatical syntax. Because tokens are removed, merged, or reordered, the model cannot map the resulting text back to the temporal timeline of the underlying audio stream. As a result, speaker attribution and word-level timestamps are unavailable in smart mode. Furthermore, this mode returns only unannotated continuous text (output_text) and does not automatically extract action items.
  • 30-Minute Boundary for Advanced Metadata: Standard transcription requests without detailed annotations accept audio files up to 60 minutes long. However, once you activate diarization_mode or timestamp_granularities, the documented maximum file duration drops to 30 minutes. Submitting files that exceed this threshold falls outside the supported API specification and should not be attempted; long audio must be split on the client side prior to submission.

Preparing and Splitting the 45-Minute Recording

To extract diarization and word-level timestamps from a 45-minute recording, the source file must be segmented into chunks strictly under 30 minutes. In our hypothetical scenario, we split the recording into two segments:

  • Part 1: 00:00–25:00 (assumed to be exactly 1500.0 seconds);
  • Part 2: 25:00–45:00 (the remaining 20 minutes, or 1200.0 seconds).

The 1500.0-second boundary is chosen here purely to simplify the math for this example. In production environments, cuts should be placed during natural conversational pauses between spoken turns, and the exact physical duration of the first segment must be extracted directly from media file metadata using an inspection tool such as ffprobe.

Splitting the audio introduces two critical continuity challenges:

  1. Timestamp Reset in the Second Segment: The API processes the second segment as an entirely independent file, resetting its internal word-level offsets to begin at 0.000s. To reconstruct a continuous timeline for the full meeting, the actual duration of the first segment must be added programmatically to every word timestamp extracted from the second segment.
  2. Locality of Speaker Labels: The model assigns speaker identifiers such as spk_1 and spk_2 independently within the scope of each individual API call. The speaker designated as spk_1 in the first chunk may be assigned spk_2 in the second. Merging identical technical labels across separate requests without verification will scramble speaker attribution. Each chunk requires an independent lookup table mapped against real participants through auditory spot-checking.

Configuration Variants for Different Tasks

Transcription parameters are configured inside the transcription_config field of the generation_config dictionary. Below are baseline configurations tailored to distinct operational needs.

For coherent, normalized meeting notes without speaker labels or timestamps (suitable for whole files under 60 minutes):

generation_config = {
    "transcription_config": {
        "mode": "smart"
    }
}

For discussions centered on niche product names or proprietary jargon where accurate spelling takes precedence:

generation_config = {
    "transcription_config": {
        "custom_vocabulary": ["DataPulse", "CloudForge", "ClickHouse", "gRPC"]
    }
}

For generating meeting protocols and subtitle timing grids (subject to the 30-minute audio limit):

generation_config = {
    "transcription_config": {
        "mode": {
            "type": "verbatim",
            "diarization_mode": "speaker",
            "timestamp_granularities": ["word"],
        }
    }
}

Full Working Pipeline with the Python SDK: Processing Two Parts

This script is executed before listening to the audio or performing manual verification. It uploads both segments via the Files API, sends requests to the Interactions API, extracts word-level annotations, validates timestamps and speaker identifiers, shifts the timeline of the second part by 1500.0 seconds, and writes out preliminary draft files:

  • meeting_transcript.txt — a draft UTF-8 dialogue transcript organized by speaker turns;
  • word_timestamps.json — a draft array of continuous word-level timestamps intended for subtitle segmentation.

The files produced by this script are explicitly unverified drafts, not finalized records. The practitioner must listen to the recording, map technical speaker tokens to actual voices (speaker mapping), verify technical terminology (terms), and audit time offsets (time offsets). After completing this audit, update the configuration parameters in the code and re-run the pipeline or edit the generated text files directly. Only after this verification protocol should final copies be delivered to a note taker or caption editor.

The script produces intermediate data structures for analysts and video editors; it does not automatically generate action items or formatted .srt subtitle files. Speaker names and the 1500.0-second boundary are specified here for demonstration purposes within this hypothetical scenario.

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)

Illustrative Example of Word-Level Structure and Inspection Zones

Below is a synthetic representation of the extracted data stream across a speaker transition.

Note: This snippet serves exclusively to illustrate the structure of the returned objects, not as a real API call log. Actual transcription quality depends on acoustics, microphones, and speech clarity.

[Строка 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) логично.

Critical Checklist for Manual Review:

  1. Mapping Labels to Real Participants (Lines 1–5 and 7–9): The identifiers spk_1 and spk_2 are arbitrary positional voice tokens. An editor must listen to the opening moments of each segment to establish the true identities: for example, verifying that in Part 1, spk_1 corresponds to Alexey and spk_2 to Mikhail.
  2. Turn Handoffs and Short Interjections (Line 6): The word “Да,” is grouped into spk_1’s turn. In active conversation, this may actually be an overlapping affirmative nod or backchannel response from the listener (spk_2). Speaker handoff boundaries require targeted auditory cross-checks.
  3. Phonetic Spelling of Unrecognized Brands (Line 3): With custom_vocabulary disabled, English technical names may be transcribed phonetically into Cyrillic (e.g., ДатаПульс). An editor must adjust these occurrences to their canonical format: DataPulse.
  4. Prohibition of Blind Global Replacement: Terminology edits must always be applied selectively in context. Global “find-and-replace” operations across the entire document risk corrupting common words, idioms, or literal quotations that share phonetic similarities.
  5. Conversational Hesitations and Fillers (Line 8): In verbatim mode, hesitation markers such as “э-э-э” are explicitly captured. In a polished meeting protocol, these are pruned. For video subtitle tracks, however, their timing must be preserved if subtitle pacing needs to align with visible on-screen articulation.

Protocol for Spot-Checking Before Handing Off Material

Raw data generated by programmatic concatenation must pass a focused quality audit across three verification checkpoints:

  1. Verify Speakers and Conversational Boundaries: Listen to 15–20 seconds of audio across 2–3 speaker turn transitions. Confirm that distinct voices have not merged into a single speaker and that continuous monologues have not been fractured into phantom labels. According to official documentation, diarization for meetings with three or more participants is experimental and requires considerably tighter scrutiny.
  2. Verify Terminology, Numeric Values, and Named Entities: Compile a targeted checklist of key project names (DataPulse, CloudForge, version numbers, budget allocations). Search for these items in the text and cross-reference uncertain segments directly against the audio, avoiding indiscriminate mass replacements.
  3. Verify Timeline Continuity Across Segment Boundaries: Compare the first minute of Part 1 with the bridge into Part 2 (immediately following the 25:00 / 1500.0-second boundary). Ensure that the second segment’s timestamps seamlessly continue the timeline rather than resetting to zero.

Downstream Handoff and Process Stop Conditions

The meeting_transcript.txt and word_timestamps.json files generated by the script are created prior to auditory review and must be treated strictly as preliminary drafts. Never hand them off in their raw state. First execute the manual verification protocol, reconcile speaker identities, terms, and timeline offsets against the audio, correct the output (by updating code parameters and re-running or by editing files directly), and only then release verified copies downstream:

  • For Note Takers or Summarization Models (Note Taker / LLM Summarization): Provide a verified copy of the meeting transcript—a reviewed document with confirmed speaker attribution and normalized technical terms. This serves as the foundation for executive summaries and decision logs, though it does not constitute an automated action item list on its own.
  • For Video Editors or Caption Specialists (Caption Editor): Provide a verified copy of the word timestamp array—a validated sequence of words pinned to continuous timestamps. Downstream styling rules (screen duration limits, line length caps, reading speeds) depend on the target video player and language, after which a dedicated formatting tool generates final .srt or .vtt caption files.

Process Stop Conditions

Halt the workflow and do not pass files to downstream teams if any of the following conditions are met:

  1. Audio exceeding 30 minutes in length was submitted for diarization or word timestamps without prior segmentation (this exceeds documented API limits and must not be sent).
  2. Raw draft files (meeting_transcript.txt or word_timestamps.json) were released to downstream consumers immediately following script execution without auditory spot-checking and manual review.
  3. Speaker labels in the second segment were mapped blindly based on label numbers from the first segment without listening to the audio, or unresolved speaker identifiers remain in the dataset.
  4. The physical duration of the first segment was not added to the second segment’s timeline, or missing word timestamps are detected.
  5. Indiscriminate global find-and-replace rules were applied to terminology without contextual validation, or critical configuration parameters and numerical figures were left unverified against the recording.

Ready to optimize your LLM workflow?

Join thousands of developers building faster, smarter, and more cost-effective AI applications with BetterToken.

Get Started for Free