Перейти к основному содержанию

Gemini 3.5 Transcribe API: как расшифровать запись и запустить live-транскрипцию

Запись расшифровывается через Files и Interactions API, субтитры поступают из отдельной Live-модели. Пользовательский словарь и разметка по словам требуют разных конфигураций; предварительный текст потока нужно заменять, окончательный — сохранять.

LaoZhang AI TeamОпубликованоОбновлено 9 мин чтения
Содержание
Два пути Gemini 3.5 Transcribe: отдельные профили записи через Interactions и предварительные/окончательные субтитры Live

Чтобы расшифровать запись, загрузите её через Files API и передайте полученный URI в Interactions API с моделью gemini-3.5-transcribe. Для субтитров во время речи используйте gemini-3.5-transcribe-live: ей нужен поток несжатого PCM и отдельная обработка предварительного и окончательного текста.

Главный выбор для записанного созвона — словарь терминов или разметка по словам и спикерам. Их нельзя включить в одном запросе: текущий API отклоняет сочетание custom_vocabulary с разделением говорящих или временными метками. Ниже есть оба допустимых варианта, разбор ответа и пример Live-клиента с ограниченным ожиданием после конца аудио. Схема запросов основана на руководстве по транскрипции; локально проверены только собственные обработчики на искусственных данных, без обращений к модели.

Как выбрать модель для записи или субтитров

Что нужно получитьЗаписанный файлАудиопоток
Model IDgemini-3.5-transcribegemini-3.5-transcribe-live
ПодключениеFiles → InteractionsLive API, двустороннее соединение
ВходНапример, MP3 или WAV с соответствующим MIMEPCM: signed 16-bit, 16 кГц, mono, little-endian
Предельная длительность1 час; с разделением говорящих или временными метками — 30 минутСессия до 10 минут
Разделение по спикерамДа, в режиме verbatimНет
Временные метки словДа, в режиме verbatimНет
Результат для приложенияТекст и, при запросе, аннотации словПредварительные и окончательные фрагменты

Ограничения относятся к конкретным моделям Gemini 3.5 Transcribe, а формат потока — к Live transcription. Русский поддерживается с кодом ru-RU; без языковой подсказки работает автоопределение, в том числе при переключении языков.

Выбор между обработкой записанного файла и потоком Live API

Для часовой встречи с разметкой придётся разделить запись на допустимые части. Сохраните время начала каждой части: смещение слова в ответе относится к отправленному аудио, и к нему нужно прибавить смещение части в исходной записи. Метки говорящих между отдельными запросами не следует автоматически считать идентификаторами одного и того же человека.

Это специализированное распознавание речи. Вызов инструментов, рассуждение по аудио и озвучивание ответа не входят в возможности этих моделей. Резюме встречи можно строить следующим этапом, после сохранения расшифровки; отдельный этап имеет собственную стоимость и правила обработки данных.

Расшифровать файл: два допустимых профиля Python

Для примера нужен Google Gen AI SDK с поддержкой текущего Interactions API, доступ к модели в вашем проекте и разрешённый к отправке файл. Ключ должен быть настроен на сервере; код использует стандартное чтение учётных данных SDK. Здесь нет проверки доступности конкретного аккаунта, установленной версии SDK или успешного сетевого вызова.

Сохраните пример как recorded.py. Профиль terms задаёт словарь, а marked запрашивает слова, временные метки и метки говорящих. Каждый запуск делает одну транскрипцию; запуск обоих профилей оплачивается как два независимых запроса.

python
import argparse
import json
from pathlib import Path
from google import genai

PROFILES = {
    "terms": {
        "language_codes": ["ru-RU"],
        "custom_vocabulary": ["Кубернетес", "BigQuery", "ЛаоЧжан"],
    },
    "marked": {
        "language_codes": ["ru-RU"],
        "mode": {
            "type": "verbatim",
            "diarization_mode": "speaker",
            "timestamp_granularities": ["word"],
        },
    },
}


