Switch AudioButton to Piper TTS (Thorsten/Lessac voices)

AudioButton now tries Piper TTS via /api/vocabulary/tts endpoint
first, falls back to Browser Web Speech API if unavailable.

Backend: New GET /api/vocabulary/tts?text=...&lang=de endpoint.
audio_service.py: Fixed presigned URL flow for MinIO download.

This gives the same high-quality voice as the Investor Agent
in the pitch deck (Thorsten DE / Lessac EN, MIT license).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-26 23:17:39 +02:00
parent 3cdab5a967
commit 0f0bbc3dc0
3 changed files with 82 additions and 40 deletions

View File

@@ -74,16 +74,24 @@ async def synthesize_word(
return None
data = resp.json()
audio_url = data.get("audio_url") or data.get("presigned_url")
bucket = data.get("bucket")
object_key = data.get("object_key")
if audio_url:
# Download the audio file
audio_resp = await client.get(audio_url)
if audio_resp.status_code == 200:
with open(cached, "wb") as f:
f.write(audio_resp.content)
logger.info(f"TTS cached: '{text}' ({language}) → {cached}")
return cached
if bucket and object_key:
# Get presigned URL to download the audio
url_resp = await client.post(
f"{TTS_SERVICE_URL}/presigned-url",
json={"bucket": bucket, "object_key": object_key, "expires": 300},
)
if url_resp.status_code == 200:
audio_url = url_resp.json().get("url")
if audio_url:
audio_resp = await client.get(audio_url)
if audio_resp.status_code == 200:
with open(cached, "wb") as f:
f.write(audio_resp.content)
logger.info(f"TTS cached: '{text}' ({language}) → {cached}")
return cached
except Exception as e:
logger.warning(f"TTS service unavailable: {e}")

View File

@@ -161,6 +161,22 @@ async def api_get_syllable_audio(word_id: str, lang: str = "en"):
return FastAPIResponse(content=audio_bytes, media_type="audio/mpeg")
@router.get("/tts")
async def api_tts(text: str = Query("", min_length=1), lang: str = Query("de")):
"""Text-to-Speech endpoint. Returns MP3 audio for any text.
Uses Piper TTS (Thorsten DE / Lessac EN). Cached by text+lang.
"""
from fastapi.responses import Response as FastAPIResponse
from services.audio import get_or_generate_audio
audio_bytes = await get_or_generate_audio(text, language=lang)
if not audio_bytes:
raise HTTPException(status_code=503, detail="TTS Service nicht verfuegbar")
return FastAPIResponse(content=audio_bytes, media_type="audio/mpeg")
# ---------------------------------------------------------------------------
# Learning Unit Creation from Word Selection
# ---------------------------------------------------------------------------