feat(tts): multi-voice switcher for EN & RU, Irina default voice, punctuation pause normalization and speed calibration

This commit is contained in:
Antigravity committed 2026-10-07 12:07:01 +03:00
1 parent 7893b353aa
commit 31e105b2c2
11 files changed
+486 -61

No files matched your search

+9 -11
View File
@@ -66,6 +66,7 @@ from app.tts import (
prewarm_card_audio,
AUDIO_CACHE_DIR,
is_piper_available,
get_available_voices,
)
from app.listening import (
get_listening_exercise,
@@ -523,16 +524,17 @@ async def api_tts(
text: str = Query(..., min_length=1, max_length=1000),
lang: str = Query("en"),
speed: float = Query(1.0, ge=0.5, le=2.0),
voice: Optional[str] = Query(None),
user: dict = Depends(require_user),
):
"""
Synthesizes speech on-demand using local Piper TTS (en_US-lessac / ru_RU-dmitri).
Instantly returns cached audio if previously synthesized.
Synthesizes speech on-demand using local Piper TTS.
Instantly returns cached audio if previously synthesized for this voice, speed, and text.
"""
clean_text = text.strip()
if not clean_text:
raise HTTPException(status_code=400, detail="Text cannot be empty")
audio_path = await synthesize_speech(clean_text, lang=lang, speed=speed)
audio_path = await synthesize_speech(clean_text, lang=lang, speed=speed, voice=voice)
if not audio_path or not audio_path.is_file():
raise HTTPException(status_code=500, detail="TTS synthesis failed or unavailable")
return FileResponse(
@@ -544,13 +546,8 @@ async def api_tts(
@app.get("/api/tts/status")
async def api_tts_status(user: dict = Depends(require_user)):
"""Returns status of local Piper TTS and available voice models."""
return {
"available": is_piper_available(),
"engine": "piper-vits",
"voices": ["en_US-lessac", "ru_RU-dmitri"],
"cache_dir": str(AUDIO_CACHE_DIR),
}
"""Returns status of local Piper TTS and all available voice models."""
return get_available_voices()
# ---------------------------------------------------------------------------
@@ -561,6 +558,7 @@ async def api_tts_status(user: dict = Depends(require_user)):
async def api_listening_exercise(
topic: Optional[str] = Query("all"),
level: Optional[str] = Query("B1"),
voice: Optional[str] = Query(None),
user: dict = Depends(require_user),
):
"""
@@ -581,7 +579,7 @@ async def api_listening_exercise(
).fetchall()
user_cards = [dict(r) for r in rows]
ex = await get_listening_exercise(topic=topic, level=level, user_cards=user_cards)
ex = await get_listening_exercise(topic=topic, level=level, user_cards=user_cards, voice=voice)
# Strip keywords and ideal_answer from client response to prevent inspection
safe_ex = {k: v for k, v in ex.items() if k not in ("keywords", "ideal_answer")}
return safe_ex