gemini-tts-studio
v0.3.3
Published
通用 TTS 服务:Gemini / Azure / 本地 OpenAI 兼容语音合成,Hono 中间件式挂载
Readme
Merge TTS · 通用语音合成服务与工作台
基于 Bun + Hono + React 的多厂商 TTS 服务:Google Gemini、Azure Speech、本地 OpenAI 兼容 TTS,附带一个录音棚风格的 Web 工作台。
- 后端可独立部署,也可作为 npm 包(
gemini-tts-studio/server)被其他项目直接挂载。 - 统一合成接口
POST /api/tts/unified:调用方只给文本/语种/用途/语音偏好/首选服务/凭证,模型与音色解析、超时取消、失败兜底全部由包负责。 - 新增 TTS 厂商只需实现
TTSProvider接口并注册一行,无需改路由与前端。 - 可发布的客户端入口
gemini-tts-studio/client:无头 API 客户端 +VoiceSettings音色设置组件 + 自带样式。
快速开始
bun install
# 配置密钥(二选一:弹窗填写存浏览器,或写 .env)
cp .env.example .env # GEMINI_API_KEY / AZURE_SPEECH_KEY / AZURE_SPEECH_REGION
bun run dev # 后端 :3001 + 前端 :5173| 命令 | 说明 |
|---|---|
| bun run dev | 前后端联调 |
| bun run dev:server / dev:client | 单独起后端 / 前端 |
| bun run build | 打包前端到 dist/ |
| bun start | 生产启动(含静态托管) |
| bun run build:lib | 打包可发布的 server 库到 lib/ |
端口被占用(EADDRINUSE / vite 跳到 :5174)说明有旧进程没退:先停掉再起。
Windows PowerShell:Get-NetTCPConnection -LocalPort 3001,5173 -State Listen
看 OwningProcess,Stop-Process -Id <pid> -Force 后重跑 bun run dev。
换端口也行:$env:PORT=3002(后端)+ $env:VITE_PORT=5174(前端),前端代理会自动跟随 PORT。
环境变量
| 变量 | 说明 |
|---|---|
| GEMINI_API_KEY | Google AI Studio Key(Gemini 全功能必需) |
| AZURE_SPEECH_KEY | Azure Speech Key(Azure 通道必需,与区域同区) |
| AZURE_SPEECH_REGION | 如 japaneast、eastus(必须与 Key 同区) |
| LOCAL_TTS_ENDPOINT | 本地服务 /v1 或 /v1/audio/speech 地址;也可每次请求传 endpoint |
| LOCAL_TTS_MODEL | 本地服务默认模型;请求 model 可覆盖 |
| LOCAL_TTS_API_KEY | 本地服务可选 Key;无鉴权的回环服务可留空 |
| LOCAL_TTS_VOICES | 本地服务缺少 /v1/audio/voices 时可选的逗号分隔音色 ID |
| PORT | 后端端口,默认 3001 |
前端弹窗填写的 Key 存 localStorage,请求时经 x-{provider}-api-key 头发送,优先级高于服务端 .env。
HTTP API(默认前缀 /api)
通用约定:provider 缺省为 gemini(query / body / x-tts-provider 头均可传);Key 缺失返回 401,未知厂商返回 400。
健康与发现
GET /api/health # { status, hasServerKey, providers: { gemini, azure, local }, timestamp }
GET /api/providers # 已注册厂商及默认模型/音色
GET /api/models?provider=gemini # 动态模型列表(Gemini 经原生 SDK 实时拉取;Azure 无模型概念,返回空列表)
GET /api/models/gemini-3.8-flash-tts # 指定模型详情(原生 SDK models.get)
GET /api/voices?provider=azure®ion=japaneast
GET /api/models?provider=local&endpoint=http://127.0.0.1:8880/v1
GET /api/voices?provider=local&endpoint=http://127.0.0.1:8880/v1失败显式化:需要 Key 的服务缺 Key 返回 401,未知厂商/缺参返回 400;合成和发现接口的错误为 { "error": "可读说明", "code": "E_...", "retryable": false }。密钥和上游错误原文不会写入合成错误响应。Gemini 合成缺 text / voiceName / model 直接拒绝;从动态发现接口选择可用值。
语音合成
# 一次性:返回纯二进制音频(wav/mp3)
POST /api/tts/generate
Content-Type: application/json
x-gemini-api-key: <key> # 或 x-azure-api-key
{
"provider": "gemini",
"text": "Hello! [laughs] 真好听",
"voiceName": "Puck",
"model": "gemini-3.8-flash-tts",
"speechMetadata": "cheerful",
"languageCode": "en-us", # 仅 Gemini
"region": "japaneast", # 仅 Azure
"style": "cheerful", # 仅 Azure;必须在所选音色 StyleList 中
"speakers": [ # 仅 Gemini:双人对话(支持任意角色名,如 田中/李 或 Speaker 1/2)
{ "speaker": "Speaker 1", "voiceName": "Puck" },
{ "speaker": "Speaker 2", "voiceName": "Kore" }
]
}本地服务使用同一路由,provider 为 local。请求可传完整回环地址、请求级 Key、模型、音色和语速:
{
"provider": "local",
"endpoint": "http://127.0.0.1:8880/v1/audio/speech",
"apiKey": "optional-local-key",
"model": "kokoro",
"voiceName": "ja-female",
"text": "こんにちは",
"speed": 1.1,
"format": "mp3"
}请求中的 endpoint 只接受 localhost、127.0.0.1 或 [::1];服务器可信环境变量或 createLocalTTSProvider 配置可指向其他网络地址。请求级 apiKey 优先于 x-{provider}-api-key、x-provider-api-key 和环境变量,仅用于服务端转发。GET /models 查询本地 /v1/models,GET /voices 查询 /v1/audio/voices;后者不是 OpenAI 标准接口,缺失时设置 LOCAL_TTS_VOICES,不要虚构音色。流式路由对本地服务返回单包 SSE,MIME 为上游实际音频格式。
# 流式(SSE):首包即播;Azure 自动降级为单包,前端同一套解析
POST /api/tts/stream
# 事件:data: {"audio":"<base64 PCM>","mimeType":"audio/L16;codec=pcm;rate=24000"}
# 收尾:data: {"done":true,"mimeType":"..."} 出错:data: {"error":"..."}双人对话规则(v0.3.2+):
- 支持多种剧本格式:
Speaker 1:/Speaker 2:、角色名称如田中: .../李: ...、或中括号标记[Alice] .../[Bob] ...; - 若声明了
speakers(如[{ speaker: "Speaker 1", voiceName: "Puck" }, { speaker: "Speaker 2", voiceName: "Charon" }]),系统自动将剧本中出现的角色按顺序映射到指定的说话人音色; - 连续未带前缀的台词自动归入上一位说话人;未识别出至少两位说话人时安全降级为单人发音;
voice_/voicekey_开头的自定义音色 ID 可直接填入voiceName。
统一合成(嵌入调用方,如 Lingua Studio)
调用方无需填写任何厂商默认模型/音色,最小请求只需文本:
POST /api/tts/unified
Content-Type: application/json
x-gemini-api-key: <key> # 凭证也可在 body.credentials 里按厂商分别给
{
"text": "床前明月光,疑是地上霜。",
"language": "zh-CN",
"purpose": "narration",
"voicePreference": { "gender": "female" },
"speakers": [ # 双人对话可选:指定两位说话人音色
{ "speaker": "Speaker 1", "voiceName": "Puck" },
{ "speaker": "Speaker 2", "voiceName": "Charon" }
],
"preferredService": "gemini",
"timeoutMs": 120000,
"credentials": { "geminiApiKey": "<key>" }
}成功一律 200 JSON(usedFallback 标记是否走过本地兜底):
{
"ok": true,
"audioBase64": "…",
"mimeType": "audio/wav",
"provider": "local",
"voice": "local-voice",
"model": "local-1",
"usedFallback": true,
"preferredError": { "provider": "gemini", "message": "…" },
"warnings": ["无female音色,已选用现有音色"]
}失败规则(全部显式,无静默):首选失败只兜底到本地 TTS,绝不碰浏览器语音或另一个云服务;本地成功则带 usedFallback: true + 首选的安全错误信息供调用方明示用户;本地也失败则 500 返回两次失败原因:
{
"ok": false,
"preferredError": { "provider": "gemini", "message": "…" },
"fallbackError": { "provider": "local", "message": "…" }
}缺参(空文本、未知厂商)返回 400 同构错误体。超时(默认 120s,可配)与调用方取消(signal / HTTP 断开)会立即拒绝等待中的调用。
官方异步批量(仅 Gemini,fifty-kana 这类多文本场景)
复用通用 Gemini Batch API:model = gemini-3.8-flash-tts / gemini-3.8-flash-lite-tts(模型页 Batch API Supported),每个 item 自带 AUDIO + speechConfig。异步 job 语义:创建后轮询,标准价 50%,目标 24h 内完成。双人对话不进批量(含 Speaker 标记的 item 建单即 400,请走单次合成)。
# 建单(共享默认值 + 逐条覆盖;缺 model / 有效音色 / 空文本一律 400)
POST /api/tts/batch-jobs
Content-Type: application/json
x-gemini-api-key: <key>
{
"provider": "gemini",
"model": "gemini-3.8-flash-tts",
"voiceName": "Kore",
"speechMetadata": "calm and friendly",
"displayName": "kana-46",
"items": [
{ "key": "あ", "text": "あ" },
{ "key": "い", "text": "い", "voiceName": "Puck" }
]
}
# → { "name": "batches/xxx", "state": "JOB_STATE_PENDING", "model": "..." }
# 轮询(name 含斜杠,走 query 传参;完成时带逐项 results)
GET /api/tts/batch-jobs?name=batches%2Fxxx&provider=gemini
# → { "name": "...", "state": "JOB_STATE_SUCCEEDED",
# "results": [{ "key": "あ", "ok": true, "audioBase64": "…", "mimeType": "audio/wav" },
# { "key": "い", "ok": false, "error": "…" }] }
# 取消未完成的任务
POST /api/tts/batch-jobs/cancel
{ "provider": "gemini", "name": "batches/xxx" }包内直接调用(外部项目 import ... from 'gemini-tts-studio/server'):validateBatchCreateInput / normalizeBatchJob / extractBatchItemAudio 及全部批量类型已导出;无头客户端对应 client.createBatchJob / getBatchJob / cancelBatchJob(见 gemini-tts-studio/client)。
外部项目五十音建库示例(挂载复用同一套路由,也可直调 provider):
import { createTTSApp } from 'gemini-tts-studio/server';
import { createTTSClient } from 'gemini-tts-studio/client';
// 方式一:挂载路由(网关/业务服务内复用)
const app = createTTSApp({ prefix: '/tts-engine', corsOrigins: false, enableLogger: false });
// POST /tts-engine/tts/batch-jobs → { name },再轮询 GET /tts-engine/tts/batch-jobs?name=...
// 方式二:无头客户端(宿主自渲染 UI 时用)
const client = createTTSClient({ baseUrl: 'https://tts.example.com', headers: { 'x-gemini-api-key': key } });
const kana = ['あ', 'い', 'う', 'え', 'お' /* …46 音 */];
const { name } = await client.createBatchJob({
provider: 'gemini',
model: 'gemini-3.8-flash-tts',
voiceName: 'Kore',
speechMetadata: 'calm and friendly',
displayName: 'kana-46',
items: kana.map((t) => ({ key: t, text: t })),
});
// 轮询直到完成(官方异步 job,小时级;成功后一次性落盘本地,之后永远复用)
for (;;) {
const job = await client.getBatchJob(name);
if (job.state === 'JOB_STATE_SUCCEEDED') {
for (const r of job.results || []) {
if (r.ok) await Bun.write(`./kana/${r.key}.wav`, Buffer.from(r.audioBase64!, 'base64'));
else console.error(r.key, r.error);
}
break;
}
if (/FAILED|CANCELLED|EXPIRED/.test(job.state)) throw new Error(`批量任务失败:${job.state}`);
await new Promise((r) => setTimeout(r, 60_000));
}自定义音色(仅 Gemini)
# 自然语言设计音色(返回 id + 可直接试听的 sampleAudio)
POST /api/voices/design
{ "input": "A warm narrator in his 40s with a British accent",
"displayName": "My Narrator", "gender": "male", "languageCode": "en-GB" }
# 声音复刻(base64 音频;source 10–30s 干净人声 + 同一人授权声明朗读,建议 24kHz WAV)
POST /api/voices/replicate
{ "displayName": "Me", "store": true,
"sourceAudio": "<base64>", "sourceMime": "audio/wav",
"consentAudio": "<base64>", "consentMime": "audio/wav" }
# 授权声明原文:"I am the owner of this voice and I consent to Google using
# this voice to create a synthetic voice model."
GET /api/voices/:id # 自定义音色详情(含 prompted 试听小样)
DELETE /api/voices/:id # 删除自定义音色(200 个/项目上限,1 年 TTL)作为库被其他项目使用
只需要打包 server 部分(bun run build:lib → lib/,已在 package.json 的 exports / files 中配好,npm publish 即带走;前端 React 代码不进包)。
npm i gemini-tts-studioimport { createTTSApp, registerProvider, createLocalTTSProvider } from 'gemini-tts-studio/server';
import type { TTSProvider } from 'gemini-tts-studio/server';
// 挂载到自己的 Hono/Bun 服务,任意前缀
const app = createTTSApp({ prefix: '/tts-engine', corsOrigins: false, enableLogger: false });
export default { port: 3001, fetch: app.fetch };已有 Hono Gateway 可用 gateway.route('/', app) 挂载,完整路径为 /tts-engine/tts/generate 等,无需另起端口或开启 CORS。createTTSApp 选项:prefix(默认 /api)、corsOrigins(false 关闭 CORS)、extraAllowHeaders、enableLogger、extraProviders、defaultProvider。同时导出 getProvider / listProviders / geminiProvider / azureProvider / localProvider / createLocalTTSProvider 及全部类型。
自定义厂商最小实现:
const myProvider: TTSProvider = {
id: 'mytts',
displayName: 'My TTS',
defaultModel: 'my-1',
defaultVoice: 'my-voice',
listModels: async () => ({ models: [...], source: 'fallback' }),
listVoices: async () => [...],
synthesize: async (params, apiKey) => ({ audioBuffer, mimeType }),
synthesizeStream: async (params, apiKey, onChunk) => ({ mimeType }), // 可选
};客户端入口(音色设置界面)
npm i gemini-tts-studioimport 'gemini-tts-studio/client/style.css';
import { VoiceSettings, createTTSClient } from 'gemini-tts-studio/client';
function Panel() {
const [voice, setVoice] = useState('');
return (
<VoiceSettings
baseUrl="" // 同源留空;跨域填网关地址
headers={{ 'x-gemini-api-key': key }}
value={voice}
onChange={(id) => setVoice(id)} // 厂商页签 + 搜索 + 试听 + 自定义 ID 自带
/>
);
}
// 无头调用(宿主自渲染 UI 时用)
const client = createTTSClient({ baseUrl: '', headers: { 'x-gemini-api-key': key } });
const result = await client.synthesizeUnified({ text: '你好', language: 'zh-CN', preferredService: 'gemini' });
// result.usedFallback / result.preferredError / result.warnings 按统一约定返回VoiceSettings 的试听走统一合成(previewText 可配,默认中英短句)。样式自包含:导出的 style.css 已含主题变量与用到的工具类,宿主无需配置 Tailwind; React ≥18、lucide-react 为 peer 依赖。
厂商差异速览
| | Gemini | Azure | Local |
|---|---|---|
| 流式 | 真流式(首包即播) | REST 单包,服务端自动降级,前端同一套解析 | 单包 SSE |
| 语言 | languageCode 全局切换 | 语言由音色决定(zh-CN-* 中文),无全局切换 | 由模型/音色决定 |
| 风格/标签 | speechMetadata + [laughs] 行内标签 | style 经音色 StyleList 验证;[..] 标签服务端自动去掉 | 由本地服务决定 |
| 免费额度 | AI Studio 免费测试层 | F0 每月 50 万字符 |
| Key | AI Studio API Key | Speech Key,且必须与区域同区 | 可选 |
发布
package.json 已就绪(exports/files/prepublishOnly),仓库 Actions 在 Release 发布时自动 build:lib 并 npm publish(需在仓库 Secrets 配 NPM_TOKEN)。发新版前先 bump version。
0.1.0 → 0.2.0
现有 gemini-tts-studio/server 导出及 Gemini/Azure 路由保持可用。新增 local provider、请求体 apiKey / endpoint / Azure style、Azure 音色 locale / styles,合成与发现错误增加 code 和 retryable 字段。请继续读取 error 字段向用户展示;Gemini 的模型和音色仍需先动态发现并在合成请求中明确传入。
0.2.0 → 0.3.0
新增统一合成 POST /api/tts/unified(请求/成功/失败稳定类型见上,失败只兜底本地,usedFallback + 双原因显式返回)与客户端入口 gemini-tts-studio/client(VoiceSettings 组件 + style.css + 无头 createTTSClient)。旧路由与 /server 导出保持兼容。
0.3.0 → 0.3.1
新增官方异步批量(仅 Gemini):POST /api/tts/batch-jobs 建单、GET /api/tts/batch-jobs?name=... 轮询(含逐项 results)、POST /api/tts/batch-jobs/cancel 取消。TTSProvider 新增可选 createBatchJob / getBatchJob / cancelBatchJob;/server 导出批量函数与类型,/client 增加同名无头方法。旧路由与导出保持兼容。
许可证
MIT,见 LICENSE。
目录结构
src/server/
├── app.ts # createTTSApp 工厂(可发布,无副作用)
├── index.ts # 独立部署入口(/api + 前端静态托管)
├── types.ts # 通用类型
├── unified.ts # 统一合成(模型/音色解析、本地兜底、超时取消)
├── unified.test.ts # 统一合成测试(bun:test)
└── providers/ # gemini.ts / azure.ts / local.ts / registry(index.ts) / types.ts
src/client/ # React 工作台(暂不进包)
src/client-lib/ # 可发布客户端:api.ts / VoiceSettings.tsx / client.css
scripts/build-lib.ts # lib 打包脚本