From: root Date: Fri, 26 Jun 2026 14:34:35 +0000 (+0800) Subject: TTS: 替换浏览器内置发音为有道美式发音 X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=c734d32782dde718bd75ffde1ea4023cf645dcaf;p=words.git TTS: 替换浏览器内置发音为有道美式发音 - 后端新增 /api/tts/{word} 端点,代理有道 dictvoice type=2 美式发音 - 音频文件缓存到 backend/db/tts_cache/,二次请求直接读缓存 - 前端 StudyMode/TestMode 改用 new Audio() 播放后端 TTS - 学习模式保留读两遍+500ms间隔,测试模式读一遍 - .gitignore 忽略 TTS 缓存目录 --- diff --git a/.gitignore b/.gitignore index 4ebf247..3bd2a53 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/backend/main.py b/backend/main.py index e6d4753..ec6850f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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: diff --git a/frontend/src/components/StudyMode.js b/frontend/src/components/StudyMode.js index 13545a7..75c69ad 100644 --- a/frontend/src/components/StudyMode.js +++ b/frontend/src/components/StudyMode.js @@ -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)); } }; diff --git a/frontend/src/components/TestMode.js b/frontend/src/components/TestMode.js index 5d5c52f..7c638f9 100644 --- a/frontend/src/components/TestMode.js +++ b/frontend/src/components/TestMode.js @@ -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)); }, []); // 提交答案