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 30 s 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 " ) ,
" default_language_mode " : ( " Sprachmodus (Standard) " , " str " , " fix | flex — 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 " ) ,
2026-06-20 20:08:10 +02:00
" allow_cloud_stt " : ( " Geräte-STT via Cloud erlauben " , " bool " , " true = auch Cloud-Erkennung (z.B. Chrome-Desktop -> Audio zu Google); false = nur on-device " ) ,
2026-06-19 01:39:36 +02:00
}
_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 )