flysuper commited on
Commit
fc61798
·
verified ·
1 Parent(s): 3e4cd48

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +316 -316
app.py CHANGED
@@ -1,317 +1,317 @@
1
- """
2
- Hugging Face Spaces 入口點
3
- 基於 main.py 但針對 Hugging Face Spaces 進行優化
4
- """
5
-
6
- from fastapi import FastAPI, HTTPException, Query, Request
7
- from fastapi.responses import FileResponse, JSONResponse
8
- from fastapi.middleware.cors import CORSMiddleware
9
- from fastapi.staticfiles import StaticFiles
10
- from pydantic import BaseModel
11
- from contextlib import asynccontextmanager
12
- import edge_tts
13
- import asyncio
14
- import os
15
- import uuid
16
- from typing import Optional, List
17
- import aiofiles
18
- import json
19
- from urllib.parse import urlparse
20
- import tempfile
21
- import shutil
22
-
23
- # 創建臨時輸出目錄
24
- OUTPUT_DIR = tempfile.mkdtemp(prefix="edge_tts_")
25
- os.makedirs(OUTPUT_DIR, exist_ok=True)
26
-
27
- @asynccontextmanager
28
- async def lifespan(app: FastAPI):
29
- # 啟動時執行
30
- print("===== Application Startup =====")
31
- yield
32
- # 關閉時執行
33
- print("===== Application Shutdown =====")
34
- try:
35
- if os.path.exists(OUTPUT_DIR):
36
- shutil.rmtree(OUTPUT_DIR)
37
- print("✅ 清理臨時文件完成")
38
- except Exception as e:
39
- print(f"⚠️ 清理臨時文件時發生錯誤: {e}")
40
-
41
- app = FastAPI(
42
- title="Edge TTS API",
43
- description="A web service for text-to-speech using Microsoft Edge TTS",
44
- version="1.0.0",
45
- lifespan=lifespan
46
- )
47
-
48
- # 添加 CORS 中間件(允許所有來源以方便測試)
49
- app.add_middleware(
50
- CORSMiddleware,
51
- allow_origins=["*"],
52
- allow_credentials=True,
53
- allow_methods=["*"],
54
- allow_headers=["*"],
55
- )
56
-
57
- # 允許的來源網址清單
58
- ALLOWED_ORIGINS = [
59
- "https://n-m2azgihk7rtcqqaxywntv75at3thf3bgps4xdlq-0lu-script.googleusercontent.com",
60
- "https://www.dfes.ntpc.edu.tw",
61
- "https://www.dfes.ntpc.edu.tw/"
62
- ]
63
-
64
- # 如果希望允許所有 GAS,取消下面兩行的註解
65
- # "https://script.google.com", # 所有 Google Apps Script
66
-
67
- # 掛載靜態文件(如果存在)
68
- if os.path.exists("static"):
69
- app.mount("/static", StaticFiles(directory="static"), name="static")
70
-
71
- class TTSRequest(BaseModel):
72
- text: str
73
- voice: Optional[str] = "zh-TW-HsiaoChenNeural"
74
- rate: Optional[str] = "+0%"
75
- volume: Optional[str] = "+0%"
76
- pitch: Optional[str] = "+0Hz"
77
-
78
- class TTSResponse(BaseModel):
79
- success: bool
80
- message: str
81
- audio_url: Optional[str] = None
82
- error: Optional[str] = None
83
-
84
- # 來源檢查函數
85
- def _is_origin_allowed(request: Request) -> bool:
86
- """檢查請求來源是否被允許"""
87
- origin = request.headers.get("origin")
88
- referer = request.headers.get("referer")
89
- user_agent = request.headers.get("user-agent", "")
90
-
91
- print(f"請求來源檢查 - Origin: {origin}, Referer: {referer}, User-Agent: {user_agent}")
92
-
93
- # 檢查允許的來源(一般網頁)
94
- if origin:
95
- for allowed_origin in ALLOWED_ORIGINS:
96
- if origin.startswith(allowed_origin):
97
- print(f"✅ Origin 匹配: {origin} 匹配 {allowed_origin}")
98
- return True
99
-
100
- if referer:
101
- for allowed_origin in ALLOWED_ORIGINS:
102
- if referer.startswith(allowed_origin):
103
- print(f"✅ Referer 匹配: {referer} 匹配 {allowed_origin}")
104
- return True
105
-
106
- # 允許來自 Hugging Face Spaces 的直接調用
107
- if "huggingface" in user_agent.lower():
108
- print("✅ 檢測到 Hugging Face Spaces 環境")
109
- return True
110
-
111
- print(f"❌ 請求被拒絕 - Origin: {origin}, Referer: {referer}")
112
- return False
113
-
114
- @app.get("/")
115
- async def root():
116
- """根路徑,返回 API 信息"""
117
- return {
118
- "message": "Edge TTS API Service - Hugging Face Spaces",
119
- "version": "1.0.0",
120
- "endpoints": {
121
- "GET /voices": "獲取所有可用語音",
122
- "POST /tts": "文字轉語音",
123
- "GET /tts": "文字轉語音 (GET 方法)",
124
- "GET /health": "健康檢查",
125
- "GET /allowed-origins": "查看允許的來源網址",
126
- "GET /debug-request": "調試請求信息"
127
- },
128
- "note": "此服務部署在 Hugging Face Spaces 上,僅允許特定來源網址訪問"
129
- }
130
-
131
- @app.get("/health")
132
- async def health_check():
133
- """健康檢查端點"""
134
- return {"status": "healthy", "service": "edge-tts-api", "platform": "huggingface-spaces"}
135
-
136
- @app.get("/allowed-origins")
137
- async def get_allowed_origins():
138
- """獲取允許的來源列表(僅供管理員查看)"""
139
- return {
140
- "allowed_origins": ALLOWED_ORIGINS,
141
- "count": len(ALLOWED_ORIGINS),
142
- "description": "允許的來源網址清單,支援擴充匹配"
143
- }
144
-
145
- @app.get("/debug-request")
146
- async def debug_request(request: Request):
147
- """調試端點:顯示請求的詳細信息"""
148
- headers = dict(request.headers)
149
- return {
150
- "method": request.method,
151
- "url": str(request.url),
152
- "headers": headers,
153
- "origin": headers.get("origin"),
154
- "referer": headers.get("referer"),
155
- "user_agent": headers.get("user-agent"),
156
- "is_allowed": _is_origin_allowed(request),
157
- "client_ip": request.client.host if request.client else "unknown",
158
- "request_info": {
159
- "has_origin": bool(headers.get("origin")),
160
- "has_referer": bool(headers.get("referer")),
161
- "is_huggingface": "huggingface" in headers.get("user-agent", "").lower(),
162
- "api_key_provided": "x-api-key" in headers
163
- }
164
- }
165
-
166
-
167
- @app.get("/voices")
168
- async def get_voices():
169
- """獲取所有可用的語音列表"""
170
- try:
171
- voices = await edge_tts.list_voices()
172
- return {
173
- "success": True,
174
- "voices": voices,
175
- "count": len(voices)
176
- }
177
- except Exception as e:
178
- raise HTTPException(status_code=500, detail=f"獲取語音列表失敗: {str(e)}")
179
-
180
- @app.post("/tts", response_model=TTSResponse)
181
- async def text_to_speech(request: TTSRequest, http_request: Request):
182
- """文字轉語音 API (POST 方法)"""
183
- try:
184
- # 檢查來源是否被允許
185
- if not _is_origin_allowed(http_request):
186
- return TTSResponse(
187
- success=False,
188
- message="來源網站未被允許使用此 API",
189
- error="forbidden"
190
- )
191
-
192
- # 生成唯一文件名
193
- file_id = str(uuid.uuid4())
194
- output_file = os.path.join(OUTPUT_DIR, f"{file_id}.mp3")
195
-
196
- # 創建 TTS 通信對象
197
- communicate = edge_tts.Communicate(
198
- text=request.text,
199
- voice=request.voice,
200
- rate=request.rate,
201
- volume=request.volume,
202
- pitch=request.pitch
203
- )
204
-
205
- # 生成語音文件
206
- await communicate.save(output_file)
207
-
208
- # 檢查文件是否成功創建
209
- if not os.path.exists(output_file):
210
- raise Exception("語音文件生成失敗")
211
-
212
- return TTSResponse(
213
- success=True,
214
- message="語音生成成功",
215
- audio_url=f"/audio/{file_id}.mp3"
216
- )
217
-
218
- except Exception as e:
219
- return TTSResponse(
220
- success=False,
221
- message="語音生成失敗",
222
- error=str(e)
223
- )
224
-
225
- @app.get("/tts")
226
- async def text_to_speech_get(
227
- text: str = Query(..., description="要轉換的文字"),
228
- voice: str = Query("zh-TW-HsiaoChenNeural", description="語音名稱"),
229
- rate: str = Query("+0%", description="語速調整"),
230
- volume: str = Query("+0%", description="音量調整"),
231
- pitch: str = Query("+0Hz", description="音調調整"),
232
- http_request: Request = None
233
- ):
234
- """文字轉語音 API (GET 方法)"""
235
- try:
236
- print(f"TTS 請求: text={text}, voice={voice}, rate={rate}, volume={volume}, pitch={pitch}")
237
-
238
- # 檢查來源是否被允許
239
- if http_request and not _is_origin_allowed(http_request):
240
- raise HTTPException(
241
- status_code=403,
242
- detail="來源網站未被允許使用此 API"
243
- )
244
-
245
- # 生成唯一文件名
246
- file_id = str(uuid.uuid4())
247
- output_file = os.path.join(OUTPUT_DIR, f"{file_id}.mp3")
248
- print(f"輸出文件路徑: {output_file}")
249
-
250
- # 創建 TTS 通信對象
251
- communicate = edge_tts.Communicate(
252
- text=text,
253
- voice=voice,
254
- rate=rate,
255
- volume=volume,
256
- pitch=pitch
257
- )
258
-
259
- # 生成語音文件
260
- print("開始生成語音文件...")
261
- await communicate.save(output_file)
262
- print("語音文件生成完成")
263
-
264
- # 檢查文件是否成功創建
265
- if not os.path.exists(output_file):
266
- print(f"文件不存在: {output_file}")
267
- raise HTTPException(status_code=500, detail="語音文件生成失敗")
268
-
269
- file_size = os.path.getsize(output_file)
270
- print(f"文件大小: {file_size} bytes")
271
-
272
- # 返回音頻文件
273
- return FileResponse(
274
- output_file,
275
- media_type="audio/mpeg",
276
- filename=f"tts_{file_id}.mp3"
277
- )
278
-
279
- except Exception as e:
280
- print(f"TTS 錯誤: {str(e)}")
281
- import traceback
282
- traceback.print_exc()
283
- raise HTTPException(status_code=500, detail=f"語音生成失敗: {str(e)}")
284
-
285
- @app.get("/audio/{file_id}.mp3")
286
- async def get_audio_file(file_id: str):
287
- """獲取生成的音頻文件"""
288
- file_path = os.path.join(OUTPUT_DIR, f"{file_id}.mp3")
289
-
290
- if not os.path.exists(file_path):
291
- raise HTTPException(status_code=404, detail="音頻文件不存在")
292
-
293
- return FileResponse(
294
- file_path,
295
- media_type="audio/mpeg",
296
- filename=f"tts_{file_id}.mp3"
297
- )
298
-
299
- @app.delete("/audio/{file_id}.mp3")
300
- async def delete_audio_file(file_id: str):
301
- """刪除音頻文件"""
302
- file_path = os.path.join(OUTPUT_DIR, f"{file_id}.mp3")
303
-
304
- if not os.path.exists(file_path):
305
- raise HTTPException(status_code=404, detail="音頻文件不存在")
306
-
307
- try:
308
- os.remove(file_path)
309
- return {"success": True, "message": "文件刪除成功"}
310
- except Exception as e:
311
- raise HTTPException(status_code=500, detail=f"文件刪除失敗: {str(e)}")
312
-
313
- # 清理函數已移至 lifespan 處理器中
314
-
315
- if __name__ == "__main__":
316
- import uvicorn
317
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ """
2
+ Hugging Face Spaces 入口點
3
+ 基於 main.py 但針對 Hugging Face Spaces 進行優化
4
+ """
5
+
6
+ from fastapi import FastAPI, HTTPException, Query, Request
7
+ from fastapi.responses import FileResponse, JSONResponse
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.staticfiles import StaticFiles
10
+ from pydantic import BaseModel
11
+ from contextlib import asynccontextmanager
12
+ import edge_tts
13
+ import asyncio
14
+ import os
15
+ import uuid
16
+ from typing import Optional, List
17
+ import aiofiles
18
+ import json
19
+ from urllib.parse import urlparse
20
+ import tempfile
21
+ import shutil
22
+
23
+ # 創建臨時輸出目錄
24
+ OUTPUT_DIR = tempfile.mkdtemp(prefix="edge_tts_")
25
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
26
+
27
+ @asynccontextmanager
28
+ async def lifespan(app: FastAPI):
29
+ # 啟動時執行
30
+ print("===== Application Startup =====")
31
+ yield
32
+ # 關閉時執行
33
+ print("===== Application Shutdown =====")
34
+ try:
35
+ if os.path.exists(OUTPUT_DIR):
36
+ shutil.rmtree(OUTPUT_DIR)
37
+ print("✅ 清理臨時文件完成")
38
+ except Exception as e:
39
+ print(f"⚠️ 清理臨時文件時發生錯誤: {e}")
40
+
41
+ app = FastAPI(
42
+ title="Edge TTS API",
43
+ description="A web service for text-to-speech using Microsoft Edge TTS",
44
+ version="1.0.0",
45
+ lifespan=lifespan
46
+ )
47
+
48
+ # 添加 CORS 中間件(允許所有來源以方便測試)
49
+ app.add_middleware(
50
+ CORSMiddleware,
51
+ allow_origins=["*"],
52
+ allow_credentials=True,
53
+ allow_methods=["*"],
54
+ allow_headers=["*"],
55
+ )
56
+
57
+ # 允許的來源網址清單
58
+ ALLOWED_ORIGINS = [
59
+ "https://n-m2azgihk7rtcqqaxywntv75at3thf3bgps4xdlq-0lu-script.googleusercontent.com",
60
+ "https://www.dfes.ntpc.edu.tw",
61
+ "https://demo.dfes.ntpc.edu.tw"
62
+ ]
63
+
64
+ # 如果希望允許所有 GAS,取消下面兩行的註解
65
+ # "https://script.google.com", # 所有 Google Apps Script
66
+
67
+ # 掛載靜態文件(如果存在)
68
+ if os.path.exists("static"):
69
+ app.mount("/static", StaticFiles(directory="static"), name="static")
70
+
71
+ class TTSRequest(BaseModel):
72
+ text: str
73
+ voice: Optional[str] = "zh-TW-HsiaoChenNeural"
74
+ rate: Optional[str] = "+0%"
75
+ volume: Optional[str] = "+0%"
76
+ pitch: Optional[str] = "+0Hz"
77
+
78
+ class TTSResponse(BaseModel):
79
+ success: bool
80
+ message: str
81
+ audio_url: Optional[str] = None
82
+ error: Optional[str] = None
83
+
84
+ # 來源檢查函數
85
+ def _is_origin_allowed(request: Request) -> bool:
86
+ """檢查請求來源是否被允許"""
87
+ origin = request.headers.get("origin")
88
+ referer = request.headers.get("referer")
89
+ user_agent = request.headers.get("user-agent", "")
90
+
91
+ print(f"請求來源檢查 - Origin: {origin}, Referer: {referer}, User-Agent: {user_agent}")
92
+
93
+ # 檢查允許的來源(一般網頁)
94
+ if origin:
95
+ for allowed_origin in ALLOWED_ORIGINS:
96
+ if origin.startswith(allowed_origin):
97
+ print(f"✅ Origin 匹配: {origin} 匹配 {allowed_origin}")
98
+ return True
99
+
100
+ if referer:
101
+ for allowed_origin in ALLOWED_ORIGINS:
102
+ if referer.startswith(allowed_origin):
103
+ print(f"✅ Referer 匹配: {referer} 匹配 {allowed_origin}")
104
+ return True
105
+
106
+ # 允許來自 Hugging Face Spaces 的直接調用
107
+ if "huggingface" in user_agent.lower():
108
+ print("✅ 檢測到 Hugging Face Spaces 環境")
109
+ return True
110
+
111
+ print(f"❌ 請求被拒絕 - Origin: {origin}, Referer: {referer}")
112
+ return False
113
+
114
+ @app.get("/")
115
+ async def root():
116
+ """根路徑,返回 API 信息"""
117
+ return {
118
+ "message": "Edge TTS API Service - Hugging Face Spaces",
119
+ "version": "1.0.0",
120
+ "endpoints": {
121
+ "GET /voices": "獲取所有可用語音",
122
+ "POST /tts": "文字轉語音",
123
+ "GET /tts": "文字轉語音 (GET 方法)",
124
+ "GET /health": "健康檢查",
125
+ "GET /allowed-origins": "查看允許的來源網址",
126
+ "GET /debug-request": "調試請求信息"
127
+ },
128
+ "note": "此服務部署在 Hugging Face Spaces 上,僅允許特定來源網址訪問"
129
+ }
130
+
131
+ @app.get("/health")
132
+ async def health_check():
133
+ """健康檢查端點"""
134
+ return {"status": "healthy", "service": "edge-tts-api", "platform": "huggingface-spaces"}
135
+
136
+ @app.get("/allowed-origins")
137
+ async def get_allowed_origins():
138
+ """獲取允許的來源列表(僅供管理員查看)"""
139
+ return {
140
+ "allowed_origins": ALLOWED_ORIGINS,
141
+ "count": len(ALLOWED_ORIGINS),
142
+ "description": "允許的來源網址清單,支援擴充匹配"
143
+ }
144
+
145
+ @app.get("/debug-request")
146
+ async def debug_request(request: Request):
147
+ """調試端點:顯示請求的詳細信息"""
148
+ headers = dict(request.headers)
149
+ return {
150
+ "method": request.method,
151
+ "url": str(request.url),
152
+ "headers": headers,
153
+ "origin": headers.get("origin"),
154
+ "referer": headers.get("referer"),
155
+ "user_agent": headers.get("user-agent"),
156
+ "is_allowed": _is_origin_allowed(request),
157
+ "client_ip": request.client.host if request.client else "unknown",
158
+ "request_info": {
159
+ "has_origin": bool(headers.get("origin")),
160
+ "has_referer": bool(headers.get("referer")),
161
+ "is_huggingface": "huggingface" in headers.get("user-agent", "").lower(),
162
+ "api_key_provided": "x-api-key" in headers
163
+ }
164
+ }
165
+
166
+
167
+ @app.get("/voices")
168
+ async def get_voices():
169
+ """獲取所有可用的語音列表"""
170
+ try:
171
+ voices = await edge_tts.list_voices()
172
+ return {
173
+ "success": True,
174
+ "voices": voices,
175
+ "count": len(voices)
176
+ }
177
+ except Exception as e:
178
+ raise HTTPException(status_code=500, detail=f"獲取語音列表失敗: {str(e)}")
179
+
180
+ @app.post("/tts", response_model=TTSResponse)
181
+ async def text_to_speech(request: TTSRequest, http_request: Request):
182
+ """文字轉語音 API (POST 方法)"""
183
+ try:
184
+ # 檢查來源是否被允許
185
+ if not _is_origin_allowed(http_request):
186
+ return TTSResponse(
187
+ success=False,
188
+ message="來源網站未被允許使用此 API",
189
+ error="forbidden"
190
+ )
191
+
192
+ # 生成唯一文件名
193
+ file_id = str(uuid.uuid4())
194
+ output_file = os.path.join(OUTPUT_DIR, f"{file_id}.mp3")
195
+
196
+ # 創建 TTS 通信對象
197
+ communicate = edge_tts.Communicate(
198
+ text=request.text,
199
+ voice=request.voice,
200
+ rate=request.rate,
201
+ volume=request.volume,
202
+ pitch=request.pitch
203
+ )
204
+
205
+ # 生成語音文件
206
+ await communicate.save(output_file)
207
+
208
+ # 檢查文件是否成功創建
209
+ if not os.path.exists(output_file):
210
+ raise Exception("語音文件生成失敗")
211
+
212
+ return TTSResponse(
213
+ success=True,
214
+ message="語音生成成功",
215
+ audio_url=f"/audio/{file_id}.mp3"
216
+ )
217
+
218
+ except Exception as e:
219
+ return TTSResponse(
220
+ success=False,
221
+ message="語音生成失敗",
222
+ error=str(e)
223
+ )
224
+
225
+ @app.get("/tts")
226
+ async def text_to_speech_get(
227
+ text: str = Query(..., description="要轉換的文字"),
228
+ voice: str = Query("zh-TW-HsiaoChenNeural", description="語音名稱"),
229
+ rate: str = Query("+0%", description="語速調整"),
230
+ volume: str = Query("+0%", description="音量調整"),
231
+ pitch: str = Query("+0Hz", description="音調調整"),
232
+ http_request: Request = None
233
+ ):
234
+ """文字轉語音 API (GET 方法)"""
235
+ try:
236
+ print(f"TTS 請求: text={text}, voice={voice}, rate={rate}, volume={volume}, pitch={pitch}")
237
+
238
+ # 檢查來源是否被允許
239
+ if http_request and not _is_origin_allowed(http_request):
240
+ raise HTTPException(
241
+ status_code=403,
242
+ detail="來源網站未被允許使用此 API"
243
+ )
244
+
245
+ # 生成唯一文件名
246
+ file_id = str(uuid.uuid4())
247
+ output_file = os.path.join(OUTPUT_DIR, f"{file_id}.mp3")
248
+ print(f"輸出文件路徑: {output_file}")
249
+
250
+ # 創建 TTS 通信對象
251
+ communicate = edge_tts.Communicate(
252
+ text=text,
253
+ voice=voice,
254
+ rate=rate,
255
+ volume=volume,
256
+ pitch=pitch
257
+ )
258
+
259
+ # 生成語音文件
260
+ print("開始生成語音文件...")
261
+ await communicate.save(output_file)
262
+ print("語音文件生成完成")
263
+
264
+ # 檢查文件是否成功創建
265
+ if not os.path.exists(output_file):
266
+ print(f"文件不存在: {output_file}")
267
+ raise HTTPException(status_code=500, detail="語音文件生成失敗")
268
+
269
+ file_size = os.path.getsize(output_file)
270
+ print(f"文件大小: {file_size} bytes")
271
+
272
+ # 返回音頻文件
273
+ return FileResponse(
274
+ output_file,
275
+ media_type="audio/mpeg",
276
+ filename=f"tts_{file_id}.mp3"
277
+ )
278
+
279
+ except Exception as e:
280
+ print(f"TTS 錯誤: {str(e)}")
281
+ import traceback
282
+ traceback.print_exc()
283
+ raise HTTPException(status_code=500, detail=f"語音生成失敗: {str(e)}")
284
+
285
+ @app.get("/audio/{file_id}.mp3")
286
+ async def get_audio_file(file_id: str):
287
+ """獲取生成的音頻文件"""
288
+ file_path = os.path.join(OUTPUT_DIR, f"{file_id}.mp3")
289
+
290
+ if not os.path.exists(file_path):
291
+ raise HTTPException(status_code=404, detail="音頻文件不存在")
292
+
293
+ return FileResponse(
294
+ file_path,
295
+ media_type="audio/mpeg",
296
+ filename=f"tts_{file_id}.mp3"
297
+ )
298
+
299
+ @app.delete("/audio/{file_id}.mp3")
300
+ async def delete_audio_file(file_id: str):
301
+ """刪除音頻文件"""
302
+ file_path = os.path.join(OUTPUT_DIR, f"{file_id}.mp3")
303
+
304
+ if not os.path.exists(file_path):
305
+ raise HTTPException(status_code=404, detail="音頻文件不存在")
306
+
307
+ try:
308
+ os.remove(file_path)
309
+ return {"success": True, "message": "文件刪除成功"}
310
+ except Exception as e:
311
+ raise HTTPException(status_code=500, detail=f"文件刪除失敗: {str(e)}")
312
+
313
+ # 清理函數已移至 lifespan 處理器中
314
+
315
+ if __name__ == "__main__":
316
+ import uvicorn
317
  uvicorn.run(app, host="0.0.0.0", port=7860)