2026-06-18 08:27:37 +02:00
|
|
|
import asyncio
|
2026-06-17 05:19:07 +02:00
|
|
|
import time
|
2026-06-18 08:27:37 +02:00
|
|
|
from contextlib import asynccontextmanager
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
from pathlib import Path
|
2026-06-17 05:19:07 +02:00
|
|
|
|
|
|
|
|
from fastapi import FastAPI, Request
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
from fastapi.staticfiles import StaticFiles
|
2026-06-25 04:45:11 +02:00
|
|
|
from starlette.responses import RedirectResponse, JSONResponse, HTMLResponse
|
2026-06-17 05:19:07 +02:00
|
|
|
|
2026-06-27 07:02:02 +02:00
|
|
|
from app import __version__
|
feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.
TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
loudness-normalisiert.
LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).
Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.
Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
|
|
|
from app.config import settings
|
2026-06-25 04:24:52 +02:00
|
|
|
from urllib.parse import urlencode
|
|
|
|
|
|
|
|
|
|
from app.auth import authenticate, _bearer_token, CAPABILITY_COOKIE
|
|
|
|
|
from app.dependencies import get_store
|
2026-06-18 08:27:37 +02:00
|
|
|
from app.core.warmup import warmup_local_models
|
2026-06-17 05:19:07 +02:00
|
|
|
from app.metrics import metrics
|
2026-06-17 01:48:56 +02:00
|
|
|
from app.api.health import router as health_router
|
|
|
|
|
from app.api.chat import router as chat_router
|
|
|
|
|
from app.api.transcribe import router as transcribe_router
|
|
|
|
|
from app.api.speak import router as speak_router
|
|
|
|
|
from app.api.devices import router as devices_router
|
|
|
|
|
from app.api.sessions import router as sessions_router
|
|
|
|
|
from app.api.config import router as config_router
|
2026-06-17 02:14:25 +02:00
|
|
|
from app.api.admin import router as admin_router
|
|
|
|
|
from app.api.me import router as me_router
|
2026-06-17 05:19:07 +02:00
|
|
|
from app.api.metrics import router as metrics_router
|
2026-06-17 04:26:55 +02:00
|
|
|
from app.api.ws import router as ws_router
|
2026-06-17 01:48:56 +02:00
|
|
|
|
2026-06-25 19:03:54 +02:00
|
|
|
# --- Logging: App-eigene Logger sichtbar machen ---------------------------
|
|
|
|
|
# uvicorn konfiguriert nur seine eigenen Logger; "app.*"-INFO-Logs (z. B. die
|
|
|
|
|
# Notruf-Diagnose) würden sonst durch den Default-Level (WARNING) verschluckt.
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
_app_logger = logging.getLogger("app")
|
|
|
|
|
_app_logger.setLevel(getattr(logging, os.getenv("LOG_LEVEL", "INFO").upper(), logging.INFO))
|
|
|
|
|
if not _app_logger.handlers:
|
|
|
|
|
_log_handler = logging.StreamHandler()
|
|
|
|
|
_log_handler.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(message)s"))
|
|
|
|
|
_app_logger.addHandler(_log_handler)
|
|
|
|
|
_app_logger.propagate = False
|
|
|
|
|
|
|
|
|
|
|
2026-06-18 08:27:37 +02:00
|
|
|
@asynccontextmanager
|
|
|
|
|
async def _lifespan(app: FastAPI):
|
|
|
|
|
# Lokale Modelle im Hintergrund vorladen -> Server ist sofort verfuegbar,
|
|
|
|
|
# der erste Nutzer zahlt nicht den Kaltstart.
|
|
|
|
|
task = asyncio.create_task(warmup_local_models())
|
|
|
|
|
yield
|
|
|
|
|
if not task.done():
|
|
|
|
|
task.cancel()
|
|
|
|
|
|
|
|
|
|
|
2026-06-27 07:02:02 +02:00
|
|
|
app = FastAPI(title="Alexis", version=__version__, lifespan=_lifespan)
|
2026-06-17 05:19:07 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.middleware("http")
|
|
|
|
|
async def record_metrics(request: Request, call_next):
|
|
|
|
|
start = time.perf_counter()
|
|
|
|
|
response = await call_next(request)
|
|
|
|
|
duration = time.perf_counter() - start
|
|
|
|
|
# Route-Template (z. B. /api/sessions/{session_id}/route) statt konkreter URL,
|
|
|
|
|
# um die Label-Kardinalitaet niedrig zu halten.
|
|
|
|
|
route = request.scope.get("route")
|
|
|
|
|
path = getattr(route, "path", request.url.path)
|
|
|
|
|
labels = {"method": request.method, "path": path}
|
|
|
|
|
metrics.inc("http_requests_total", {**labels, "status": response.status_code})
|
|
|
|
|
metrics.observe("http_request_duration_seconds", duration, labels)
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.
TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
loudness-normalisiert.
LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).
Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.
Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
|
|
|
# Pfade mit eigener Auth, Health-Checks und Favicons: nicht gaten.
|
2026-06-25 04:45:11 +02:00
|
|
|
_NO_ACCESS_PAGE = """<!doctype html><html lang="de"><head><meta charset="utf-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width,initial-scale=1"><title>Zugang</title>
|
|
|
|
|
<style>body{font-family:system-ui,-apple-system,sans-serif;background:#f8fafc;color:#1e293b;
|
|
|
|
|
display:grid;place-items:center;min-height:100vh;margin:0;padding:1.5rem}
|
|
|
|
|
.box{max-width:30rem;text-align:center}h1{font-size:1.3rem;margin:0 0 .75rem}
|
2026-06-26 19:04:37 +02:00
|
|
|
p{color:#475569;line-height:1.6;margin:.5rem 0}.emoji{font-size:2.5rem}
|
|
|
|
|
.btn{display:inline-block;margin-top:1.25rem;background:#2563eb;color:#fff;text-decoration:none;
|
|
|
|
|
font-weight:600;font-size:1.05rem;padding:.8rem 1.4rem;border-radius:.75rem}</style></head>
|
2026-06-25 04:45:11 +02:00
|
|
|
<body><div class="box"><div class="emoji">🔒</div>
|
|
|
|
|
<h1>Persönlicher Zugang nötig</h1>
|
2026-06-26 19:04:37 +02:00
|
|
|
<p>Senioren: bitte die App über den persönlichen Zugangslink öffnen.</p>
|
|
|
|
|
<p>Betreuer und Admins können sich mit Benutzername und Passwort anmelden:</p>
|
|
|
|
|
<a class="btn" href="/login">Mit Benutzername & Passwort anmelden</a>
|
2026-06-25 04:45:11 +02:00
|
|
|
</div></body></html>"""
|
|
|
|
|
|
|
|
|
|
|
feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.
TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
loudness-normalisiert.
LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).
Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.
Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
|
|
|
_PUBLIC_PREFIXES = ("/api", "/ws", "/health", "/favicon", "/apple-touch-icon")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.middleware("http")
|
|
|
|
|
async def gate_web_ui(request: Request, call_next):
|
|
|
|
|
"""Schuetzt die statische Web-UI: unauthentifizierte Seitenaufrufe -> SSO-Login (sonst 401).
|
|
|
|
|
|
|
|
|
|
Defense-in-Depth zusaetzlich zu SSOwat: Selbst wenn der Reverse-Proxy einen
|
|
|
|
|
unauthentifizierten Request durchliesse (oder jemand den Port direkt trifft),
|
|
|
|
|
bekommt er die Seite nicht ausgeliefert. API/WS haben ihre eigene Auth (require_user
|
|
|
|
|
-> 401/JSON), Health-Checks bleiben offen. Bei abgeschalteter Auth (LAN-Dev) liefert
|
|
|
|
|
authenticate() einen anonymen Nutzer -> die Seite wird wie bisher ausgeliefert.
|
|
|
|
|
"""
|
|
|
|
|
path = request.url.path
|
|
|
|
|
if not path.startswith(_PUBLIC_PREFIXES):
|
2026-06-25 04:24:52 +02:00
|
|
|
# Ein-Klick-Link: ?k=<token> -> Token im Cookie speichern, aus der URL entfernen.
|
|
|
|
|
k = request.query_params.get("k")
|
|
|
|
|
if k and get_store().get_user_by_token(k):
|
|
|
|
|
params = {kk: vv for kk, vv in request.query_params.items() if kk != "k"}
|
|
|
|
|
clean = path + ("?" + urlencode(params) if params else "")
|
|
|
|
|
resp = RedirectResponse(clean, status_code=303)
|
|
|
|
|
resp.set_cookie(CAPABILITY_COOKIE, k, httponly=True, secure=True,
|
|
|
|
|
samesite="lax", max_age=31536000)
|
|
|
|
|
return resp
|
feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.
TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
loudness-normalisiert.
LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).
Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.
Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
|
|
|
client_host = request.client.host if request.client else ""
|
2026-06-25 04:24:52 +02:00
|
|
|
token = (_bearer_token(request.headers.get("authorization"))
|
|
|
|
|
or k or request.cookies.get(CAPABILITY_COOKIE))
|
feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.
TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
loudness-normalisiert.
LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).
Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.
Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
|
|
|
if authenticate(request.headers, client_host, token) is None:
|
2026-06-25 04:45:11 +02:00
|
|
|
# Kein gültiger Zugang -> freundliche Hinweisseite (KEIN SSO-Redirect, der
|
|
|
|
|
# nach dem nginx-Cutover in eine Schleife liefe). Senioren nutzen ihren Link.
|
|
|
|
|
return HTMLResponse(_NO_ACCESS_PAGE, status_code=401)
|
feat: Geräte-TTS, native Stimmen, Favicon, Auth-Gate, UI-Fixes
Web-UI / TTS:
- Geräte-TTS ("📱 Gerät"): Antwort wird on-device vorgelesen (Web Speech
API), Server sendet nur Text (text_only) -> spart Bandbreite/Kosten.
Mobil-Default, geräte-lokale Speicherung, iOS-Autoplay-Freischaltung.
- Vorlese-Symbol (🔊) je Bubble: Hybrid-Replay (Assistent-PCM gecacht,
Eingabe via /api/speak); SVG-Icon mit kontrastreicher Farbe.
- Kombiniertes Sprachmenü (Flex + feste Sprachen) statt separatem Modus-Menü.
- "Neues Gespräch"-Button (frische Session gegen Sprach-Trägheit).
- Dark-Mode: lesbare <option>-Popups (Kontrast-Fix).
- Favicon (SVG + PNG-Fallbacks) aus mund.png.
TTS-Backend:
- Sprache wird an alle TTS-Provider durchgereicht; Piper-Stimme folgt der
Sprache; Chatterbox mehrsprachig + cross-lingual.
- Native Referenz-Stimmen je Sprache (config/voices/<lang>.wav, FLEURS CC-BY),
loudness-normalisiert.
LLM-Sprache:
- Antwort folgt zuverlässig der gewählten Sprache (verstärkte Anweisung +
Erinnerung an der letzten Nutzer-Nachricht gegen History-Trägheit).
Admin / Auth:
- Wörterbuch: alle 8 Sprachen, Zeilen editierbar, alphabetische Sortierung.
- Web-UI hinter Auth-Gate (Redirect auf SSO_LOGIN_URL / 401); Favicons offen.
- Log-Tab: Hinweis, wenn der systemd-Dienst nicht aktiv ist.
- Einstellungen: Hinweis "pro Nutzer überschreibbar" bei Sprache/Modus/Qualität.
Doku (BEDIENUNGSANLEITUNG.md): Geräte-TTS §6.5.0, Fix/Flex §6.6, native
Stimmen §6.5.3, llama.cpp<->Ollama-Wechsel §4.7, Auth/SSO §7.4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 13:12:04 +02:00
|
|
|
return await call_next(request)
|
|
|
|
|
|
|
|
|
|
|
2026-06-17 01:48:56 +02:00
|
|
|
app.include_router(health_router)
|
|
|
|
|
app.include_router(chat_router, prefix="/api")
|
|
|
|
|
app.include_router(transcribe_router, prefix="/api")
|
|
|
|
|
app.include_router(speak_router, prefix="/api")
|
|
|
|
|
app.include_router(devices_router, prefix="/api")
|
|
|
|
|
app.include_router(sessions_router, prefix="/api")
|
|
|
|
|
app.include_router(config_router, prefix="/api")
|
2026-06-17 02:14:25 +02:00
|
|
|
app.include_router(admin_router, prefix="/api")
|
|
|
|
|
app.include_router(me_router, prefix="/api")
|
2026-06-17 05:19:07 +02:00
|
|
|
app.include_router(metrics_router, prefix="/api")
|
2026-06-17 04:26:55 +02:00
|
|
|
app.include_router(ws_router)
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
|
|
|
|
|
# Statische Web-UI (same-origin -> kein CORS). Muss NACH allen API-/WS-Routen
|
|
|
|
|
# gemountet werden, damit "/" nur die uebrigen Pfade abfaengt.
|
2026-06-18 07:49:55 +02:00
|
|
|
class _NoCacheStaticFiles(StaticFiles):
|
|
|
|
|
"""Liefert die UI ohne Caching aus -> Aenderungen sind sofort sichtbar (kein Safari-Cache)."""
|
|
|
|
|
|
|
|
|
|
def file_response(self, *args, **kwargs):
|
|
|
|
|
response = super().file_response(*args, **kwargs)
|
|
|
|
|
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
feat(web): Remote-Web-UI mit Mikrofon + Forward-Auth (YunoHost-SSO)
- Minimale Web-UI (app/web/, vanilla, same-origin -> kein CORS): Text-Prompt +
Mikrofon-Button (Aufnahme im Browser -> /ws/voice -> Antwort wird vorgelesen),
Token-Streaming, PCM-Wiedergabe, Identitaet/Logout/Admin im Menue
- Forward-/Trusted-Header-Auth (app/auth.py): Identitaet aus SSO-Header, nur von
TRUSTED_PROXY_IPS akzeptiert; sonst Token/Anonymous-Fallback. Auto-Provisioning
via store.get_or_create_user_by_external_id (+ external_id-Spalte/Migration)
- /api/me um is_admin + sso_logout_url erweitert; GET /api/admin/users (Liste) und
GET /api/admin/request-headers (SSO-Header-Discovery), Admin-gated
- StaticFiles-Mount; Config: TRUSTED_AUTH_HEADER/_PROXY_IPS, ADMIN_USERS, SSO_LOGOUT_URL
- WS-Auth liest Identitaet aus dem Handshake-Header
- Deploy: nginx-Vorlage (WS-Upgrade!) + deploy/README.md (HTTPS/SSO/Firewall/Discovery)
- Tests: Forward-Auth (Provisioning, Admin-Flag, Proxy-IP-Trust, 401/403, Static)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:37:16 +02:00
|
|
|
_WEB_DIR = Path(__file__).resolve().parent / "web"
|
|
|
|
|
if _WEB_DIR.is_dir():
|
2026-06-18 07:49:55 +02:00
|
|
|
app.mount("/", _NoCacheStaticFiles(directory=str(_WEB_DIR), html=True), name="web")
|