]> acesimba.cloud Git - words.git/commitdiff
TTS: 替换浏览器内置发音为有道美式发音
authorroot <root@localhost.localdomain>
Fri, 26 Jun 2026 14:34:35 +0000 (22:34 +0800)
committerroot <root@localhost.localdomain>
Fri, 26 Jun 2026 14:34:35 +0000 (22:34 +0800)
- 后端新增 /api/tts/{word} 端点,代理有道 dictvoice type=2 美式发音
- 音频文件缓存到 backend/db/tts_cache/,二次请求直接读缓存
- 前端 StudyMode/TestMode 改用 new Audio() 播放后端 TTS
- 学习模式保留读两遍+500ms间隔,测试模式读一遍
- .gitignore 忽略 TTS 缓存目录

.gitignore
backend/main.py
frontend/src/components/StudyMode.js
frontend/src/components/TestMode.js

index 4ebf247921fa9f133c208493364b5caf35935c7e..3bd2a53ed883a315fb3896971134cc1ce38e1dd5 100644 (file)
@@ -14,3 +14,6 @@ frontend/build/
 backend/a/log.txt
 backend/a/error_log.csv
 *.db
+
+# TTS audio cache (runtime-generated)
+backend/db/tts_cache/
index e6d47537b490824602a2a6fc4f56ee4f711f070c..ec6850fe7eaa95be1fd24241f756dec61120b351 100644 (file)
@@ -3,6 +3,9 @@ from fastapi.middleware.cors import CORSMiddleware
 import sqlite3
 import os
 import datetime
+import urllib.request
+import urllib.parse
+import urllib.error
 from pydantic import BaseModel
 from typing import List, Optional
 
@@ -197,6 +200,47 @@ async def get_picture(pic_id: int):
     except Exception as e:
         raise HTTPException(status_code=500, detail=str(e))
 
+# --- TTS 代理(有道美式发音 + 本地缓存) ---
+TTS_CACHE_DIR = os.path.join(BASE_DIR, "db", "tts_cache")
+os.makedirs(TTS_CACHE_DIR, exist_ok=True)
+
+@app.get("/api/tts/{word}")
+async def get_tts(word: str):
+    word = word.strip()
+    if not word:
+        raise HTTPException(status_code=400, detail="empty word")
+
+    # 缓存文件名:小写 + 下划线,避免特殊字符
+    safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in word.lower())
+    cache_path = os.path.join(TTS_CACHE_DIR, f"{safe_name}.mp3")
+
+    # 命中缓存直接返回
+    if os.path.exists(cache_path):
+        with open(cache_path, "rb") as f:
+            data = f.read()
+        return Response(content=data, media_type="audio/mpeg")
+
+    # 拉取有道 TTS(type=2 美式发音)
+    url = "https://dict.youdao.com/dictvoice?audio=" + urllib.parse.quote(word) + "&type=2"
+    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
+    try:
+        with urllib.request.urlopen(req, timeout=10) as resp:
+            data = resp.read()
+    except urllib.error.URLError as e:
+        raise HTTPException(status_code=502, detail=f"TTS upstream error: {e}")
+
+    if not data:
+        raise HTTPException(status_code=502, detail="TTS empty response")
+
+    # 写入缓存
+    try:
+        with open(cache_path, "wb") as f:
+            f.write(data)
+    except Exception as e:
+        print(f"TTS cache write failed: {e}")
+
+    return Response(content=data, media_type="audio/mpeg")
+
 @app.post("/api/progress/update")
 async def update_progress(data: ProgressUpdate):
     try:
index 13545a78e0983944668ef78425f64ac8fa6cd151..75c69adbb4c473466674e71d4a20b57f7829c6d2 100644 (file)
@@ -18,24 +18,18 @@ function StudyMode() {
     const transitioningRef = useRef(false);
     const currentWord = words[currentIndex];
 
-    // 朗读逻辑:语速 0.6,间隔 500ms
+    // 朗读逻辑:通过后端 TTS 代理(有道美式发音)播放两遍,间隔 500ms
     const speak = useCallback((text) => {
-        window.speechSynthesis.cancel();
-        
-        const utterance1 = new SpeechSynthesisUtterance(text);
-        utterance1.lang = 'en-US';
-        utterance1.rate = 0.6; 
-
-        utterance1.onend = () => {
+        if (!text) return;
+        const url = `${API_BASE}/api/tts/${encodeURIComponent(text)}`;
+        const audio1 = new Audio(url);
+        audio1.play().catch(err => console.error("TTS play failed:", err));
+        audio1.onended = () => {
             setTimeout(() => {
-                const utterance2 = new SpeechSynthesisUtterance(text);
-                utterance2.lang = 'en-US';
-                utterance2.rate = 0.6;
-                window.speechSynthesis.speak(utterance2);
+                const audio2 = new Audio(url);
+                audio2.play().catch(err => console.error("TTS play failed:", err));
             }, 500);
         };
-        
-        window.speechSynthesis.speak(utterance1);
     }, []);
 
     // 获取单词数据
@@ -91,10 +85,8 @@ function StudyMode() {
             setPhase('correction');
             setCorrectionInput("");
             // 错误时读一遍正确发音提醒
-            const utterance = new SpeechSynthesisUtterance(currentWord.word);
-            utterance.lang = 'en-US';
-            utterance.rate = 0.6;
-            window.speechSynthesis.speak(utterance);
+            const audio = new Audio(`${API_BASE}/api/tts/${encodeURIComponent(currentWord.word)}`);
+            audio.play().catch(err => console.error("TTS play failed:", err));
         }
     };
 
index 5d5c52ffe2ff53e907d983e456390a52c57789eb..7c638f97fef5f0c760de540da98624bff539de39 100644 (file)
@@ -53,13 +53,11 @@ function TestMode() {
         }
     }, [phase, currentIndex]);
 
-    // 朗读
+    // 朗读:通过后端 TTS 代理(有道美式发音)播放一遍
     const speak = useCallback((text) => {
-        window.speechSynthesis.cancel();
-        const utterance = new SpeechSynthesisUtterance(text);
-        utterance.lang = 'en-US';
-        utterance.rate = 0.6;
-        window.speechSynthesis.speak(utterance);
+        if (!text) return;
+        const audio = new Audio(`${API_BASE}/api/tts/${encodeURIComponent(text)}`);
+        audio.play().catch(err => console.error("TTS play failed:", err));
     }, []);
 
     // 提交答案