从零开始搭建开源 TTS 服务 Edge-TTS

温馨提示:
本文所述内容具有依赖性,可能因软硬条件不同而与预期有所差异,故请以实际为准,仅供参考。

零、背景需求

本站已集成语音小助手(TTS)服务,然而原先的供应商已下线,导致小助手无法正常工作,因此需评估替代方案。

一、 架构逻辑

  • 前端:Typecho 博客页面(HTTPS)。
  • 中间层 (Nginx):负责 SSL 卸载、路径重写(伪静态)、反向代理,解决跨域和混合内容报错。
  • 逻辑层 (PHP):负责接收前端请求,生成唯一 ID,向后端发起生成指令,并返回符合 Nginx 规则的 URL。
  • 后端 (Docker/Python):运行 FastAPI 服务,调用 edge-tts 库与微软服务器通信(WSS协议),将生成的 MP3 存储在本地映射目录。

二、 核心组件部署

1. Python 后端 (server.py)

  • 功能:接收 POST 请求,调用 Edge-TTS 生成音频,保存文件,返回文件的相对路径 JSON。
  • 关键点:解决了 0 字节文件缓存问题,支持跨域。
# 路径:/root/soulx/server.py
import os
import logging
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
import edge_tts
from pydantic import BaseModel

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

OUTPUT_DIR = "output"
os.makedirs(OUTPUT_DIR, exist_ok=True)
app.mount("/files", StaticFiles(directory=OUTPUT_DIR), name="files")

VOICE_MAP = { 0: "zh-CN-XiaoxiaoNeural", 1: "zh-CN-YunxiNeural" }
SPEED_MAP = { 1: "-50%", 3: "-25%", 5: "+0%", 7: "+25%", 9: "+50%" }

class TTSRequest(BaseModel):
    id: str
    text: str
    text2speechSex: int = 0
    text2speechSpeed: int = 5

@app.post("/create")
async def create_tts(req: TTSRequest):
    try:
        voice = VOICE_MAP.get(req.text2speechSex, "zh-CN-XiaoxiaoNeural")
        rate = SPEED_MAP.get(req.text2speechSpeed, "+0%")
        filename = f"{req.id}_{req.text2speechSex}_{req.text2speechSpeed}.mp3"
        file_path = os.path.join(OUTPUT_DIR, filename)

        if not os.path.exists(file_path) or os.path.getsize(file_path) == 0:
            communicate = edge_tts.Communicate(req.text, voice, rate=rate)
            await communicate.save(file_path)

        return {"code": 0, "mp3_url": f"/files/{filename}"}
    except Exception as e:
        if 'file_path' in locals() and os.path.exists(file_path) and os.path.getsize(file_path) == 0:
            os.remove(file_path)
        return {"code": -1, "error": str(e)}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

2. Docker 容器化

  • 关键教训:必须使用 python:3.9 完整版 镜像。slim 版缺失底层 SSL 库,会导致 Edge-TTS 无法建立 WebSocket 连接(报错 No audio received)。
  • 国内优化:配置清华源加速 pip。
# 路径:/root/soulx/Dockerfile
FROM python:3.9

WORKDIR /app

# 升级 pip 并配置国内源
RUN pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple

# 安装依赖
RUN pip install edge-tts fastapi uvicorn pydantic -i https://pypi.tuna.tsinghua.edu.cn/simple

COPY server.py .
RUN mkdir -p output

EXPOSE 8000
CMD ["python3", "server.py"]
  • 启动命令(挂载目录、指定 DNS):
docker run -d \
  --name tts_runner \
  --restart always \
  --dns 223.5.5.5 --dns 114.114.114.114 \
  -p 8000:8000 \
  -v $(pwd)/output:/app/output \
  tts-service

三、 业务逻辑配置

1. PHP 逻辑 (functions.php)

  • 功能:向本地 Docker 发送请求,并构造一个带有“伪随机 ID”的 URL 给前端,迫使浏览器和 Nginx 认为这是一个新请求(防缓存)。
function getSpeech ($title, $content, $id, $sex, $speed) {
    // 直接请求本地 Docker 接口
    $api_url = 'http://127.0.0.1:8000/create';
    
    $post_data = json_encode(array(
        'id' => $id,
        'text' => $content,
        'text2speechSex' => (int)$sex,
        'text2speechSpeed' => (int)$speed
    ));

    $result = mcFetch(array(
        'method' => 'POST',
        'url' => $api_url,
        'header' => array('Content-Type' => 'application/json'),
        'data' => $post_data,
        'timeout' => 15
    ));

    if ($data = json_decode($result)) {
        if (isset($data->mp3_url)) {
            // /files/xxx.mp3 -> xxx.mp3
            $clean_path = str_replace('/files/', '', $data->mp3_url);
            // 返回格式:/tts-audio/随机ID/文件名.mp3
            return "/tts-audio/{$id}/" . $clean_path; 
        }
    }
    return '';
}

注:mcFetch 本质上就是发起请求并接收回复,这里不做说明。

2. Nginx 配置

  • 功能:拦截 /tts-audio/ 开头的 HTTPS 请求,忽略中间的随机 ID,直接代理到本地 Docker 的文件服务。解决了 Mixed Content 和 404 问题。
# TTS 音频代理 (本地直连 + 路径重写)
    # 匹配: /tts-audio/任意随机ID/文件名.mp3
    location ~ ^/tts-audio/[^/]+/(.*)$ {
        # $1 即为真实文件名
        
        # 转发给本机的 Docker 容器 (8000端口)
        proxy_pass http://127.0.0.1:8000/files/$1;

        # 标准头信息
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # 浏览器缓存 30 天
        expires 30d;
    }

四、 避坑指南(经验之谈)

  • IP 风控:微软对云数据中心(IDC)IP 有严格风控,香港/海外 VPS 极易连接失败(WebSocket 握手断开)。国内家庭宽带或部分国内云主机通常可用。
  • 镜像选择:Python 环境凡是涉及 WebSocket/SSL 的,尽量避开 slim / alpine 镜像,除非你非常清楚缺什么库。直接用 python:3.9 完整版最省心。
  • Snap 版 Docker:Snap 安装的 Docker 有严重的路径权限隔离,无法读取 /usr/local 等目录,会导致 failed to read dockerfile。建议卸载 Snap 版,安装官方 RPM/APT 版。
  • 混合内容:全站 HTTPS 的情况下,音频接口绝不能返回 HTTP 链接。必须通过 Nginx 反代将 HTTP 资源“包装”成 HTTPS。
  • 不要过度设计:既然 Nginx 和 Python 在同一台机器,不需要绕公网 DNS 解析,直接走 127.0.0.1 最快且最稳,也不会出现 404 错误。

ArmxMod for Typecho
个性化、自适应、功能强大的响应式主题

推广

 继续浏览关于 部署教程typechotts文字转语音开源项目edgetts 的文章

 本文最后更新于 2025/12/15 16:04:08,可能因经年累月而与现状有所差异

 引用转载请注明: VirCloud's Blog > 运维 > 从零开始搭建开源 TTS 服务 Edge-TTS