99i commited on
Commit
d84a49f
·
verified ·
1 Parent(s): f0f3d83

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +14 -0
  2. main.py +188 -0
  3. requirements.txt +6 -0
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ RUN mkdir -p audio_files
11
+
12
+ ENV TTS_HOST=${TTS_HOST}
13
+
14
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
main.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Request, Body
2
+ from fastapi.responses import JSONResponse, FileResponse
3
+ from fastapi.templating import Jinja2Templates
4
+ import httpx
5
+ import os
6
+ import uuid
7
+ from pathlib import Path
8
+ import aiofiles
9
+ from typing import Optional, Dict, Any
10
+ from datetime import datetime, timedelta
11
+ import hashlib
12
+ import json
13
+
14
+ app = FastAPI()
15
+
16
+ templates = Jinja2Templates(directory="templates")
17
+
18
+ # 配置
19
+ HOST = os.getenv("TTS_HOST", "https://99i-tts.hf.space") # 替换为实际环境变量名
20
+ AUDIO_DIR = "audio_files"
21
+ CACHE_EXPIRE_HOURS = 24 # 缓存过期时间(小时)
22
+ Path(AUDIO_DIR).mkdir(exist_ok=True)
23
+
24
+ # 内存缓存 (生产环境建议使用Redis等)
25
+ cache_store = {}
26
+
27
+ def get_cache_key(params: Dict[str, Any]) -> str:
28
+ """生成缓存键"""
29
+ param_str = json.dumps(params, sort_keys=True)
30
+ return hashlib.md5(param_str.encode()).hexdigest()
31
+
32
+ async def download_audio(params: dict, use_post: bool = False) -> str:
33
+ """下载音频文件并保存到本地"""
34
+ cache_key = get_cache_key(params)
35
+
36
+ # 检查缓存
37
+ if cache_key in cache_store:
38
+ cached_item = cache_store[cache_key]
39
+ if datetime.now() < cached_item["expire_time"]:
40
+ return cached_item["file_name"]
41
+
42
+ async with httpx.AsyncClient() as client:
43
+ try:
44
+ if use_post:
45
+ response = await client.post(f"{HOST}/tts", json=params)
46
+ else:
47
+ response = await client.get(f"{HOST}/tts", params=params)
48
+
49
+ response.raise_for_status()
50
+
51
+ # 生成唯一文件名
52
+ file_name = f"{uuid.uuid4()}.mp3"
53
+ file_path = Path(AUDIO_DIR) / file_name
54
+
55
+ # 保存音频文件
56
+ async with aiofiles.open(file_path, "wb") as f:
57
+ await f.write(response.content)
58
+
59
+ # 更新缓存
60
+ cache_store[cache_key] = {
61
+ "file_name": file_name,
62
+ "expire_time": datetime.now() + timedelta(hours=CACHE_EXPIRE_HOURS)
63
+ }
64
+
65
+ return file_name
66
+ except httpx.HTTPStatusError as e:
67
+ raise HTTPException(status_code=e.response.status_code, detail=str(e))
68
+
69
+ @app.get("/tts")
70
+ async def text_to_speech_get(
71
+ request: Request,
72
+ t: str,
73
+ v: Optional[str] = "zh-CN-XiaoxiaoMultilingualNeural",
74
+ r: Optional[int] = 0,
75
+ p: Optional[int] = 0,
76
+ o: Optional[str] = "audio-24khz-48kbitrate-mono-mp3"
77
+ ):
78
+ """GET方式转发TTS请求并返回在线播放链接"""
79
+ params = {
80
+ "t": t,
81
+ "v": v,
82
+ "r": r,
83
+ "p": p,
84
+ "o": o
85
+ }
86
+
87
+ try:
88
+ # 先尝试GET,失败后尝试POST
89
+ try:
90
+ file_name = await download_audio(params, use_post=False)
91
+ except:
92
+ file_name = await download_audio(params, use_post=True)
93
+
94
+ base_url = str(request.base_url)
95
+ audio_url = f"{base_url}audio/{file_name}"
96
+ return {"status": "success", "audio_url": audio_url, "cached": False}
97
+ except Exception as e:
98
+ return JSONResponse(
99
+ status_code=500,
100
+ content={"status": "error", "message": str(e)}
101
+ )
102
+
103
+ @app.post("/tts")
104
+ async def text_to_speech_post(
105
+ request: Request,
106
+ data: Dict[str, Any] = Body(...)
107
+ ):
108
+ """POST方式转发TTS请求并返回在线播放链接"""
109
+ params = {
110
+ "t": data.get("t"),
111
+ "v": data.get("v", "zh-CN-XiaoxiaoMultilingualNeural"),
112
+ "r": data.get("r", 0),
113
+ "p": data.get("p", 0),
114
+ "o": data.get("o", "audio-24khz-48kbitrate-mono-mp3")
115
+ }
116
+
117
+ # 检查必填参数
118
+ if not params["t"]:
119
+ raise HTTPException(status_code=400, detail="Parameter 't' (text) is required")
120
+
121
+ try:
122
+ file_name = await download_audio(params, use_post=True)
123
+ base_url = str(request.base_url)
124
+ audio_url = f"{base_url}audio/{file_name}"
125
+
126
+ # 检查是否来自缓存
127
+ cache_key = get_cache_key(params)
128
+ cached = cache_key in cache_store and datetime.now() < cache_store[cache_key]["expire_time"]
129
+
130
+ return {"status": "success", "audio_url": audio_url, "cached": cached}
131
+ except Exception as e:
132
+ return JSONResponse(
133
+ status_code=500,
134
+ content={"status": "error", "message": str(e)}
135
+ )
136
+
137
+ @app.get("/audio/{file_name}")
138
+ async def get_audio(file_name: str):
139
+ """返回音频文件"""
140
+ file_path = Path(AUDIO_DIR) / file_name
141
+ if not file_path.exists():
142
+ raise HTTPException(status_code=404, detail="Audio file not found")
143
+ return FileResponse(file_path, media_type="audio/mpeg")
144
+
145
+ @app.get("/voices")
146
+ async def get_voices(
147
+ l: Optional[str] = "zh",
148
+ d: Optional[bool] = False
149
+ ):
150
+ """获取语音列��"""
151
+ params = {}
152
+ if l:
153
+ params["l"] = l
154
+ if d:
155
+ params["d"] = ""
156
+
157
+ async with httpx.AsyncClient() as client:
158
+ try:
159
+ response = await client.get(f"{HOST}/voices", params=params)
160
+ response.raise_for_status()
161
+ return response.json()
162
+ except httpx.HTTPStatusError as e:
163
+ raise HTTPException(status_code=e.response.status_code, detail=str(e))
164
+
165
+ @app.get("/cache/status")
166
+ async def cache_status():
167
+ """获取缓存状态"""
168
+ return {
169
+ "cache_count": len(cache_store),
170
+ "expire_hours": CACHE_EXPIRE_HOURS
171
+ }
172
+
173
+ @app.delete("/cache/clear")
174
+ async def clear_cache():
175
+ """清除所有缓存"""
176
+ global cache_store
177
+ cache_store = {}
178
+ return {"status": "success", "message": "Cache cleared"}
179
+
180
+ @app.get("/")
181
+ async def read_root(request: Request):
182
+ """主页,展示语音列表"""
183
+ return templates.TemplateResponse("index.html", {"request": request})
184
+
185
+
186
+ if __name__ == "__main__":
187
+ import uvicorn
188
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ httpx
4
+ aiofiles
5
+ python-multipart
6
+ jinja2