gwoe-antragspruefer/docs/adr/0014-tour-system-mit-elevenlabs-voice.md
Dotty Dotter 9fc0619a20 docs(adr): 0014 — Tour-System mit ElevenLabs-Voice + Web-Speech-Fallback
Architektur-Dokumentation der heutigen Session:
- Engine-Wahl (eigene Spotlight-Logik statt intro.js/shepherd.js)
- Pro-Page-Stations via window.GWOE_TOUR_STEPS
- Audio-Pipeline: ElevenLabs/Domi mit Server-Cache, Fallback auf
  Web Speech API
- Auto-Play-Block-Workaround (persistentes <audio> + User-Gesture-Unlock)
- CSP-Erweiterung: media-src 'self' data: blob:
- Konsequenzen positiv/negativ und Folge-Iterationen.
2026-05-09 12:25:04 +02:00

6.9 KiB
Raw Blame History

0014 — Tour-System mit ElevenLabs-Voice + Web-Speech-Fallback

Status accepted
Datum 2026-05-09
Refs #185, Commits 1c74cb8bf6201e

Kontext

Aus Issue #185 ("Bürger:innen-Perspektive: Was überfordert? Profi-Infos hinter Klick verstecken") kam die User-Anforderung, den Profi-Modus (v3-Antrag-Detail mit GWÖ-Score, Matrix 5×5, Marker-Pillen) für Erst-Besucher:innen erklärbar zu machen — ohne den Profi-Modus selbst zu vereinfachen.

Anforderung wurde im Verlauf konkretisiert:

  1. Schaltfläche „Tour" auf jeder Seite außer Administration.
  2. „Du bist neu hier?"-Banner auf der Startseite, einmalig per localStorage dismissable; Topbar-Tour-Link bleibt permanent.
  3. Pro Menüpunkt eigene Stationen, die die Funktionalität dieser Page erklären (nicht eine generische Erklärung des Layouts).
  4. Sprachausgabe: weibliche, freundliche, warme, seriöse Stimme. Phase 1: Web Speech API als MVP. Phase 2: ElevenLabs (Domi).
  5. Ermächtigend formulieren, nicht vereinfachend — Bürger:innen sollen das Profi-Tool lesen können, nicht eine reduzierte Variante bekommen.

Optionen

Option A — Externe Tour-Library (intro.js, shepherd.js, driver.js)

Etabliertes Library-Ökosystem mit Spotlight-Effekt und Tour-Choreografie.

Vorteile: weniger eigener Code, vertraute UX-Patterns. Nachteile: zusätzliche JS-Dependency (~3080 KB gezippt), keine Audio-Integration, kein bürgernahes Wording out of the box, Styling anders als das übrige v2/v3-Design.

Option B — Eigene Spotlight-Engine, page-spezifische Stations

Eine schlanke Tour-Engine in app/templates/v3/components/tour.html, ~250 Zeilen JS + CSS. Stationen pro Page über window.GWOE_TOUR_STEPS gesetzt. Engine löst Selektoren zur Laufzeit auf und überspringt Stations, deren Element nicht im DOM ist (z.B. keine Plenum-Votes auf einer bestimmten Drucksache).

Vorteile: keine externe Dependency, volle Kontrolle über Wording, eigenes Design (pill-shaped Buttons, teal-Farbgebung), enge Integration in CSP/Auth-Logik. Nachteile: Engine-Code muss selbst gepflegt werden.

Option C — Server-seitig generierte Tour-Texte pro User-Profil

Backend bestimmt anhand des User-Profils (Erst-Visit, Eingeloggt, Bundesland-Filter), welche Stationen geladen werden. Mehr Personalisierung.

Vorteile: maximale Anpassung. Nachteile: über-engineered für den Erst-Aufschlag; macht jede Tour-Iteration zum Server-Deploy.

Entscheidung

Option B mit folgender Architektur:

Engine

app/templates/v3/components/tour.html enthält UI (Overlay, Spotlight, Bubble, Mute-Toggle), Engine-Logik und CSS. Wird global in v2/base.html per Jinja-{% include %} eingebunden, nur ausgeschlossen für Administration:

{% if v2_active_nav not in ['admin_stand', 'admin_queue', 'admin_abos',
                            'freischaltungen', 'queue', 'abos'] %}
{% include "v3/components/tour.html" ignore missing %}
{% endif %}

Stations pro Page

Jedes Page-Template definiert seine Stationen am Anfang des body_scripts-Blocks:

