Gemini 3.8 TTS 把语气指令也念出来?把朗读要求移到语音元数据
迁移到 Gemini 3.8 Flash TTS 时,将台词与 speech_metadata 分开,统一使用 Interactions API,并核对音频响应格式。
使用 Gemini 3.8 Flash TTS 时,需要念出的文字写进台词,持续的朗读方式写进语音元数据。 如果把“平静地说”放进模型按字面朗读的文本,它也可能成为音频内容。Gemini 3.8 Flash TTS 迁移文档明确区分台词和朗读元数据。
Google 在 2026 年 9 月 22 日的 API 更新日志公布了新 TTS 模型。本文提供依据文档编写的 Interactions REST API 迁移示例。请求结构和解码逻辑可以在本地检查,但本文不声称完成真实听音测试、证实音质提升,或确认 Ofox 当前支持该端点。
把“说什么”和“怎么说”分开
| 信息 | 本例中的位置 |
|---|---|
| 听众应该听到的文字 | 文本内容的 text 字段 |
| 持续的朗读要求,如平静、清晰 | speech_metadata 注解的 style |
| 所选音色 | generation_config.speech_config |
| 要求输出音频 | response_format |
即使要求很短,也应保持这种区分,方便检查请求,避免把指令当作台词。如果角色本来就要说“平静地说”这几个字,它们才应该出现在台词中。
模型文档还描述了特定时间点的发声事件。不要假定所有旧提示标签或任意注解都受支持,应查所选模型和 API 类型的当前参考文档。
把旧配音请求逐字段迁移
先拿到确认过的口播稿,不要把旧提示词当作一个整体直接迁移。例如旧输入是“Read warmly as a station announcer: The next train leaves at noon.”,真正应读出的只有“The next train leaves at noon.”。将温暖的车站播报风格放入 style,从口播正文移除指令。这是编辑示例,不是生成音频结果。
首个请求保持音色和句子不变。另存旧请求,再创建下文的 Interactions 请求。不要把 GenerateContent 中的 contents、parts、speech_metadata 位置或响应解析直接搬过来;字段名称相似,不代表在 JSON 中的位置相同。
当前模型文档区分持续风格和瞬时发声事件。整句轻声说话应放在元数据中;确实希望出现叹气时,短暂的 <sigh> 可以留在正文。不要把每条指令改造成自创标签。批准的对白中如果本来就有类似指令的词,也应保留为正文。
双人配音先列回合表:序号、完整台词、speaker 标识、style、配置音色。每一回合都必须显式指定与配置匹配的 speaker。直接在正文写 Speaker 1: 不等于结构化 speaker 字段,还可能被读出来。长对话前先确定映射,复制单人音色数组不算完成多人配置。
请求与响应保持同一种 API 格式
以下示例使用 Interactions API。不要把它的 input 数组与 annotations 塞进 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
通过环境变量安全传入自己的 Key。运行命令会请求供应商 API,可能产生用量费用。本地 JSON 解析通过,不证明账号有模型权限,也不证明供应商已接受请求。
这是单说话人示例。官方多说话人配置有不同结构,不能随意把上述 speech_config 数组改成自己设计的对话 schema。对话各轮的说话人必须与配置相匹配。
先检查响应,再解码音频
保存进 response.json 的 HTTP 错误不是音频。解码 base64 前,先检查 HTTP 结果和响应结构。Interactions REST 响应应查看 model_output 步骤里的 content 块,不要假设 SDK 便捷属性 output_audio 一定存在于原始 JSON。
稳妥的解码器应完成四件事:
- 识别并拒绝错误响应,不把错误写成音频文件。
- 从 model_output 步骤选择音频内容,忽略文本和工具内容。
- 根据返回的 MIME 类型选择扩展名。
- 解码 base64,保留实际封装格式。
迁移指南说明,非流式输出默认采用 WAV。不要直接套用旧版原始 PCM 示例,再加一次 WAV 文件头;重复文件头可能破坏已有的有效 WAV。如果主动请求其他格式,就按实际 MIME 类型和封装处理,不能只改文件后缀。
用小型解码器保存经过检查的 WAV
本例采用 unary 响应,建议显式指定 WAV,将 response_format 改为:
{"type":"audio","mime_type":"audio/wav"}
Interactions 响应参考定义了 step/content 结构。把下面代码存成与 response.json 同目录的 decode_tts.py,运行 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 标签 | 从正文移除标签,使用与配置匹配的 speaker 元数据 |
| 把 JSON 错误保存为音频 | 在 HTTP/interaction 错误处停止,不解码错误对象 |
| WAV 损坏或像噪声 | 核对容器和编码;已有 WAV 时删除旧的 raw PCM 包头逻辑 |
| 配音为空或不完整 | 查看完成状态与结束详情,检查文本大小和模型限制后再决定拆分 |
| 对话音色意外变化 | 核对回合与 speaker 映射,先用短句逐个检查音色 |
没有 data 载荷 | 确认是否启用了 URI 交付或流式输出,按对应模式处理 |
流式处理是另一项实现任务。分块不一定是可独立播放、能直接拼接的 WAV。把多个完整 WAV 字节直接连接,也会在中间保留多余文件头。第一次迁移先保持 unary;以后需要流式或拼接时,再显式处理文档规定的 PCM 格式和时序。
第一次迁移测试尽量小
先用单个说话人、一小段台词和一个朗读要求。检查是否漏字、是否多念了指令、发音是否正确,以及有没有意外变声。将请求、模型 ID、时间和输出文件一起保存。这些是建议执行的验收项目,不是本文已经取得的结果。
确认后再加长台词或增加说话人。每次只变一个条件:如果同时移动指令、换音色、拆脚本和切换 API 格式,就很难定位失败原因。
配音用于视频前,应逐字核对音频与台词。不露脸视频制作指南介绍更完整的制作流程。TTS 迁移只是其中一步,并不保证时长、发音或特定声音的使用授权。
配音验收后再放进视频
长稿生成前写好验收标准。上文车站句子应完整读出批准内容,不夹带“calm and clear”、speaker 前缀、不漏时间、结尾不重复。分别记录发音、削波、不必要的静音和音色变化。文件格式正确,仍可能在这些项目中失败。
可用的迁移测试集包括一条中性短句、一条包含项目中专名或数字的句子;只有产品需要多人时,再加双回合对话。把指令移到元数据时保持台词和音色不变。保存请求/输出配对及听测笔记;没有按明确分母给所有运行打分,就不要报告成功率。
配音通过后,以真实音频时长安排画面。要求视频 15 秒不会强制 TTS 也生成 15 秒。按需要明确修改台词或时间线,调整速度后重新听一遍。保留原音频,才能区分后期编辑缺陷与生成问题。至此完成的是从指令位置到可用素材的迁移,不是模型音质榜单。
换供应商之前核对什么
OpenAI 兼容的文本端点,不等于支持 Google Interactions API 或其语音元数据字段。检查供应商专门的音频路由、精确模型 ID 和输出格式。本文的 Google 直连示例不是 Ofox 端点配置。
选择模型与迁移 API 格式是两件事。Gemini 3.8 Flash-Lite TTS 是相关产品,但替换模型字符串前,必须按它自己的文档确认支持与行为。多模态 API 概览(英文)可提供背景,不能取代供应商当前文档。
常见问题
- 为什么模型会把语气要求也念出来?
- 新模型把输入文本当作字面台词。持续的朗读要求应放进文档规定的语音元数据,而不是写进台词。
- 可以把这份 JSON 直接发给 GenerateContent 吗?
- 不可以。本例使用 Interactions 的 input 和注解字段,GenerateContent 请求结构不同,应完整采用该 API 自己的示例。
- 输出一定是原始 PCM 吗?
- 不是。迁移说明中非流式输出默认 WAV。应检查实际响应,不要重复添加 WAV 文件头。