def validate_profile(config):
    mode = config.get("mode", {"type": "verbatim"})
    if isinstance(mode, dict):
        if mode.get("type") != "verbatim":
            raise ValueError("Object mode must be verbatim")
        marked = bool(mode.get("diarization_mode")
                      or mode.get("timestamp_granularities"))
    else:
        if mode != "smart":
            raise ValueError("String mode must be smart")
        marked = False
    if config.get("custom_vocabulary") and marked:
        raise ValueError("Vocabulary and annotations are incompatible")
    if len(config.get("custom_vocabulary", [])) > 1000:
        raise ValueError("Vocabulary exceeds 1000 terms")


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("audio", type=Path)
    parser.add_argument("--profile", choices=PROFILES, default="marked")
    args = parser.parse_args()
    config = PROFILES[args.profile]
    validate_profile(config)
    client = genai.Client()
    audio = client.files.upload(file=str(args.audio))
    result = client.interactions.create(
        model="gemini-3.5-transcribe",
        input=[{"type": "audio", "uri": audio.uri,
                "mime_type": audio.mime_type}],
        generation_config={"transcription_config": config},
    )
    Path("interaction.json").write_text(
        json.dumps(result.model_dump(mode="json"),
                   ensure_ascii=False, indent=2), encoding="utf-8")
    print(result.output_text)


if __name__ == "__main__":
    main()

Примеры команд для вашего окружения: python recorded.py sample.mp3 --profile terms или python recorded.py sample.mp3 --profile marked. Сохранённый interaction.json содержит ответ целиком; фиксированное имя в этом минимальном примере перезаписывается при следующем запуске. В приложении используйте отдельное имя для каждой записи. Полный ответ полезнее одного напечатанного текста: он сохраняет статус, структуру и аннотации, если они вернулись.

Локальный validate_profile отсеивает рассматриваемые несовместимые сочетания, но не заменяет серверную проверку всех параметров. По официальному описанию словаря и режимов, максимум — 1 000 терминов, а обычно лучшие результаты достигаются со списком до 100. Это разные условия: допустимый размер и рекомендация по использованию. Выбирайте фамилии, названия и сокращения, ошибки в которых действительно мешают работе.

У профиля terms режим verbatim используется по умолчанию. Для текста без слов-паразитов и самоисправлений можно заменить конфигурацию на {"language_codes": ["ru-RU"], "mode": "smart"}. В записанном API smart — строка, а объект mode предназначен для {"type": "verbatim", ...}. Не используйте {"type": "smart"} и не добавляйте разметку к smart: это не текущая документированная форма.

Для проверяемой цитаты сохраняйте verbatim и исходную запись. Читабельная версия smart может убрать оговорку или повтор, поэтому она не заменяет дословную расшифровку и ручную сверку важных мест.

Как сохранить слова, время и говорящих из ответа

output_text — объединённая расшифровка. Аннотации находятся глубже: steps[].content[].annotations[], где type равен word_info. В них используются text, speaker, start_offset и end_offset; смещения в JSON представлены строками длительности, например "0.100s". Это структура ответа Interactions, а не поля Live-субтитров.

Следующий обработчик работает с interaction.json, сохранённым предыдущим примером. Он не обращается к API. Для отсутствующего времени или говорящего остаётся null, а неверное время не превращается в выдуманное значение.

python
import json
import re
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path


def milliseconds(value):
    if value is None:
        return None
    if not isinstance(value, str) or not re.fullmatch(r"\d+(?:\.\d+)?s", value):
        raise ValueError(f"Invalid duration: {value!r}")
    return int((Decimal(value[:-1]) * 1000).quantize(
        Decimal("1"), rounding=ROUND_HALF_UP))


def word_rows(document):
    rows = []
    for step in document.get("steps") or []:
        for content in step.get("content") or []:
            for item in content.get("annotations") or []:
                if item.get("type") != "word_info":
                    continue
                start = milliseconds(item.get("start_offset"))
                end = milliseconds(item.get("end_offset"))
                if start is not None and end is not None and end < start:
                    raise ValueError("Word ends before it starts")
                rows.append({
                    "text": item.get("text"),
                    "speaker": item.get("speaker"),
                    "start_offset": item.get("start_offset"),
                    "end_offset": item.get("end_offset"),
                    "start_ms": start, "end_ms": end,
                })
    return rows


if __name__ == "__main__":
    document = json.loads(Path("interaction.json").read_text(encoding="utf-8"))
    result = {"status": document.get("status"), "words": word_rows(document)}
    Path("words.json").write_text(json.dumps(result, ensure_ascii=False, indent=2),
                                  encoding="utf-8")
    print(f"Words: {len(result['words'])}")

Начало "0s" корректно сохраняется как 0 мс: его нельзя отбрасывать проверкой if start. Если words пуст, это не доказательство тишины или плохого распознавания. Сначала проверьте профиль запроса, статус ответа и наличие аннотаций. Текст может быть непустым и без разметки.

Из таких строк можно строить переход к месту записи или группировать слова для SRT. Группировка, длительность показа субтитра и обработка пересекающейся речи — уже решения приложения. Парсер выше не обещает готовый монтажный файл и не проверяет попадание слов во время на слух.

