温馨提示:
本文所述内容具有依赖性,可能因软硬条件不同而与预期有所差异,故请以实际为准,仅供参考。
零、背景需求
本站已集成语音小助手(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 错误。