<script>
window.GWOE_TOUR_STEPS = [
  { selector: '.v3-bewertung',
    title: 'Die Gemeinwohl-Note',
    text: '…' },
  { selector: '.v3-fraktionen',
    title: 'Programm-Treue pro Fraktion',
    text: '…' },
];
</script>

Selektoren mit Komma-separierter Fallback-Liste; Engine nimmt das erste Match und überspringt Stations ohne Match. Damit funktioniert die Tour auch, wenn ein Element auf manchen Drucksachen fehlt.

Auth-spezifische Stations via Jinja: z.B. die Startseite hat unterschiedliche „Navigation links"-Texte je nach is_authenticated, weil die Sidebar unterschiedliche Items zeigt.

Fallback

Pages ohne window.GWOE_TOUR_STEPS bekommen drei generische Stations (Logo+Konzept, Topbar, Sidebar) — damit der Topbar-Tour-Link überall einen Inhalt zeigt.

Audio: zwei Pfade

Phase 1 (Web Speech API): Browser-eingebaute TTS via speechSynthesis. Funktioniert ohne API-Key, klingt mechanisch.

Phase 2 (ElevenLabs/Domi):

  • Backend-Endpoint GET /api/tour/voice?text=…, ENV-konfiguriert via ELEVENLABS_API_KEY und ELEVENLABS_VOICE_ID (Default: Domi AZnzlk1XvdvUeBnXmlld).
  • Server-seitiges Caching: data/tour_audio/<sha256(text|voice|model)>.mp3. Folgeabrufe gehen aus dem Datei-Cache, kein API-Quota-Verbrauch.
  • Frontend: speak() versucht erst den Endpoint. Bei 503 (kein API-Key) oder Fehler Fallback auf Web Speech.
  • Auto-Play-Block-Workaround: persistentes <audio>-Element wird einmal beim Tour-Start im Click-Handler (synchroner User-Gesture-Frame) mit einer 1×1-silent-WAV als data-URL entsperrt. Spätere src-Updates spielen dann ohne NotAllowedError.

Content-Security-Policy

media-src 'self' data: blob:; ergänzt. data: für den Unlock-WAV, blob: für die ElevenLabs-MP3-Blob-URLs. Ohne diesen Eintrag blockt der Browser jeden <audio>.play()-Versuch.

Konsequenzen

Positiv

  • Tour-Engine ist auf jeder Page verfügbar (ausgenommen Administration), ohne dass jede Page selbst die Engine einbindet.
  • Stationen pro Page sind deklarativ und page-lokal — pflegbar ohne Backend-Deploy bei reinen Text-Änderungen.
  • Audio-Pipeline ist Cache-basiert und API-Quota-schonend (1 ElevenLabs- Call pro einzigartigem Text-String über alle Sessions).
  • Web-Speech-Fallback macht den Code robust, wenn ElevenLabs ausfällt oder der Key entfernt wird.
  • Mute-Toggle, ESC-Schließen und Pfeiltasten-Navigation sind Standard ohne externe Library.

Negativ

  • Engine-Code wächst mit Features (~250 LOC heute). Bei zukünftigen Tour-Erweiterungen muss eigene Pflege geleistet werden, statt sich auf eine Library zu stützen.
  • ElevenLabs-Voices haben ein Free-Tier-Limit (10 k Zeichen/Monat). Bei breiter Nutzung wäre ein Server-seitiges Rate-Limit zusätzlich zum 30/min-Limit am Endpoint sinnvoll — heute reicht der Cache.
  • CSP-Erweiterung um data: und blob: für media-src erweitert die Angriffsfläche minimal (Audio-Embed via XSS könnte missbraucht werden). Risiko-Bewertung: niedrig, weil unsere Templates keine user-generated Audio-Sources einbetten.

Folgen für andere ADRs

  • ADR 0008 (DDD-Lightweight): Tour-System ist UI-only, kein Domain-Konzept; berührt die Schichten-Architektur nicht.
  • ADR 0012 (Debug-Auth-Token-Bypass): Tour-Tests in Playwright nutzen den Bypass für headless E2E.
  • Methodik-Page (/methodik): Tour ist die bürgernahe Variante der Methodik-Erklärung, beide bestehen nebeneinander — Methodik ausführlich, Tour als 3-5-Stationen-Quickstart.

Folge-Iterationen (out of scope dieser ADR)

  • Verfeinerung der Tour-Texte pro Page (User-Feedback bei konkreten Stations).
  • Weitere Voice-Settings (stability, similarity, speed) ggf. als Cookie/UI-Toggle.
  • Auf Antrag-Detail eigene Stations je Konfidenz oder vorhandenen Daten (z.B. zusätzliche Station bei vorhandenen Plenum-Votes).