9Router 文本转语音(TTS)API 完整实战指南:从 /v1/audio/speech 到多提供商语音合成
【免费下载链接】9routerUnlimited FREE AI coding. Connect Claude Code, Codex, Cursor, Cline, Copilot, Antigravity to FREE Claude/GPT/Gemini via 40+ providers. Auto-fallback, RTK -40% tokens, never hit limits.项目地址: https://gitcode.com/GitHub_Trending/9r/9router
本篇技术指南以 9Router 技能文档 skills/9router-tts/SKILL.md 为核心骨架,系统讲解如何通过 OpenAI 兼容的/v1/audio/speech接口,把文字转换为语音。文中覆盖模型与音色发现、请求格式、响应格式、以及 OpenAI、ElevenLabs、Edge TTS、Google TTS、Deepgram 等十余家提供商的model参数格式差异,并结合仓库源码(speech 路由、TTS 处理器、核心合成器)深入剖析其内部实现。读完本文,你将能够在任意语言环境中(curl、Node.js、Python 等)一键调用 9Router 完成语音合成,并理解其"统一接口、多提供商、自动回退"的底层机制。
一、环境准备:两个环境变量
9Router 的 TTS 能力通过统一的 OpenAI 兼容 HTTP 接口暴露,调用前需要配置两个环境变量:
| 变量 | 必填 | 说明 |
|---|---|---|
NINEROUTER_URL | 是 | 9Router 服务的根地址(例如自建部署的http://localhost:3000) |
NINEROUTER_KEY | 视配置而定 | API 密钥,仅当服务端开启了鉴权(requireApiKey)时需要 |
从源码看,密钥校验逻辑位于 src/sse/handlers/tts.js:处理器在收到请求后会读取本地设置,若settings.requireApiKey为真,则从请求中提取 API Key 并校验,缺失或非法时分别返回401 Missing API key与401 Invalid API key。若你的部署未开启鉴权,则NINEROUTER_KEY可以省略。
服务的整体初始化与技能接入方式见 skills/9router/SKILL.md,TTS 只是 9Router 众多多模态能力之一,同一套鉴权与模型路由机制同样适用于聊天、图像、语音识别等接口。
二、发现能力:列出 TTS 模型、元数据与音色
在正式合成之前,建议先通过三个只读接口"侦察"可用的模型与音色。这三个接口在仓库中均有对应实现,且全部返回 OpenAI 风格的{ object: "list", data: [...] }结构。
1)列出所有 TTS 模型
curl $NINEROUTER_URL/v1/models/tts | jq '.data[].id'该接口实现位于 src/app/api/v1/models/[kind]/route.js,tts是支持的 kind 之一(其余为image、stt、embedding、image-to-text、web)。它按能力类型过滤模型列表,返回形如el/eleven_multilingual_v2、openai/tts-1、edge-tts/vi-VN-HoaiMyNeural的完整模型 ID。
2)查询单个模型的元数据
curl "$NINEROUTER_URL/v1/models/info?id=el/eleven_multilingual_v2"实现见 src/app/api/v1/models/info/route.js。返回的元数据包括id、name、kind、owned_by、endpoint(对应/v1/audio/speech),并附带params、capabilities、options等扩展字段。关键点:对于支持"按 ID 选音色"的提供商(elevenlabs、edge-tts、deepgram、inworld、local-device),返回结果中还包含voicesUrl字段,指向对应的音色列表接口(源码)。
3)列出具体提供商的音色
# 列出 edge-tts 的全部音色(可用 ?lang=vi 按语言过滤) curl "$NINEROUTER_URL/v1/audio/voices?provider=edge-tts&lang=vi" | jq '.data[].model'音色接口实现位于 src/app/api/v1/audio/voices/route.js,目前支持的提供商为:elevenlabs、deepgram、inworld、edge-tts、local-device(源码)。不传lang时返回该提供商全部音色(按语言分组后展平);传lang时仅返回该语言下的音色。每个音色条目包含id、name、lang、gender,以及可直接用于/v1/audio/speech的完整model字符串(由提供商别名前缀 + 音色 ID 拼成,见 源码)。
重要约定:
/v1/audio/speech请求体里的model字段,本质上是音色 ID 或"模型+音色"组合串,而非传统的 LLM 模型名。例如edge-tts/vi-VN-HoaiMyNeural、el/<voice_id>,或直接写openai/tts-1(此时使用该模型的默认音色)。
三、合成端点:POST /v1/audio/speech
POST $NINEROUTER_URL/v1/audio/speech该端点的实现入口是 src/app/api/v1/audio/speech/route.js,它把请求直接转交给handleTts处理。请求体字段如下:
| 字段 | 必填 | 说明 |
|---|---|---|
model | 是 | 音色 ID 或模型/音色,取自/v1/models/tts或/v1/audio/voices |
input | 是 | 要朗读的文本 |
请求体校验同样在 src/sse/handlers/tts.js:缺model返回400 Missing model,缺input返回400 Missing required field: input。
查询参数:response_format
通过 URL 查询参数控制返回格式:
?response_format=mp3(默认):直接返回原始音频字节流,Content-Type为audio/mp3;?response_format=json:返回 JSON,包含 base64 编码的音频与格式信息:{ "audio": "SUQzBAAAA...", "format": "mp3" }
响应格式的拼接逻辑见 open-sse/handlers/ttsCore.js:json模式返回{audio: base64, format}的 JSON;二进制模式则按audio/${format}设置 Content-Type 并附带Content-Length,两种模式均带 CORS 头。
可选字段:language
请求体还可携带可选的language字段作为语言提示,目前用于 Gemini 提供商(源码)。例如{"model":"gemini/...","input":"你好","language":"zh-CN"}。
四、实战示例:curl 与 Node.js
curl 保存 MP3
curl -X POST "$NINEROUTER_URL/v1/audio/speech" \ -H "Authorization: Bearer $NINEROUTER_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"openai/tts-1","input":"Hello world"}' \ --output speech.mp3Node.js 保存文件
import { writeFile } from "node:fs/promises"; const r = await fetch(`${process.env.NINEROUTER_URL}/v1/audio/speech`, { method: "POST", headers: { "Authorization": `Bearer ${process.env.NINEROUTER_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: "el/eleven_multilingual_v2", input: "Xin chào" }), }); await writeFile("speech.mp3", Buffer.from(await r.arrayBuffer()));Python 示例(requests)
import requests resp = requests.post( f"{NINEROUTER_URL}/v1/audio/speech", headers={"Authorization": f"Bearer {NINEROUTER_KEY}"}, json={"model": "edge-tts/vi-VN-HoaiMyNeural", "input": "Xin chào"}, ) open("speech.mp3", "wb").write(resp.content)若需要 JSON 格式,只需追加查询参数:POST "$NINEROUTER_URL/v1/audio/speech?response_format=json",然后对返回的audio字段做 base64 解码即可。
五、提供商差异:model 格式速查表
不同提供商对model字段的格式约定差异较大,这是使用 TTS 接口时最容易踩坑的地方。以下完整继承自技能文档并补充说明:
| Provider | model格式 | 说明 |
|---|---|---|
openai | tts-1/alloy(模型/音色)或仅音色 | 默认模型gpt-4o-mini-tts |
elevenlabs | <model_id>/<voice_id>或<voice_id> | 默认模型eleven_flash_v2_5;音色可在 Dashboard 查看 |
openrouter | openai/gpt-4o-mini-tts/alloy | 通过 chat-completions 的 audio 模态流式输出 |
edge-tts | 音色 ID,如vi-VN-HoaiMyNeural | 无需鉴权;默认vi-VN-HoaiMyNeural |
google-tts | 语言代码,如en、vi | 无需鉴权 |
local-device | 操作系统语音名(say -v ?/ SAPI) | 无需鉴权;需要本机安装ffmpeg |
deepgram | aura-asteria-en等 | Token 鉴权 |
nvidia、inworld、cartesia、playht | model/voice | 提供商专属鉴权头 |
coqui、tortoise | speaker / voice ID | 本地 localhost,无需鉴权 |
hyperbolic | 模型 ID | 请求体只有{text} |
源码层面的解析规则
无论采用哪种格式,最终都要经过统一的model/voice拆解。核心实现在 open-sse/handlers/ttsProviders/_base.js 的parseModelVoice函数:
- 优先与已知模型 ID 列表做最长前缀匹配:若
model恰好等于某模型 ID,则使用该模型 + 默认音色;若以模型ID/开头,则后半段作为音色; - 匹配不到时,取最后一个
/分割模型/音色; - 兜底:
模型ID/音色整体作为默认模型,音色用默认值。
以 OpenAI 适配器为例(open-sse/handlers/ttsProviders/openai.js):openai/tts-1/alloy会被拆成模型tts-1与音色alloy,再转发到上游POST /v1/audio/speech;若只传alloy,则使用默认模型gpt-4o-mini-tts。
六、架构与内部原理:一次 TTS 请求的完整旅程
理解内部调用链有助于排查问题和发挥接口的全部能力。一次合成请求会依次经过以下层级:
POST /v1/audio/speech └─ src/app/api/v1/audio/speech/route.js ← 路由入口 └─ src/sse/handlers/tts.js handleTts ← 校验 + 路由分发 ├─ 鉴权校验(requireApiKey) ├─ 参数校验(model / input) ├─ Combo 展开(可选)→ open-sse/services/combo.js └─ handleSingleModelTts ├─ 无凭据提供商 → 直接合成 └─ 有凭据提供商 → 凭据轮询 + 失败回退 └─ open-sse/handlers/ttsCore.js handleTtsCore ├─ 专用适配器(SPECIAL_ADAPTERS) └─ 通用配置驱动(synthesizeViaConfig) └─ ttsProviders/{provider}.js1)Combo 多模型聚合
handleTts在正式合成前会检查model是否为已配置的 Combo 名称(源码)。若命中 Combo,会按其策略(fallback回退 / 轮询等)在多个 TTS 模型间自动切换,实现"一个 Combo 名 = 一组可用音色,自动兜底"。Combo 的建模与路由逻辑在 open-sse/services/combo.js,与聊天接口共用同一套机制。
2)凭据管理与失败回退
对于需要凭据的提供商(elevenlabs、openai、deepgram、inworld 等),处理器维护了一个CREDENTIALED_PROVIDERS集合(源码,凡声明了serviceKinds含tts、且noAuth不为真、且ttsConfig.authType !== "none"的提供商都算在内)。对这类提供商,请求进入凭据轮询 + 失败回退循环(源码):每次取一组可用凭据合成,若上游报错,则通过markAccountUnavailable标记该账号暂不可用并尝试下一个账号,直到成功或全部账号耗尽。这意味着即使某个账号额度耗尽,也能自动切到备用账号继续合成。
3)适配器与通用调度
核心合成器 open-sse/handlers/ttsCore.js 采用"专用适配器 + 通用配置驱动"双轨制:
- 专用适配器:
google-tts、edge-tts、local-device、elevenlabs、openai、openrouter、gemini七个提供商有自定义的synthesize()逻辑,注册表见 open-sse/handlers/ttsProviders/index.js; - 通用配置驱动:其余提供商(hyperbolic、deepgram、nvidia、huggingface、inworld、cartesia、playht、coqui、tortoise、qwen 等)通过
ttsConfig.format映射到 genericFormats.js 中的格式处理器,按统一的{baseUrl, apiKey, text, modelId, voiceId}参数调用(源码)。
上游返回的音频统一转成{base64, format}内部结构(见 _base.js,会根据 Content-Type 识别wav/mp3/ogg),最终由createTtsResponse按response_format打包返回给调用方。
七、实践建议与常见问题
- 优先走"发现"三步曲:写死音色 ID 容易在提供商调整音色后失效,建议在应用启动时调用
/v1/models/tts或/v1/audio/voices动态获取可用的model值。 - 无需鉴权的提供商:
edge-tts、google-tts、local-device、coqui、tortoise免费可用,适合快速验证链路或做低成本批量合成;local-device依赖本机语音库与ffmpeg,运行环境需提前准备。 - 超长文本:
input为单次请求的文本,超长文本建议先分段再逐段合成,最后按序拼接音频。 - 调试技巧:返回
400 Invalid model format时,检查model是否带上了合法前缀(如el/、openai/、edge-tts/);返回503且提示All accounts unavailable时,通常是该提供商所有已存凭据均被标记为限流,可稍后重试或在仪表盘中补充凭据。 - Combo 兜底:把多个提供商/音色组织成 Combo,可显著提升合成的可用性——单一上游抖动时自动回退,不影响最终用户体验。
八、延伸阅读
- 技能定义文件:skills/9router-tts/SKILL.md(本文骨架来源)
- 9Router 主技能(环境初始化):skills/9router/SKILL.md
- 路由与处理器:src/app/api/v1/audio/speech/route.js、src/app/api/v1/audio/voices/route.js、src/sse/handlers/tts.js
- 核心合成与提供商适配:open-sse/handlers/ttsCore.js、open-sse/handlers/ttsProviders/index.js、open-sse/handlers/ttsProviders/openai.js、open-sse/handlers/ttsProviders/_base.js
- 模型发现接口:src/app/api/v1/models/[kind]/route.js、src/app/api/v1/models/info/route.js
- 单元测试示例:tests/unit/gemini-tts.test.js、tests/unit/minimax-tts.test.js(含
response_format与音色列表的断言,可作为接口行为的可执行文档)
【免费下载链接】9routerUnlimited FREE AI coding. Connect Claude Code, Codex, Cursor, Cline, Copilot, Antigravity to FREE Claude/GPT/Gemini via 40+ providers. Auto-fallback, RTK -40% tokens, never hit limits.项目地址: https://gitcode.com/GitHub_Trending/9r/9router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考