From 6ec05d2b86f82e602640b2b7199cea23daf15816 Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Sat, 9 May 2026 03:17:06 +0200 Subject: [PATCH] =?UTF-8?q?feat(tour):=20ElevenLabs-Voice=20f=C3=BCr=20die?= =?UTF-8?q?=20Tour=20(#185=20Phase=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audio-Backend: - ``app/tour_audio.py`` ruft ElevenLabs-TTS mit voice_id=Domi (AZnzlk1XvdvUeBnXmlld) und model=eleven_multilingual_v2. ENV-konfiguriert via ``ELEVENLABS_API_KEY``, ``ELEVENLABS_VOICE_ID``, ``ELEVENLABS_MODEL_ID``. - Voice-Settings: stability 0.55, similarity_boost 0.7 (warm, klar, natürlich). - Caching: SHA-256(text|voice|model) → ``data/tour_audio/.mp3``. Folgeabrufe gehen aus dem Datei-Cache, kein API-Quota-Verbrauch. Endpoint: ``GET /api/tour/voice?text=...`` rate-limited 30/min, liefert audio/mpeg mit Cache-Control 30 Tage. Bei fehlendem API-Key 503 — Frontend fällt dann auf ``speechSynthesis`` zurück (Browser-eingebaute Stimme). Frontend (tour.html): - ``speak()`` versucht erst Server-Audio (ElevenLabs), bei 503/Fehler Fallback auf Web Speech API. - Session-Cache via Blob-URL: Vor/Zurück-Navigation in der Tour zieht nicht jedes Mal eine neue Network-Roundtrip. - ``stopSpeak()`` stoppt beide Audio-Pfade sauber. Konfiguration für dev: ``ELEVENLABS_API_KEY`` und (optional) ``ELEVENLABS_VOICE_ID`` in ``/opt/gwoe-antragspruefer-dev/.env`` setzen, dann Container restart. --- app/main.py | 30 +++++++ app/templates/v3/components/tour.html | 51 ++++++++++- app/tour_audio.py | 120 ++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 app/tour_audio.py diff --git a/app/main.py b/app/main.py index 947cc3f..3ceeac3 100644 --- a/app/main.py +++ b/app/main.py @@ -2129,6 +2129,36 @@ async def quellen_search( }) +@app.get("/api/tour/voice") +@limiter.limit("30/minute") +async def tour_voice(request: Request, text: str = Query(..., min_length=2, max_length=2000)): + """Generiert (oder liefert aus Cache) eine MP3 für Tour-Erklär-Texte (#185). + + Nutzt ElevenLabs-TTS, wenn ENV ``ELEVENLABS_API_KEY`` gesetzt ist — + sonst 503, damit das Frontend auf ``speechSynthesis`` (browser- + eingebaute Stimme) zurückfällt. + + Caching: pro (text, voice_id, model_id) wird einmal generiert und in + ``data/tour_audio/.mp3`` gespeichert. Folgeabrufe gehen aus + dem Cache und kosten kein API-Quota. + """ + from .tour_audio import get_or_generate, is_available + if not is_available(): + raise HTTPException( + status_code=503, + detail="ElevenLabs nicht konfiguriert (ELEVENLABS_API_KEY fehlt)", + ) + audio = await get_or_generate(text) + if audio is None: + raise HTTPException(status_code=502, detail="TTS-Generierung fehlgeschlagen") + from fastapi.responses import Response + return Response( + content=audio, + media_type="audio/mpeg", + headers={"Cache-Control": "public, max-age=2592000"}, # 30 Tage Browser-Cache + ) + + @app.get("/api/wahlprogramm-cite") async def wahlprogramm_cite( request: Request, diff --git a/app/templates/v3/components/tour.html b/app/templates/v3/components/tour.html index 6bd67de..f53b67a 100644 --- a/app/templates/v3/components/tour.html +++ b/app/templates/v3/components/tour.html @@ -152,11 +152,14 @@ let _resolvedSteps = []; // STEPS gefiltert auf vorhandene Elemente let _tourMuted = false; let _tourUtter = null; + let _tourAudio = null; //