2026-06-19 01:39:36 +02:00
|
|
|
|
"""Laufzeit-Konfigurationsüberschreibungen aus der Datenbank.
|
|
|
|
|
|
|
|
|
|
|
|
Einzelne Settings-Felder können zur Laufzeit via Admin-UI geändert werden,
|
|
|
|
|
|
ohne den Server neu zu starten. Die Werte liegen in der Tabelle
|
|
|
|
|
|
`config_overrides` und werden mit 30s TTL gecacht.
|
|
|
|
|
|
|
|
|
|
|
|
Nur Felder aus RUNTIME_SETTABLE sind überschreibbar — alle anderen
|
|
|
|
|
|
kommen weiterhin aus .env / TOML / pydantic-settings.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from app.config import Settings, settings as _base
|
|
|
|
|
|
|
|
|
|
|
|
# (label, type_str, hint)
|
|
|
|
|
|
RUNTIME_SETTABLE: dict[str, tuple[str, str, str]] = {
|
|
|
|
|
|
"default_stt_provider": ("STT-Provider (Standard)", "str", "openrouter | faster-whisper"),
|
|
|
|
|
|
"default_llm_provider": ("LLM-Provider (Standard)", "str", "openrouter | local-openai-compatible"),
|
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
|
|
|
|
"default_tts_provider": ("TTS-Provider (Standard)", "str", "openrouter | piper | chatterbox — Standard, pro Nutzer überschreibbar"),
|
|
|
|
|
|
"default_language": ("Sprache (Standard)", "str", "de | en | … — Standard, pro Nutzer überschreibbar"),
|
2026-06-19 01:39:36 +02:00
|
|
|
|
"openrouter_llm_model": ("LLM-Modell (OpenRouter)", "str", "z.B. google/gemini-3.1-flash-lite"),
|
|
|
|
|
|
"openrouter_tts_model": ("TTS-Modell (OpenRouter)", "str", "z.B. google/gemini-3.1-flash-tts-preview"),
|
|
|
|
|
|
"openrouter_tts_voice": ("TTS-Stimme (OpenRouter)", "str", "z.B. Zephyr, Puck, Kore"),
|
|
|
|
|
|
"piper_voice": ("TTS-Stimme (piper)", "str", "z.B. de_DE-thorsten-high"),
|
|
|
|
|
|
"local_llm_system_prompt": ("Systemprompt (lokal)", "str", "Freier Text"),
|
2026-06-20 18:46:33 +02:00
|
|
|
|
"local_llm_temperature": ("Temperatur (lokal)", "float", "0.0–2.0 — wirkt sofort (kein Neustart)"),
|
|
|
|
|
|
"local_llm_top_p": ("Top-p (lokal)", "float", "0.0–1.0 — wirkt sofort (kein Neustart)"),
|
2026-06-19 01:39:36 +02:00
|
|
|
|
"local_llm_max_tokens": ("Max. Tokens (lokal)", "int", "0 = kein Limit"),
|
|
|
|
|
|
"tts_normalize_level": ("TTS-Normalisierung", "str", "auto | full | light | off"),
|
|
|
|
|
|
"audio_stream_default": ("Audio-Streaming Standard", "bool", "true | false"),
|
|
|
|
|
|
"memory_extraction_enabled": ("Erinnerungs-Extraktion", "bool", "true | false"),
|
|
|
|
|
|
"memory_extraction_every_n_turns": ("Extraktion alle N Turns", "int", "z.B. 3"),
|
|
|
|
|
|
"daily_request_limit": ("Tageskontingent (global)", "int", "0 = unbegrenzt"),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_TTL = 30.0
|
|
|
|
|
|
_cache: dict[str, str] = {}
|
|
|
|
|
|
_cache_time: float = 0.0
|
|
|
|
|
|
_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _coerce(key: str, raw: str) -> Any:
|
|
|
|
|
|
_, type_str, _ = RUNTIME_SETTABLE[key]
|
|
|
|
|
|
try:
|
|
|
|
|
|
if type_str == "bool":
|
|
|
|
|
|
return raw.strip().lower() in ("1", "true", "yes")
|
|
|
|
|
|
if type_str == "int":
|
|
|
|
|
|
return int(raw)
|
|
|
|
|
|
if type_str == "float":
|
|
|
|
|
|
return float(raw)
|
|
|
|
|
|
except (ValueError, AttributeError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
return raw
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_cache() -> dict[str, str]:
|
|
|
|
|
|
global _cache, _cache_time
|
|
|
|
|
|
now = time.monotonic()
|
|
|
|
|
|
with _lock:
|
|
|
|
|
|
if now - _cache_time < _TTL:
|
|
|
|
|
|
return _cache
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.dependencies import get_store
|
|
|
|
|
|
_cache = get_store().get_config_overrides()
|
|
|
|
|
|
_cache_time = now
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return _cache
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def invalidate_cache() -> None:
|
|
|
|
|
|
global _cache_time
|
|
|
|
|
|
with _lock:
|
|
|
|
|
|
_cache_time = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RuntimeSettings:
|
|
|
|
|
|
"""Wraps Settings; liest überschreibbare Felder aus der DB (30s TTL)."""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, base: Settings):
|
|
|
|
|
|
object.__setattr__(self, "_base", base)
|
|
|
|
|
|
|
|
|
|
|
|
def __getattr__(self, name: str) -> Any:
|
|
|
|
|
|
if name in RUNTIME_SETTABLE:
|
|
|
|
|
|
overrides = _get_cache()
|
|
|
|
|
|
if name in overrides:
|
|
|
|
|
|
return _coerce(name, overrides[name])
|
|
|
|
|
|
return getattr(object.__getattribute__(self, "_base"), name)
|
|
|
|
|
|
|
|
|
|
|
|
# Delegiere Pydantic-Metadaten ans Basis-Objekt.
|
|
|
|
|
|
@property
|
|
|
|
|
|
def model_fields(self):
|
|
|
|
|
|
return self._base.model_fields
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
runtime_settings = RuntimeSettings(_base)
|