В карточке модели заявлено до восьми говорящих, но определение говорящего для трёх и более экспериментально. Временные метки могут снижать точность распознавания. Метка вроде spk_1 обозначает голос в данной расшифровке, а не установленную личность участника встречи.

Live-субтитры: правильный PCM и конечное ожидание

Для Live сначала подготовьте правильные байты. Контейнер WAV содержит заголовок; его нельзя целиком отправить как audio/pcm. MP3, WebM/Opus и телефонный μ-law тоже не становятся PCM после замены MIME. По руководству Live API, нужен несжатый 16-битный PCM, 16 кГц, один канал, little-endian. Порция в 100 мс при этих параметрах — 1 600 отсчётов, то есть 3 200 байт.

Пример ниже читает уже подготовленный PCM WAV, проверяет формат и отправляет только аудиокадры в темпе записи. Это серверный прототип для файла длиной до 30 секунд, а не реализация захвата микрофона. Сохраните его как live_file.py; SDK и серверные учётные данные нужны те же, что и выше.

python
import argparse
import asyncio
import json
import wave
from contextlib import suppress
from dataclasses import dataclass, field
from google import genai
from google.genai import types


@dataclass
class Captions:
    final: list[str] = field(default_factory=list)
    preview: str = ""

    def apply(self, content):
        if content is None:
            return
        interim = getattr(content, "interim_input_transcription", None)
        if interim is not None and interim.text is not None:
            self.preview = interim.text
        finished = getattr(content, "input_transcription", None)
        if finished is not None and finished.text is not None:
            if finished.text:
                self.final.append(finished.text)
            self.preview = ""


def read_pcm(path):
    with wave.open(path, "rb") as audio:
        if (audio.getnchannels(), audio.getsampwidth(), audio.getframerate(),
            audio.getcomptype()) != (1, 2, 16000, "NONE"):
            raise ValueError("Expected uncompressed mono 16-bit 16-kHz WAV")
        if not 0 < audio.getnframes() <= 30 * 16000:
            raise ValueError("Use a nonempty sample no longer than 30 seconds")
        chunks = []
        while data := audio.readframes(1600):
            if len(data) % 2:
                raise ValueError("Incomplete 16-bit sample")
            chunks.append(data)
        return chunks


async def receive(session, captions):
    while True:
        seen = False
        async for response in session.receive():
            seen = True
            captions.apply(getattr(response, "server_content", None))
        if not seen:
            return "receiver_ended"
        # An iterator may end after a turn; keep listening for the next one.
        await asyncio.sleep(0.01)


async def run(path):
    chunks = read_pcm(path)
    captions = Captions()
    report = {"finish": "not_started", "error": None}
    receiver = None
    try:
        client = genai.Client()
        config = types.LiveConnectConfig(
            response_modalities=["TEXT"],
            input_audio_transcription=types.AudioTranscriptionConfig(
                language_codes=["ru-RU"], mode="VERBATIM"),
        )
        async with client.aio.live.connect(
            model="gemini-3.5-transcribe-live", config=config
        ) as session:
            receiver = asyncio.create_task(receive(session, captions))
            try:
                for chunk in chunks:
                    if receiver.done():
                        report["finish"] = receiver.result()
                        break
                    await session.send_realtime_input(audio=types.Blob(
                        data=chunk, mime_type="audio/pcm;rate=16000"))
                    await asyncio.sleep(len(chunk) / 32000)
                else:
                    await session.send_realtime_input(audio_stream_end=True)
                    done, _ = await asyncio.wait({receiver}, timeout=3.0)
                    report["finish"] = (receiver.result() if done else "drain_timeout")
            finally:
                if not receiver.done():
                    receiver.cancel()
                with suppress(asyncio.CancelledError, Exception):
                    await receiver
    except Exception as error:
        report["finish"] = "error"
        report["error"] = f"{type(error).__name__}: {error}"
    finally:
        report.update(final=captions.final, unfinished_preview=captions.preview)
        print(json.dumps(report, ensure_ascii=False))


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("wav")
    asyncio.run(run(parser.parse_args().wav))

Запуск для вашего окружения: python live_file.py sample-16k-mono.wav. В отличие от варианта с неопределённым your_pcm_source(), здесь показаны источник кадров, хранение текста и остановка задачи чтения. Пример ограничивает ожидание ответов после отправки конца аудио тремя секундами. Это выбранный срок приложения, а не обещание Google успеть вернуть последний фрагмент. Установление соединения и отправка относятся к сетевой работе SDK и этим сроком не ограничены.

