自托管语音服务别只看“能朗读”:用 TTS API 把本地合成、限流与就绪探针接进自动化
把文字转成语音接入脚本、浏览器扩展或内部工具,看起来只是一次 HTTP 请求;真正部署时,问题却通常出在模型首次下载、服务尚未预热、音频生成把 CPU 撑满,或者把一个带 Web UI 的端口直接暴露给了局域网。若每个调用方都各自安装 TTS 库、管理模型、拼接音频流,排错和权限边界会很快失控。
TTS API 是一个 MIT 许可的 FastAPI 服务。它将本地 Kokoro CPU 合成与 Microsoft Edge 在线音色放在同一 REST/WebSocket 接口之后:前者可以在模型下载完成后离线使用,后者则依赖网络。这里的重点不是把它包装成“万能语音平台”,而是把它作为自动化系统里的一个小型语音网关:调用方只面对稳定接口,部署侧集中管理模型缓存、并发、超时和网络入口。
先分清两条语音路径
项目的 POST /api/tts 接收文本、引擎、音色和播放速度,并以流式 MP3 响应;/ws/tts 则用于实时 PCM 流。两者都能服务自动化,但边界不同:
- Kokoro 是本地引擎,适合需要离线合成、可控数据边界的任务;它仍需要在首次运行时下载权重,并消耗宿主机 CPU 与内存。
- Edge 使用在线音色,适合需要更多地区化音色或更自然试听效果的场景;不能把它描述为离线能力,也要为上游失败和重试留出处理路径。
- REST MP3 很适合“生成一段通知语音后交给聊天机器人、对象存储或播放器”的异步工作;WebSocket PCM 更适合浏览器侧要边收边播、并支持暂停和跳转的交互式界面。
因此,先按数据与延迟要求选择引擎,再决定接口。不要因为接口名称统一,就假定两条引擎路径的可用性、成本和故障模式相同。
用 Compose 先把运行前条件变成可检查项
官方 README 推荐 Docker Compose。它的 Compose 配置为服务设置了 4 GiB 内存上限;README 说明两个 Kokoro pipeline 的峰值大约为 2–2.5 GiB。这个数值不是容量规划保证,但足以说明:不要在资源很紧的 CI runner 或同一台已经运行本地大模型的机器上,未经压测就并置该服务。
git clone https://github.com/babutree/TTS-API.git cd TTS-API docker compose config --quiet docker compose up --build -d docker compose ps docker compose logs --tail=100 tts-api
docker compose config --quiet 应放在启动前:它能先暴露 Compose 变量或 YAML 配置问题。第一次启动会下载 Kokoro 权重到挂载的 ./models,并预热中英文 pipeline;此时端口已监听并不代表服务可用。官方接口约定是轮询根路径,只有返回 HTTP 200 且 JSON 中 ready 为 true,才把服务交给上游自动化。模型下载或预热期间可返回 503。
until curl --fail --silent http://localhost:8880/; do sleep 5 done
上面只检查 HTTP 成功,生产脚本还应解析 JSON 并断言 ready: true。若你的部署要跨主机访问,应把 8880:8880 的默认映射视为待审查项;更稳妥的做法是限制监听范围,或通过反向代理、TLS 和网络策略提供入口,而不是把端口直接开放给不可信网络。
设置边界,而不是只复制默认配置
仓库的默认 Compose 示例中,TTS_CORS_ALLOW_ORIGINS=* 面向本地或受信任测试更省事,但不适合公网入口。对浏览器调用,应改为实际允许的来源列表。TTS_API_KEY 留空意味着服务开放;要让脚本或扩展进行鉴权,应在部署环境中设置随机密钥,且不要把它提交进仓库。
项目的 API 文档明确指出:同源 UI 与文档页可免密,而外部 REST 调用可使用 X-API-Key。这意味着 API key 不是通用的“网页登录系统”;若需要真实的网络隔离,仍应由反向代理或网络层限制访问。下面的请求将密钥留在环境变量中,避免把机密写进命令历史或 Markdown:
export TTS_API_KEY='replace-with-a-secret-from-your-secret-store'
curl --fail --silent \
-X POST http://localhost:8880/api/tts \
-H "X-API-Key: $TTS_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"text":"构建完成,预发布环境已通过健康检查。",
"engine":"kokoro",
"voice":"zf_xiaoxiao",
"speed":1.0
}' \
--output build-ready.mp3
文档规定 engine 可取 kokoro 或 edge,speed 的 API 范围是 0.5–3.0。音色不应硬编码猜测:部署后先调用受保护的 GET /api/voices,再从服务实际返回的列表选择。请求的文本上限由 MAX_TEXT_LENGTH 控制,默认值为 100000;它是单请求保护而不是队列治理策略,长文本任务仍应由调用方按段拆分并记录重试边界。
并发控制要和调用方重试配套
语音合成的常见事故不是“服务崩溃”,而是多个通知同时到来,FFmpeg 子进程或 Kokoro 推理任务开始排队,调用方又因超时重试,最终把积压放大。该项目提供 TTS_MAX_FFMPEG_PROCESSES 和 TTS_MAX_SYNTHESIS_CONCURRENCY:前者为 FFmpeg 子进程设置 fail-fast 上限,后者限制 REST 与 WebSocket 共用的 Kokoro 推理并发。官方默认值均为 2。
部署时不要把它们直接调大。先用实际的文本长度、目标音色和调用峰值做压测,再根据 CPU 核数、内存和可接受等待时间设定。对于调用方,建议把 429 与 5xx 视为可观测事件:使用有上限的退避重试、为每次语音任务设置幂等 ID,并在超过业务时限后转为文本通知,而不是无限重放合成请求。
另一个容易被忽略的参数是 TTS_SYNTHESIS_TIMEOUT_SECONDS。默认值为 0,即关闭服务端合成超时。内网低频任务可以接受这种行为;多用户或网络入口场景则应设置一个和调用方超时相协调的非零值,避免一条异常长文本长期占住资源。对 Edge 路径还应评估 EDGE_RETRY_MAX_ATTEMPTS、请求超时和缓存 TTL:上游音色目录暂时失败时,项目会保留上一次成功的列表,这不等于在线合成本身永远可用。
先测失败,再把指标接到告警
上线前至少做三组小规模演练。第一组在模型首次拉取或容器重启后连续访问根路径,确认调用方把 503 当成“尚未就绪”而不是合成失败;第二组以高于 TTS_MAX_SYNTHESIS_CONCURRENCY 的并发提交短文本,观察 429、队列等待和 CPU 峰值;第三组故意使用错误的 X-API-Key,确认不会因调用方的重试逻辑把 401 放大成请求风暴。演练记录应包含请求数、等待时间、响应码、引擎类型和任务 ID,但不要记录原始语音正文或密钥。
服务端可以把健康检查、容器重启次数、4xx/429/5xx 比例和合成耗时暴露给既有监控系统;调用方则记录“通知已发送、文本降级、放弃重试”三个终态。这样排障时能区分是模型尚未就绪、资源门槛触发、鉴权配置错误,还是 Edge 上游不可用。若只监控容器还在运行,往往会错过最重要的状态:服务虽然活着,却暂时不能交付音频。
日志也应遵循最小化原则。对语音请求而言,正文往往就是告警摘要、工单标题或内部状态,直接写入访问日志会扩大可见范围。优先记录长度、哈希或业务任务 ID,只有在受控调试窗口才保留经过脱敏的样本;同时确认反向代理不会默认记录 X-API-Key。项目自身对诊断日志中的 Authorization、X-API-Key 和 URL 查询参数 key 做了脱敏,但外围网关、APM 与 shell 历史仍要分别检查。
模型缓存也需要纳入运维约定。./models 绑定到宿主机目录的目的,是避免每次重建容器都重新下载权重;但备份、迁移和磁盘清理脚本不能把缓存误当作临时垃圾。升级镜像、改变模型版本或迁移主机后,应重新执行就绪检查和一个短文本的端到端冒烟测试,而不是仅凭 Docker 显示 Up 就宣布恢复完成。对需要完全离线运行的环境,还要在变更窗口前完成镜像、Python 依赖和模型权重的准备,在线 Edge 路径应明确关闭或从业务路由中剔除。
对于发布通知等可重复任务,建议将音频文件名或对象存储键包含任务 ID,并在成功响应后再写入“已发送”状态。网络在响应头之后中断时,不要立即假定没有生成音频;应依据任务 ID 查询自己的投递记录或采用幂等写入。语音网关负责合成,业务侧仍负责一次且仅一次的通知语义。
让自动化把“可用”当成契约
把 TTS 接到发布通知、监控告警或 Agent 工作流时,建议建立三层契约:
- 部署契约:Compose 配置能校验,容器健康检查通过,根路径返回
ready: true。 - 接口契约:调用方先从
/api/voices获得可用音色;合成请求带有来源、任务 ID 和明确超时;音频输出只在成功后被投递。 - 降级契约:本地模型未预热、Edge 上游不可达、并发触顶或鉴权失败时,调用方记录原因并回退到纯文本,而不是把失败隐藏为“没有通知”。
项目 README 还说明其输入会清理常见 Markdown 标记,避免把标题符号、链接地址和代码块逐字朗读。这对日报或告警摘要很方便,但也意味着语音文本应是专门的、简短的可听版本;不要把一整份部署日志原样投给 TTS,再期待听众从中获得有效信息。
TTS API 的价值不在于多一个网页朗读按钮,而在于把模型、流式接口和资源边界集中到一个可验证服务中。先在 loopback 或受控网络完成就绪、鉴权和并发测试,再接入自动化;把本地 Kokoro 与在线 Edge 的边界写进运行手册;对每次失败保留文本降级路径。这样,语音才会成为可靠的通知通道,而不是在高峰期制造更多队列和排障噪声。