Gemini 3.8 TTS озвучивает инструкции? Отделите текст от speech_metadata
Как перенести запрос в Gemini 3.8 Flash TTS: буквальный текст, speech_metadata, единый формат Interactions API и проверка контейнера аудио.
В Gemini 3.8 Flash TTS произносимые слова следует передавать как текст реплики, а постоянные указания о подаче — как метаданные речи. Если вставить «говори спокойно» в поле, воспринимаемое как буквальный текст, модель может произнести и эти слова. Руководство по миграции Gemini 3.8 Flash TTS прямо разделяет текст и метаданные подачи.
Google анонсировала новые TTS-модели в журнале Gemini API от 22 сентября 2026 года. Здесь приведён пример миграции для Interactions REST API, составленный по документации. Структуру запроса и логику декодирования можно проверить локально. Статья не заявляет о прослушивании реального результата, улучшении качества голоса или текущей поддержке этого эндпоинта в Ofox.
Разделите содержание и манеру речи
| Информация | Поле в этом примере |
|---|---|
| Слова, которые должен услышать слушатель | text в текстовом блоке |
| Постоянная подача, например спокойная и чёткая | style в аннотации speech_metadata |
| Выбранный голос | generation_config.speech_config |
| Запрос аудиовывода | response_format |
Сохраняйте разделение даже для короткой инструкции. Так проще проверить запрос и не перепутать указание со сценарием. Если персонаж действительно должен сказать «говори спокойно», эти слова, наоборот, относятся к тексту реплики.
Документация также описывает вокальные события в определённый момент. Не предполагайте поддержку любых старых тегов или произвольных аннотаций: используйте актуальную справку выбранной модели и API.
Перенесите старый запрос озвучки по полям
Начните с утверждённого текста для слушателя, а не со старого промпта целиком. Если он выглядел как «Read warmly as a station announcer: The next train leaves at noon.», произнести нужно только последнее предложение. Указание на тёплую подачу диктора станции поместите в style и уберите из расшифровки. Это пример редактирования, не результат генерации аудио.
В первом запросе не меняйте голос и предложение. Сохраните старый запрос отдельно, затем соберите приведённый в следующем разделе Interactions-запрос. Не переносите в него contents, parts, расположение speech_metadata или разбор ответа из GenerateContent. Знакомое имя поля не означает одинаковый уровень в JSON.
Актуальная документация различает постоянную манеру и краткие вокальные события. Шёпот на протяжении предложения относится к метаданным; <sigh> можно оставить там, где действительно нужен вздох. Не превращайте любую инструкцию в выдуманный тег. Если похожие на инструкцию слова являются частью утверждённой реплики, они должны остаться текстом.
Для двух говорящих сначала составьте таблицу: номер реплики, точный текст, идентификатор speaker, style и настроенный голос. В каждой реплике явно укажите speaker, совпадающий с конфигурацией. Префикс Speaker 1: в расшифровке не заменяет структурированное поле и может быть произнесён. Установите соответствия до расширения диалога; копирование массива одного голоса не создаёт полноценную многоголосую настройку.
Не смешивайте семейства API
Пример ниже использует Interactions API. Не переносите его массив input и аннотации в тело 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 в самостоятельно придуманную схему диалога. Говорящий в каждой реплике должен совпадать с настроенными участниками.
Проверяйте ответ до декодирования
HTTP-ошибка, сохранённая в response.json, не является аудио. Перед декодированием base64 проверьте HTTP-результат и структуру ответа. В REST-ответе Interactions ищите блоки content в шагах model_output. Не предполагайте, что удобное свойство SDK вроде output_audio существует в исходном JSON.
Надёжный декодер должен:
- Отклонять ответы с ошибкой, а не записывать их как аудиофайл.
- Выбирать аудиоблоки из шагов model_output, пропуская текст и содержимое инструментов.
- Проверять MIME-тип до выбора расширения файла.
- Декодировать base64, сохраняя фактический контейнер.
Руководство по миграции указывает WAV как формат по умолчанию для непотокового ответа. Не добавляйте автоматически WAV-заголовок из старого примера для raw PCM: второй заголовок может испортить уже корректный WAV. Если запрошен другой формат, учитывайте его MIME-тип и контейнер, а не просто меняйте расширение.
Сохраните проверенный WAV небольшим декодером
Для этого непотокового примера явно запросите WAV, заменив response_format на:
{"type":"audio","mime_type":"audio/wav"}
Структура step/content описана в справочнике ответа Interactions. Сохраните код как decode_tts.py рядом с response.json и выполните 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. Частота и длительность читаются из файла; конкретные значения не обещаются. Декодер останавливается при нескольких аудиоблоках, незавершённом interaction, доставке через URI или другой кодировке. Это узкий обработчик одного WAV-результата, а не универсальный потоковый клиент. Некорректный base64 и неподдерживаемый WAV также вызывают ошибку вместо незаметного сохранения вводящего в заблуждение файла.
Мы проверили декодер на локально созданных PCM WAV, повреждённых данных и ответах с ошибками. Это проверка только локального разбора и отклонения. Она не подтверждает доступ аккаунта, реальный ответ Google, произношение или качество голоса.
Разделяйте ошибки контейнера и ошибки речи
Если speech.wav не открывается, сначала проверьте HTTP-статус, MIME и байты, а не меняйте текст. Второй заголовок поверх WAV не исправит ошибочную реплику. И наоборот: корректные метаданные и воспроизводимый файл ещё не доказывают, что прозвучали все утверждённые слова.
| Сбой | Конкретная проверка и исправление |
|---|---|
| Произносится инструкция подачи | Сравнить с утверждённым текстом, перенести лишнюю инструкцию в style |
| Произносится метка говорящего | Убрать её из текста и использовать speaker, совпадающий с настройкой |
| JSON ошибки сохранён как аудио | Остановиться на HTTP/interaction-ошибке, не декодировать объект ошибки |
| WAV повреждён или звучит как шум | Проверить контейнер и кодировку; для готового WAV убрать старое оборачивание raw PCM |
| Пустая или неполная речь | Проверить завершение и его причину, размер текста и ограничения модели до разбиения |
| Неожиданно меняется голос | Сверить реплики со speaker и проверить каждый голос на коротком тексте |
Нет data | Проверить URI или streaming и использовать документированный обработчик этого режима |
Потоковая передача — отдельная задача реализации. Чанки необязательно являются самостоятельными WAV, которые можно побайтно соединить. Склейка нескольких полных WAV тоже оставляет лишние заголовки посередине. Первую миграцию проведите в непотоковом режиме; позже, если нужны поток или склейка, явно обработайте документированный PCM-формат и тайминг.
Начните миграцию с короткой проверки
Используйте одного говорящего, короткий текст и одно указание о подаче. Прослушайте результат: нет ли пропущенных слов, произнесённых инструкций, ошибок произношения или неожиданной смены голоса. Сохраните запрос, ID модели, время и аудиофайл вместе. Это предлагаемые критерии приёмки, а не результаты, полученные для статьи.
Только затем увеличивайте текст или добавляйте голоса. Меняйте одну переменную за раз: одновременный перенос инструкции в метаданные, смена голоса, разбиение текста и переход на другой API затрудняют диагностику.
Перед монтажом озвучки в видео сравните её с точным текстом. Руководство по созданию видео без ведущего в кадре описывает более широкий процесс. Миграция TTS не гарантирует длительность, произношение или право использования определённого голоса.
Примите озвучку до монтажа в видео
Задайте критерии до генерации длинного текста. В примере с поездом должно прозвучать ровно утверждённое предложение: без «calm and clear», префикса говорящего, пропущенного времени и повторённого окончания. Отдельно отмечайте произношение, клиппинг, ненужную тишину и смену голоса. Даже корректный файл может не пройти эти проверки.
Практичный набор миграции — нейтральное короткое предложение, фраза с именем или числом из проекта и, только если продукту нужны несколько говорящих, диалог из двух реплик. Перенося инструкции в метаданные, сохраняйте текст и голос. Записывайте пары запрос/ответ и заметки прослушивания. Не публикуйте долю успешных запусков, пока не оценили все попытки с определённым знаменателем.
После приёмки измерьте настоящую длину аудио и подгоните под неё сцены. Заказ видео на 15 секунд не заставляет TTS длиться 15 секунд. Осознанно изменяйте текст или монтаж и слушайте снова после изменения скорости. Сохраните исходную дорожку, чтобы отделить дефект монтажа от генерации. Так завершается переход от правильно размещённой инструкции к пригодному материалу без заявления о бенчмарке качества.
Что проверить у другого провайдера
OpenAI-совместимый текстовый эндпоинт не подразумевает поддержку Google Interactions API и его метаданных речи. Проверьте отдельный аудиомаршрут, точный ID модели и формат вывода. Прямой пример Google выше не является конфигурацией эндпоинта Ofox.
Выбор модели отделяйте от смены формата API. Gemini 3.8 Flash-Lite TTS — связанное предложение, но перед заменой строки модели нужно проверить его собственную документацию и поддержку. Обзор мультимодальных API (на английском) даёт контекст, но не заменяет актуальную справку провайдера.
Часто задаваемые вопросы
- Почему модель может произнести указание о подаче?
- Новая модель воспринимает входной текст как буквальную реплику. Постоянные указания передавайте в документированном поле метаданных речи, а не внутри сценария.
- Можно отправить этот JSON в GenerateContent?
- Нет. Здесь использованы input и аннотации Interactions. У GenerateContent другая структура; следуйте его примеру от запроса до ответа.
- Вывод всегда представляет собой raw PCM?
- Нет. По руководству миграции непотоковый вывод по умолчанию — WAV. Проверяйте фактический формат и не добавляйте второй WAV-заголовок.