interim_input_transcription заменяет текущую строку предпросмотра. input_transcription добавляется в список окончательных фрагментов и очищает предпросмотр. Обработчик принимает обе части одного события и спокойно пропускает события без server_content. Он не удаляет одинаковые окончательные строки: человек действительно может дважды сказать «да». Порядковые номера, если они нужны для хранения, назначайте в своём приложении; это не идентификаторы сегментов от сервера.

В отчёте drain_timeout означает, что истёк срок ожидания, а receiver_ended — что итератор завершился без новых событий. Ни один статус сам по себе не доказывает, что обработано последнее слово. unfinished_preview остаётся отдельно и не выдаётся за окончательный текст. При ошибке уже полученные фрагменты тоже сохраняются в отчёте. Для рабочего сервиса этот отчёт нужно записывать в надёжное хранилище вместе с идентификатором записи; печать в консоль здесь служит простым выходом прототипа.

В данном варианте остаётся автоматическое определение начала и конца речи. Сигнал audio_stream_end=True отправляется на конце файла, чтобы завершить аудиопоток и дать серверу сформировать окончательный текст. Для кнопки push-to-talk используется другой режим: автоматическое определение активности отключают и отправляют activity_start/activity_end. Не смешивайте эти две схемы без отдельной логики.

Для читабельных Live-субтитров в AudioTranscriptionConfig можно выбрать mode="SMART"; дословный вариант — "VERBATIM". Здесь значения прописные, в отличие от строки "smart" у записанного API. Live не возвращает разметку слов и говорящих, поэтому приведённое состояние субтитров не пытается создать такие данные из времени получения событий.

Длинный эфир потребует смены сессий до десятиминутного предела, сохранения уже подтверждённых фрагментов и явной обработки пропуска или повторной отправки аудио. Этот короткий прототип не реализует переподключение и не гарантирует отсутствие потерь между сессиями.

Сколько стоит транскрипция

На 6 октября 2026 года цены Gemini Developer API для выделенных моделей такие:

МодельВходное аудио, USD за 1 млн токеновВыходной текст, USD за 1 млн токеновОпубликованный ориентир за минуту, вход + выход
gemini-3.5-transcribe2,0012,00≈ 0,003 + 0,002 = 0,005 USD
gemini-3.5-transcribe-live3,5021,00≈ 0,005 + 0,004 = 0,009 USD

Оплата идёт по токенам, а не по фиксированному минутному тарифу. Для оценки Google использует 25 аудиотокенов в секунду и около 175 выходных текстовых токенов в минуту. Из этих предположений получаются 0,0051 USD за минуту записи и 0,008925 USD за минуту Live до округления.

Поэтому для 100 часов, то есть 6 000 минут, расчёт даёт 30,60 USD и 53,55 USD соответственно. Округлённые минутные ориентиры дают около 30 и 54 USD — это другое округление той же оценки, а не два разных тарифа. Ни одна сумма здесь не является полученным счётом.

Для фактических объёмов формула проста: audio_tokens × input_rate / 1_000_000 + text_tokens × output_rate / 1_000_000. Добавьте отдельно перекодирование, хранение, повторы запросов, последующее резюме и проверку человеком. Если расшифровываете одну запись двумя профилями, учитывайте оба вызова. Для этих специализированных моделей не стоит закладывать скидку Batch: карточка модели не указывает поддержку Batch API.

Что проверить перед отправкой рабочих записей

Отдельные проверки записи и Live: профиль, аннотации, предварительный и окончательный текст, прослушивание важных сущностей

Вместо проверки только «вернулся ли текст» разделите испытания по задаче. Для записи проверьте статус ответа, наличие требуемых аннотаций и переход по времени к нужному слову. Для Live проверьте смену предварительной строки, сохранение окончательной, остаток после конца аудио и поведение при разрыве. Затем прослушайте разрешённые к использованию записи и отдельно сверяйте фамилии, суммы, даты, артикулы и договорённости: средняя ошибка по словам может скрыть дорогую ошибку в числе.

В рамках подготовки этого руководства мы выполнили офлайн-проверки собственного кода: допустимые профили и отказ от их смешивания, разбор синтетических word_info с нулевым и отсутствующим временем, замену предпросмотра, сохранение повторяющихся окончательных фраз и чтение созданного локально PCM WAV. Синтетический асинхронный источник также проверяет завершение и отмену ожидания; арифметика стоимости проверена через Decimal. SDK-клиенты и сетевые примеры не запускались. Эти результаты подтверждают логику обработчиков, но не точность модели, задержку Live или принятие запросов сервером.

