Lee's Blog
  • 首页
  • 博客
  • 作品
  • 成长
  • 学习
  • 打榜
  • 练习
  • 关于
登录 / 注册
✦

保持联系

关注我的最新动态

GitHubbilibili

© 2026 Lee's Blog

首页/博客/EZTor/TTS大便样
TTS大便样
EZTor2026年5月31日6 分钟阅读

TTS大便样

后端
X微博

当微软 Edge TTS 被墙之后:一次 API 调用的迂回作战

引子

故事发生在一个再普通不过的夜晚。

我在维护一个英语学习 Web 应用,其中一个核心功能是发音朗读——用户查词或练 dictation 时,点击按钮就能听到单词的标准发音。

之前这个功能一直用着微软 Edge TTS,走的 @lobehub/tts 库,Node.js 进程内拉起 WebSocket 直连 speech.platform.bing.com,稳定跑了大半年。

直到有一天,用户反馈说:"听不到声音了。"

第一轮排查:代码层面看起来是好的

本地 npm run dev 打开浏览器,点发音按钮——正常出声。再点,也正常。

上生产服务器测——

WebSocket error occurred

嗯。curl 一下:

$ curl https://speech.platform.bing.com
Our services aren't available right now

破案了。从国内服务器到微软 Edge TTS 的网络链路断了。 可能是最近网络策略的调整,也可能是某条海缆出了状况——总之,speech.platform.bing.com 从国内无法直连了。

而本地能出声,是因为我开着 Clash。

架构的剖面

在深入改代码之前,先看一眼当时的调用链路:

浏览器 → Next.js API (/api/tts) → @lobehub/tts (EdgeSpeechTTS)
     → WebSocket → speech.platform.bing.com
                                            ↓
                                      被阻断 ❌
                                            ↓
               回退到浏览器 SpeechSynthesis(质量差、不稳定)

客户端侧有一个回退机制:如果服务器 TTS 挂了,就 fallback 到浏览器自带的 SpeechSynthesis。但那个引擎的中英文混读效果一言难尽,属于能响但不好听的水平。

翻项目,发现一个沉睡的 vendor

我翻了翻项目仓库,发现 vendor/ 目录下躺着一个老熟人:

vendor/edgeTTS-openai-api/

这是一个用 Flask + edge-tts(Python)写的轻量级 TTS 服务器,暴露 OpenAI 兼容的 API(POST /v1/audio/speech)。README 里写着支持一键部署到 HuggingFace Spaces。

这显然是为了解决网络问题而预留的备选方案——只是之前一直没启用。

但等等,它用的也是 edge-tts,底层也是 WebSocket 连微软。部署在同样的国内服务器上,照样连不出去。

所以问题的关键不在于用什么语言写 TTS 逻辑,而在于:从哪里发起连接。

决策树

画出拓扑之后,选项变得清晰:

我们的服务器(国内)
    ├─ Option A: 直接 Python 代理部署在 HuggingFace Spaces(海外)
    │              → 海外直连微软,国内服务器只需 HTTP 调 HuggingFace
    │
    ├─ Option B: 同一台服务器上跑 Python 代理 + HTTP_PROXY 翻墙
    │              → 依赖上游代理的稳定性
    │
    └─ Option C: 买一台海外 VPS 专门跑 TTS 代理
                   → 成本最高,但延迟最低

三个选项的权衡:

选项 运维成本 延迟 翻墙依赖
A. HuggingFace Spaces 极低(一键部署) 中等(多一跳) 无
B. 同机 + HTTP_PROXY 中等(要维护代理) 低 是
C. 海外 VPS 高 最低 无

最终选了 Option A。原因很朴素:HuggingFace 免费额度够用、一键部署不需要 DevOps 技能、并且全程不需要我配任何代理或 VPN。

手术:改代码

既然决定走 proxy 路线,代码改动其实很小。TTS 是模块化的,只需替换底层实现,上层 API 和 UI 完全不动。

修改前(src/lib/tts.ts,简化版)

import { EdgeSpeechTTS } from '@lobehub/tts'
import WebSocket from 'ws'

