flysuper commited on
Commit
0c322d0
·
verified ·
1 Parent(s): 00c8bc6

Upload 5 files

Browse files
Files changed (5) hide show
  1. .dockerignore +57 -0
  2. Dockerfile +32 -0
  3. README.md +168 -10
  4. app.py +279 -0
  5. requirements.txt +6 -0
.dockerignore ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Git
2
+ .git
3
+ .gitignore
4
+
5
+ # Python
6
+ __pycache__
7
+ *.pyc
8
+ *.pyo
9
+ *.pyd
10
+ .Python
11
+ env
12
+ pip-log.txt
13
+ pip-delete-this-directory.txt
14
+ .tox
15
+ .coverage
16
+ .coverage.*
17
+ .cache
18
+ nosetests.xml
19
+ coverage.xml
20
+ *.cover
21
+ *.log
22
+ .git
23
+ .mypy_cache
24
+ .pytest_cache
25
+ .hypothesis
26
+
27
+ # OS
28
+ .DS_Store
29
+ .DS_Store?
30
+ ._*
31
+ .Spotlight-V100
32
+ .Trashes
33
+ ehthumbs.db
34
+ Thumbs.db
35
+
36
+ # IDE
37
+ .vscode
38
+ .idea
39
+ *.swp
40
+ *.swo
41
+ *~
42
+
43
+ # Project specific
44
+ output/
45
+ *.mp3
46
+ *.wav
47
+ *.ogg
48
+ logs/
49
+ *.log
50
+
51
+ # Documentation
52
+ README.md
53
+ HUGGINGFACE_DEPLOYMENT.md
54
+ deploy_to_hf.py
55
+ deploy_hf.bat
56
+ Dockerfile_HF
57
+ README_HF.md
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # 設置工作目錄
4
+ WORKDIR /app
5
+
6
+ # 安裝系統依賴
7
+ RUN apt-get update && apt-get install -y \
8
+ gcc \
9
+ curl \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # 複製依賴文件
13
+ COPY requirements.txt .
14
+
15
+ # 安裝 Python 依賴
16
+ RUN pip install --no-cache-dir -r requirements.txt
17
+
18
+ # 複製應用程式代碼
19
+ COPY app.py .
20
+
21
+ # 創建必要的目錄
22
+ RUN mkdir -p static
23
+
24
+ # 暴露端口(Hugging Face Spaces 使用 7860)
25
+ EXPOSE 7860
26
+
27
+ # 健康檢查
28
+ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
29
+ CMD curl -f http://localhost:7860/health || exit 1
30
+
31
+ # 啟動命令
32
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,168 @@
1
- ---
2
- title: Tts
3
- emoji: 😻
4
- colorFrom: indigo
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Edge TTS API
3
+ emoji: 🎤
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ short_description: 基於 Microsoft Edge TTS 的文字轉語音 API 服務
10
+ ---
11
+
12
+ # Edge TTS API 服務
13
+
14
+ 這是一個基於 Microsoft Edge TTS 的文字轉語音網路服務,使用 FastAPI 框架構建,現在部署在 Hugging Face Spaces 上。
15
+
16
+ ## 功能特色
17
+
18
+ - 🎤 支援多種語音和語言
19
+ - ⚡ 快速響應的 API 服務
20
+ - 🔧 可調整語速、音量和音調
21
+ - 📱 支援 CORS,可用於前端應用
22
+ - 📊 自動生成的 API 文檔
23
+ - 🗂️ 文件管理功能
24
+
25
+ ## 快速開始
26
+
27
+ ### 1. 訪問服務
28
+
29
+ - **API 服務**: 點擊右上角的 "View API" 按鈕
30
+ - **API 文檔**: `https://your-space-name.hf.space/docs`
31
+ - **健康檢查**: `https://your-space-name.hf.space/health`
32
+
33
+ ### 2. 使用 API
34
+
35
+ #### 獲取語音列表
36
+ ```http
37
+ GET /voices
38
+ ```
39
+
40
+ #### 文字轉語音 (POST)
41
+ ```http
42
+ POST /tts
43
+ Content-Type: application/json
44
+
45
+ {
46
+ "text": "你好,世界!",
47
+ "voice": "zh-TW-HsiaoChenNeural",
48
+ "rate": "+0%",
49
+ "volume": "+0%",
50
+ "pitch": "+0Hz"
51
+ }
52
+ ```
53
+
54
+ #### 文字轉語音 (GET)
55
+ ```http
56
+ GET /tts?text=你好世界&voice=zh-TW-HsiaoChenNeural&rate=+0%&volume=+0%&pitch=+0Hz
57
+ ```
58
+
59
+ ## 使用範例
60
+
61
+ ### Python 客戶端
62
+
63
+ ```python
64
+ import requests
65
+
66
+ # 替換為您的 Hugging Face Space URL
67
+ base_url = "https://your-space-name.hf.space"
68
+
69
+ # 使用 POST 方法
70
+ response = requests.post(f"{base_url}/tts", json={
71
+ "text": "你好,這是測試文字",
72
+ "voice": "zh-TW-HsiaoChenNeural"
73
+ })
74
+
75
+ if response.json()["success"]:
76
+ audio_url = response.json()["audio_url"]
77
+ print(f"音頻文件:{base_url}{audio_url}")
78
+
79
+ # 使用 GET 方法
80
+ response = requests.get(f"{base_url}/tts", params={
81
+ "text": "你好,這是測試文字",
82
+ "voice": "zh-TW-HsiaoChenNeural"
83
+ })
84
+
85
+ # 直接下載音頻文件
86
+ with open("output.mp3", "wb") as f:
87
+ f.write(response.content)
88
+ ```
89
+
90
+ ### JavaScript 客戶端
91
+
92
+ ```javascript
93
+ // 替換為您的 Hugging Face Space URL
94
+ const baseUrl = "https://your-space-name.hf.space";
95
+
96
+ // 使用 POST 方法
97
+ fetch(`${baseUrl}/tts`, {
98
+ method: 'POST',
99
+ headers: {
100
+ 'Content-Type': 'application/json',
101
+ },
102
+ body: JSON.stringify({
103
+ text: '你好,這是測試文字',
104
+ voice: 'zh-TW-HsiaoChenNeural'
105
+ })
106
+ })
107
+ .then(response => response.json())
108
+ .then(data => {
109
+ if (data.success) {
110
+ console.log('音頻文件:', `${baseUrl}${data.audio_url}`);
111
+ }
112
+ });
113
+
114
+ // 使用 GET 方法
115
+ const audioUrl = `${baseUrl}/tts?text=你好世界&voice=zh-TW-HsiaoChenNeural`;
116
+ window.open(audioUrl, '_blank');
117
+ ```
118
+
119
+ ## 常用語音
120
+
121
+ ### 🇹🇼 台灣語音
122
+ - `zh-TW-HsiaoChenNeural` - 台灣女聲 (HsiaoChen)
123
+ - `zh-TW-HsiaoYuNeural` - 台灣女聲 (HsiaoYu)
124
+ - `zh-TW-YunJheNeural` - 台灣男聲 (YunJhe)
125
+
126
+ ### 🇨🇳 中國語音
127
+ - `zh-CN-XiaoxiaoNeural` - 中國女聲 (Xiaoxiao)
128
+ - `zh-CN-XiaoyiNeural` - 中國女聲 (Xiaoyi)
129
+ - `zh-CN-YunjianNeural` - 中國男聲 (Yunjian)
130
+ - `zh-CN-YunxiNeural` - 中國男聲 (Yunxi)
131
+ - `zh-CN-YunxiaNeural` - 中國男聲 (Yunxia)
132
+ - `zh-CN-YunyangNeural` - 中國男聲 (Yunyang)
133
+
134
+ ### 🇺🇸 美國語音
135
+ - `en-US-JennyNeural` - 美國女聲 (Jenny)
136
+ - `en-US-GuyNeural` - 美國男聲 (Guy)
137
+
138
+ ### 🇬🇧 英國語音
139
+ - `en-GB-SoniaNeural` - 英國女聲 (Sonia)
140
+ - `en-GB-RyanNeural` - 英國男聲 (Ryan)
141
+
142
+ ## 參數說明
143
+
144
+ ### 語速調整 (rate)
145
+ - `+50%` - 加快 50%
146
+ - `-50%` - 減慢 50%
147
+ - `+0%` - 正常速度
148
+
149
+ ### 音量調整 (volume)
150
+ - `+50%` - 增加音量 50%
151
+ - `-50%` - 減少音量 50%
152
+ - `+0%` - 正常音量
153
+
154
+ ### 音調調整 (pitch)
155
+ - `+50Hz` - 提高音調
156
+ - `-50Hz` - 降低音調
157
+ - `+0Hz` - 正常音調
158
+
159
+ ## 注意事項
160
+
161
+ 1. 生成的音頻文件會保存在臨時目錄中
162
+ 2. 文件會自動生成唯一 ID,避免衝突
163
+ 3. 服務需要網路連接才能使用 Microsoft Edge TTS
164
+ 4. 在 Hugging Face Spaces 上,音頻文件會在短時間後自動清理
165
+
166
+ ## 授權
167
+
168
+ 本項目基於 MIT 授權條款開源。
app.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import edge_tts
12
+ import asyncio
13
+ import os
14
+ import uuid
15
+ from typing import Optional, List
16
+ import aiofiles
17
+ import json
18
+ from urllib.parse import urlparse
19
+ import tempfile
20
+ import shutil
21
+
22
+ app = FastAPI(
23
+ title="Edge TTS API",
24
+ description="A web service for text-to-speech using Microsoft Edge TTS",
25
+ version="1.0.0"
26
+ )
27
+
28
+ # 添加 CORS 中間件(允許所有來源以方便測試)
29
+ app.add_middleware(
30
+ CORSMiddleware,
31
+ allow_origins=["*"],
32
+ allow_credentials=True,
33
+ allow_methods=["*"],
34
+ allow_headers=["*"],
35
+ )
36
+
37
+ # 創建臨時輸出目錄
38
+ OUTPUT_DIR = tempfile.mkdtemp(prefix="edge_tts_")
39
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
40
+
41
+ # 允許的來源網址清單
42
+ ALLOWED_ORIGINS = [
43
+ "https://www.dfes.ntpc.edu.tw",
44
+ "https://script.google.com/a/macros/apps.dfes.ntpc.edu.tw",
45
+ "https://www.dfes.ntpc.edu.tw/"
46
+ ]
47
+
48
+ # 掛載靜態文件(如果存在)
49
+ if os.path.exists("static"):
50
+ app.mount("/static", StaticFiles(directory="static"), name="static")
51
+
52
+ class TTSRequest(BaseModel):
53
+ text: str
54
+ voice: Optional[str] = "zh-TW-HsiaoChenNeural"
55
+ rate: Optional[str] = "+0%"
56
+ volume: Optional[str] = "+0%"
57
+ pitch: Optional[str] = "+0Hz"
58
+
59
+ class TTSResponse(BaseModel):
60
+ success: bool
61
+ message: str
62
+ audio_url: Optional[str] = None
63
+ error: Optional[str] = None
64
+
65
+ # 來源檢查函數
66
+ def _is_origin_allowed(request: Request) -> bool:
67
+ """檢查請求來源是否被允許"""
68
+ origin = request.headers.get("origin")
69
+ referer = request.headers.get("referer")
70
+
71
+ # 檢查 Origin 標頭
72
+ if origin:
73
+ for allowed_origin in ALLOWED_ORIGINS:
74
+ if origin.startswith(allowed_origin):
75
+ return True
76
+
77
+ # 檢查 Referer 標頭(備用檢查)
78
+ if referer:
79
+ for allowed_origin in ALLOWED_ORIGINS:
80
+ if referer.startswith(allowed_origin):
81
+ return True
82
+
83
+ # 如果沒有 Origin 或 Referer 標頭,檢查是否為直接 API 調用
84
+ # 允許來自 Hugging Face Spaces 的直接調用
85
+ user_agent = request.headers.get("user-agent", "")
86
+ if "huggingface" in user_agent.lower():
87
+ return True
88
+
89
+ return False
90
+
91
+ @app.get("/")
92
+ async def root():
93
+ """根路徑,返回 API 信息"""
94
+ return {
95
+ "message": "Edge TTS API Service - Hugging Face Spaces",
96
+ "version": "1.0.0",
97
+ "endpoints": {
98
+ "GET /voices": "獲取所有可用語音",
99
+ "POST /tts": "文字轉語音",
100
+ "GET /tts": "文字轉語音 (GET 方法)",
101
+ "GET /health": "健康檢查",
102
+ "GET /allowed-origins": "查看允許的來源網址"
103
+ },
104
+ "note": "此服務部署在 Hugging Face Spaces 上,僅允許特定來源網址訪問"
105
+ }
106
+
107
+ @app.get("/health")
108
+ async def health_check():
109
+ """健康檢查端點"""
110
+ return {"status": "healthy", "service": "edge-tts-api", "platform": "huggingface-spaces"}
111
+
112
+ @app.get("/allowed-origins")
113
+ async def get_allowed_origins():
114
+ """獲取允許的來源列表(僅供管理員查看)"""
115
+ return {
116
+ "allowed_origins": ALLOWED_ORIGINS,
117
+ "count": len(ALLOWED_ORIGINS),
118
+ "description": "允許的來源網址清單,支援擴充匹配"
119
+ }
120
+
121
+ @app.get("/voices")
122
+ async def get_voices():
123
+ """獲取所有可用的語音列表"""
124
+ try:
125
+ voices = await edge_tts.list_voices()
126
+ return {
127
+ "success": True,
128
+ "voices": voices,
129
+ "count": len(voices)
130
+ }
131
+ except Exception as e:
132
+ raise HTTPException(status_code=500, detail=f"獲取語音列表失敗: {str(e)}")
133
+
134
+ @app.post("/tts", response_model=TTSResponse)
135
+ async def text_to_speech(request: TTSRequest, http_request: Request):
136
+ """文字轉語音 API (POST 方法)"""
137
+ try:
138
+ # 檢查來源是否被允許
139
+ if not _is_origin_allowed(http_request):
140
+ return TTSResponse(
141
+ success=False,
142
+ message="來源網站未被允許使用此 API",
143
+ error="forbidden"
144
+ )
145
+
146
+ # 生成唯一文件名
147
+ file_id = str(uuid.uuid4())
148
+ output_file = os.path.join(OUTPUT_DIR, f"{file_id}.mp3")
149
+
150
+ # 創建 TTS 通信對象
151
+ communicate = edge_tts.Communicate(
152
+ text=request.text,
153
+ voice=request.voice,
154
+ rate=request.rate,
155
+ volume=request.volume,
156
+ pitch=request.pitch
157
+ )
158
+
159
+ # 生成語音文件
160
+ await communicate.save(output_file)
161
+
162
+ # 檢查文件是否成功創建
163
+ if not os.path.exists(output_file):
164
+ raise Exception("語音文件生成失敗")
165
+
166
+ return TTSResponse(
167
+ success=True,
168
+ message="語音生成成功",
169
+ audio_url=f"/audio/{file_id}.mp3"
170
+ )
171
+
172
+ except Exception as e:
173
+ return TTSResponse(
174
+ success=False,
175
+ message="語音生成失敗",
176
+ error=str(e)
177
+ )
178
+
179
+ @app.get("/tts")
180
+ async def text_to_speech_get(
181
+ text: str = Query(..., description="要轉換的文字"),
182
+ voice: str = Query("zh-TW-HsiaoChenNeural", description="語音名稱"),
183
+ rate: str = Query("+0%", description="語速調整"),
184
+ volume: str = Query("+0%", description="音量調整"),
185
+ pitch: str = Query("+0Hz", description="音調調整"),
186
+ http_request: Request = None
187
+ ):
188
+ """文字轉語音 API (GET 方法)"""
189
+ try:
190
+ print(f"TTS 請求: text={text}, voice={voice}, rate={rate}, volume={volume}, pitch={pitch}")
191
+
192
+ # 檢查來源是否被允許
193
+ if http_request and not _is_origin_allowed(http_request):
194
+ raise HTTPException(
195
+ status_code=403,
196
+ detail="來源網站未被允許使用此 API"
197
+ )
198
+
199
+ # 生成唯一文件名
200
+ file_id = str(uuid.uuid4())
201
+ output_file = os.path.join(OUTPUT_DIR, f"{file_id}.mp3")
202
+ print(f"輸出文件路徑: {output_file}")
203
+
204
+ # 創建 TTS 通信對象
205
+ communicate = edge_tts.Communicate(
206
+ text=text,
207
+ voice=voice,
208
+ rate=rate,
209
+ volume=volume,
210
+ pitch=pitch
211
+ )
212
+
213
+ # 生成語音文件
214
+ print("開始生成語音文件...")
215
+ await communicate.save(output_file)
216
+ print("語音文件生成完成")
217
+
218
+ # 檢查文件是否成功創建
219
+ if not os.path.exists(output_file):
220
+ print(f"文件不存在: {output_file}")
221
+ raise HTTPException(status_code=500, detail="語音文件生成失敗")
222
+
223
+ file_size = os.path.getsize(output_file)
224
+ print(f"文件大小: {file_size} bytes")
225
+
226
+ # 返回音頻文件
227
+ return FileResponse(
228
+ output_file,
229
+ media_type="audio/mpeg",
230
+ filename=f"tts_{file_id}.mp3"
231
+ )
232
+
233
+ except Exception as e:
234
+ print(f"TTS 錯誤: {str(e)}")
235
+ import traceback
236
+ traceback.print_exc()
237
+ raise HTTPException(status_code=500, detail=f"語音生成失敗: {str(e)}")
238
+
239
+ @app.get("/audio/{file_id}.mp3")
240
+ async def get_audio_file(file_id: str):
241
+ """獲取生成的音頻文件"""
242
+ file_path = os.path.join(OUTPUT_DIR, f"{file_id}.mp3")
243
+
244
+ if not os.path.exists(file_path):
245
+ raise HTTPException(status_code=404, detail="音頻文件不存在")
246
+
247
+ return FileResponse(
248
+ file_path,
249
+ media_type="audio/mpeg",
250
+ filename=f"tts_{file_id}.mp3"
251
+ )
252
+
253
+ @app.delete("/audio/{file_id}.mp3")
254
+ async def delete_audio_file(file_id: str):
255
+ """刪除音頻文件"""
256
+ file_path = os.path.join(OUTPUT_DIR, f"{file_id}.mp3")
257
+
258
+ if not os.path.exists(file_path):
259
+ raise HTTPException(status_code=404, detail="音頻文件不存在")
260
+
261
+ try:
262
+ os.remove(file_path)
263
+ return {"success": True, "message": "文件刪除成功"}
264
+ except Exception as e:
265
+ raise HTTPException(status_code=500, detail=f"文件刪除失敗: {str(e)}")
266
+
267
+ # 清理函數(可選)
268
+ @app.on_event("shutdown")
269
+ async def cleanup():
270
+ """應用關閉時清理臨時文件"""
271
+ try:
272
+ if os.path.exists(OUTPUT_DIR):
273
+ shutil.rmtree(OUTPUT_DIR)
274
+ except Exception:
275
+ pass
276
+
277
+ if __name__ == "__main__":
278
+ import uvicorn
279
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ edge-tts==7.2.0
4
+ python-multipart==0.0.6
5
+ aiofiles==23.2.1
6
+ requests==2.31.0