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

184 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 0014 — Tour-System mit ElevenLabs-Voice + Web-Speech-Fallback
| | |
|---|---|
| **Status** | accepted |
| **Datum** | 2026-05-09 |
| **Refs** | #185, Commits 1c74cb8 → bf6201e |
## 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:
```jinja
{% 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:
```html
<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).