// 注入 WebSocket 全局变量
;(globalThis as any).WebSocket = WebSocket
const tts = new EdgeSpeechTTS()

export async function synthesizeSpeech(req) {
  const response = await tts.create({
    input: req.input,
    options: { voice, rate: speed },
    signal: abortController.signal,
  })
  return response
}

修改后

const TTS_PROXY_URL = process.env.TTS_PROXY_URL
  || 'http://localhost:5050/v1/audio/speech'

export async function synthesizeSpeech(req) {
  const response = await fetch(TTS_PROXY_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}),
    },
    body: JSON.stringify({
      input: req.input,
      voice: normalizeVoice(req.voice),
      speed: clampSpeed(req.speed),
      response_format: 'mp3',
    }),
    signal: abortController.signal,
  })

  if (!response.ok) throw new Error('TTS proxy error')
  return response
}

核心变化是:把 in-process 的 WebSocket 直连,换成了 out-of-process 的 HTTP 调用。

好处是:

  • 调用方(Next.js API route)完全无感知——synthesizeSpeech() 返回的依然是 Response 对象
  • 所有 UI 组件不动——它们调的是 speakText('/api/tts'),链路末端的变化对它们是透明的
  • 后端可以随便换——改两行环境变量就能在本地代理、HuggingFace、海外 VPS 之间切换

副作用:依赖瘦身

移除 @lobehub/tts 和 ws 两个包之后,next.config.ts 里的 serverExternalPackages 也清了两项。顺便把 node_modules 里的体积倒腾掉了——不过那是另一个话题了。

目前的状态

代码层面的活干完了。改动范围:

文件 改动
src/lib/tts.ts 全部重写,替换核心实现
next.config.ts 移除废弃的 external packages
.env / .env.example 新增 TTS_PROXY_URL、TTS_PROXY_API_KEY

TypeScript 编译零错误。

唯一下的部分就是:我还没部署到 HuggingFace。 昨晚只睡了六个小时,脑子不太转了,就先到这里吧。

后来我跟 AI 说:"我没有部署到 hugging face 的经验,而且我昨晚只睡了六个小时,要不先放一放,帮我写一篇博客吧。"

就有了你现在读到的这篇。

回头看的感悟

做技术的人常会下意识追求"最优雅"的解法——比如在 Node.js 里给 WebSocket 套一层代理层。但实际工程里,最简单的方案往往就是对的。

用 HuggingFace Spaces 部署一个 Flask 应用来转发 TTS 流量,技术上毫无炫酷之处。但它:

  • 不需要我理解 WebSocket 代理的原理
  • 不需要我在生产服务器上搭 Clash
  • 零成本(HuggingFace 免费)
  • 零运维

把复杂藏在简单后面,是工程的真功夫(虽然这个功夫是 AI 帮我做的)。


写完这篇博客之后,我打算去睡个好觉。TTS 的事,明天再说。

这篇博客是 5月25日 写的,今天是 5月31 日,我来说说我最后是怎么解决TTS的。

很简单,我用了小米mimo的tts模型,免费的,接个API,把key发给AI就弄好了。

← 上一篇一次关于通信的探索下一篇 →职业规划软件构想

相关文章

  • 退出后重登卡在"登录中":一次靠 Network 面板定位的 NextAuth session 时序问题约 5 分钟
  • 用户说"第一天的闪卡更好玩"——一次把 bug 当需求改的复盘约 6 分钟
  • meteorological 变成了 neteorological?记一次长单词被"削头"的 UI 排查约 6 分钟

评论

需要登录账号才能发表评论。

  • 加载中…

本页内容

  • 当微软 Edge TTS 被墙之后:一次 API 调用的迂回作战
  • 引子
  • 第一轮排查:代码层面看起来是好的
  • 架构的剖面
  • 翻项目,发现一个沉睡的 vendor
  • 决策树
  • 手术:改代码
  • 修改前(`src/lib/tts.ts`,简化版)
  • 修改后
  • 副作用:依赖瘦身
  • 目前的状态
  • 回头看的感悟