backend/a/log.txt
backend/a/error_log.csv
*.db
+
+# TTS audio cache (runtime-generated)
+backend/db/tts_cache/
import sqlite3
import os
import datetime
+import urllib.request
+import urllib.parse
+import urllib.error
from pydantic import BaseModel
from typing import List, Optional
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:
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);
}, []);
// 获取单词数据
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));
}
};
}
}, [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));
}, []);
// 提交答案