У файлов есть отдельный жизненный цикл: по документации Files API, загруженные объекты удаляются автоматически через 48 часов; пределы — 2 ГБ на файл и 20 ГБ на проект. Само хранение через Files API, где оно доступно, не оплачивается отдельно, но это не делает вызов транскрипции бесплатным. Сохраните исходное аудио у себя: URI файла не является долговременным архивом. Период 48 часов нельзя переносить на ответы Interactions и все журналы провайдера.

Для конфиденциальных звонков важны условия использования Gemini API, а не только колонка Free/Paid в таблице цен. В общем случае данные бесплатного сервиса могут использоваться для улучшения продуктов и просматриваться людьми; такие запросы не предназначены для чувствительной информации. Для платного API требуется проект с активным Cloud Billing: запросы и ответы не используются для улучшения продуктов, но возможна ограниченная обработка и журналирование для безопасности и юридических требований. Это не обещание нулевого хранения.

Для ЕЭЗ, Швейцарии и Великобритании условия использования данных платного сервиса распространяются также на бесплатную квоту; приложения, предоставляющие API-клиенты пользователям этих регионов, должны использовать Paid Services. Язык записи не определяет регион пользователя или право отправлять данные. Согласие участников и правила вашей организации проверяются отдельно.

При прямом подключении браузера постоянный ключ на клиенте недопустим. Краткоживущие токены выпускает доверенный сервер после аутентификации клиента; сейчас они предназначены для Live API v1beta и могут ограничивать модель и конфигурацию. По умолчанию на начало сессии даётся 1 минута, на обмен сообщениями — 30 минут, число использований — 1. Тридцать минут жизни токена не продлевают десятиминутный предел Transcribe Live. Выдача токенов и браузерная аутентификация в приведённые серверные примеры не входят.

Частые вопросы

Можно ли запустить Gemini 3.5 Transcribe локально или скачать open-source модель?

Приведённые официальные способы подключения — облачные API. Python-скрипт может работать на вашем компьютере, но аудио отправляется Google, а распознавание не выполняется офлайн. Рассмотренные документы модели не дают способа скачать её веса и развернуть локально. Офлайн-проверка парсера — проверка приложения, не локальный запуск Gemini.

Можно ли одновременно добавить словарь и получить спикеров с временными метками?

Нет, текущий API запрещает словарь вместе с любой из этих двух функций. Выберите профиль под задачу. Если нужны два разных результата, сделайте отдельные допустимые запросы и сохраните их раздельно; не считайте автоматическое сопоставление двух расшифровок точным без проверки.

Есть ли бесплатная транскрипция и работает ли она для любого аккаунта?

В таблице цен указан бесплатный уровень, однако это не обещание одинаковых квот и доступа во всех проектах и странах. Доступность модели, условия данных и разрешение отправить конкретную запись проверяются для вашего аккаунта. Короткий тест AI Studio, описанный другим автором, не доказывает, что ваш SDK-запрос доступен бесплатно.

Модели уже GA или всё ещё public preview?

В проверенных 6 октября официальных материалах есть расхождение: журнал изменений называет релиз 26 августа GA, а публикация о запуске продолжает использовать public preview. Поэтому статус запуска нельзя превращать в гарантию доступа конкретного проекта. Для интеграции используйте точные model ID и текущую спецификацию нужного API.

Можно ли использовать Live для готовых субтитров с точным временем каждого слова?

Live даёт предварительные и окончательные фрагменты, но не поддерживает временные метки слов и разделение говорящих. Для разметки готовой записи используйте gemini-3.5-transcribe с профилем marked. Время прихода события в приложение не является временем произнесения слова в аудио.

Источники9

Внешние страницы, на которые ссылается это руководство, в порядке упоминания. Последнее обновление: 7 окт. 2026 г..

  1. 1.руководстве по транскрипцииai.google.dev/gemini-api/docs/transcribe
  2. 2.конкретным моделям Gemini 3.5 Transcribeai.google.dev/gemini-api/docs/models/gemini-3.5-transcribe
  3. 3.Live transcriptionai.google.dev/gemini-api/docs/live-api/live-transcribe
  4. 4.цены Gemini Developer APIai.google.dev/gemini-api/docs/pricing
  5. 5.документации Files APIai.google.dev/gemini-api/docs/files
  6. 6.условия использования Gemini APIai.google.dev/gemini-api/terms
  7. 7.Краткоживущие токеныai.google.dev/gemini-api/docs/live-api/ephemeral-tokens
  8. 8.журнал измененийai.google.dev/gemini-api/docs/changelog
  9. 9.публикация о запускеblog.google/innovation-and-ai/models-and-research/gemini-models/gemini-3-5-transcribe