npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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&region=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-studio
import { 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-studio
import '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 打包脚本