2026-06-17 01:48:56 +02:00
|
|
|
|
import os
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
import tomllib # Python >= 3.11 (stdlib)
|
|
|
|
|
|
except ModuleNotFoundError: # pragma: no cover - Fallback fuer aeltere Interpreter
|
|
|
|
|
|
import tomli as tomllib # type: ignore
|
|
|
|
|
|
|
|
|
|
|
|
from pydantic.fields import FieldInfo
|
|
|
|
|
|
from pydantic_settings import (
|
|
|
|
|
|
BaseSettings,
|
|
|
|
|
|
PydanticBaseSettingsSource,
|
|
|
|
|
|
SettingsConfigDict,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
ENV_FILE = BASE_DIR / ".env"
|
|
|
|
|
|
DEFAULT_CONFIG_FILE = BASE_DIR / "config" / "voice-assistant.toml"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _setting_lookup(key: str) -> str | None:
|
|
|
|
|
|
"""Liest einen Steuer-Schluessel: echte Umgebung zuerst, dann die .env-Datei.
|
|
|
|
|
|
|
|
|
|
|
|
Noetig fuer VA_PROFILE/VA_CONFIG_FILE, weil diese gebraucht werden, BEVOR
|
|
|
|
|
|
pydantic-settings die .env laedt - und .env-Werte sonst nicht in os.environ stehen.
|
|
|
|
|
|
"""
|
|
|
|
|
|
value = os.getenv(key)
|
|
|
|
|
|
if value is not None:
|
|
|
|
|
|
return value
|
|
|
|
|
|
try:
|
|
|
|
|
|
from dotenv import dotenv_values
|
|
|
|
|
|
except ModuleNotFoundError: # pragma: no cover
|
|
|
|
|
|
return None
|
|
|
|
|
|
if ENV_FILE.is_file():
|
|
|
|
|
|
return dotenv_values(ENV_FILE).get(key)
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _config_file_path() -> Path:
|
|
|
|
|
|
return Path(_setting_lookup("VA_CONFIG_FILE") or str(DEFAULT_CONFIG_FILE))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def active_profile() -> str | None:
|
|
|
|
|
|
"""Name des aktiven Profils (VA_PROFILE) aus Umgebung oder .env, falls gesetzt."""
|
|
|
|
|
|
profile = _setting_lookup("VA_PROFILE")
|
|
|
|
|
|
return profile.strip() or None if profile else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_profile_config() -> dict:
|
|
|
|
|
|
"""Liest die zentrale TOML-Config und merged [defaults] + [profiles.<VA_PROFILE>].
|
|
|
|
|
|
|
|
|
|
|
|
- Fehlt die Datei, gilt ein leeres dict (nur ENV/Defaults greifen) - kein Fehler,
|
|
|
|
|
|
damit reine Cloud-Deployments ohne Datei (nur ENV) funktionieren.
|
|
|
|
|
|
- Ein gesetztes, aber unbekanntes VA_PROFILE ist ein Konfigurationsfehler.
|
|
|
|
|
|
"""
|
|
|
|
|
|
path = _config_file_path()
|
|
|
|
|
|
if not path.is_file():
|
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
with path.open("rb") as handle:
|
|
|
|
|
|
data = tomllib.load(handle)
|
|
|
|
|
|
|
|
|
|
|
|
merged: dict = dict(data.get("defaults", {}))
|
|
|
|
|
|
|
|
|
|
|
|
profile = active_profile()
|
|
|
|
|
|
if profile:
|
|
|
|
|
|
profiles = data.get("profiles", {})
|
|
|
|
|
|
if profile not in profiles:
|
|
|
|
|
|
raise ValueError(
|
|
|
|
|
|
f"Unbekanntes VA_PROFILE {profile!r}. "
|
|
|
|
|
|
f"Verfuegbar: {sorted(profiles)}"
|
|
|
|
|
|
)
|
|
|
|
|
|
merged.update(profiles[profile])
|
|
|
|
|
|
|
|
|
|
|
|
return merged
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TomlProfileSource(PydanticBaseSettingsSource):
|
|
|
|
|
|
"""Settings-Quelle aus der zentralen TOML-Config (inkl. aktivem Profil).
|
|
|
|
|
|
|
|
|
|
|
|
Liegt in der Praezedenz unter ENV/.env, aber ueber den eingebauten Defaults.
|
|
|
|
|
|
Es werden nur Schluessel durchgereicht, die auch als Settings-Feld existieren.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, settings_cls):
|
|
|
|
|
|
super().__init__(settings_cls)
|
|
|
|
|
|
raw = load_profile_config()
|
|
|
|
|
|
known = set(settings_cls.model_fields)
|
|
|
|
|
|
self._values = {
|
|
|
|
|
|
key.lower(): value
|
|
|
|
|
|
for key, value in raw.items()
|
|
|
|
|
|
if key.lower() in known
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def get_field_value(self, field: FieldInfo, field_name: str):
|
|
|
|
|
|
if field_name in self._values:
|
|
|
|
|
|
return self._values[field_name], field_name, False
|
|
|
|
|
|
return None, field_name, False
|
|
|
|
|
|
|
|
|
|
|
|
def __call__(self) -> dict:
|
|
|
|
|
|
return dict(self._values)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
|
|
|
|
app_env: str = "dev"
|
|
|
|
|
|
host: str = "0.0.0.0"
|
|
|
|
|
|
port: int = 8080
|
|
|
|
|
|
log_level: str = "info"
|
|
|
|
|
|
openrouter_api_key: str = ""
|
|
|
|
|
|
openrouter_stt_model: str = "openai/whisper-large-v3"
|
|
|
|
|
|
openrouter_tts_model: str = "openai/gpt-4o-mini-tts"
|
|
|
|
|
|
openrouter_tts_voice: str = "alloy"
|
|
|
|
|
|
openrouter_llm_model: str = "openai/gpt-4.1-mini"
|
|
|
|
|
|
default_language: str = "de"
|
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_language_mode: str = "fix"
|
2026-06-17 01:48:56 +02:00
|
|
|
|
default_input_endpoint: str = "local-default"
|
|
|
|
|
|
default_output_endpoint: str = "local-default"
|
|
|
|
|
|
default_stt_provider: str = "openrouter"
|
|
|
|
|
|
default_llm_provider: str = "local-openai-compatible"
|
|
|
|
|
|
default_tts_provider: str = "openrouter"
|
2026-06-18 02:57:57 +02:00
|
|
|
|
# Lokaler llama.cpp-Server (OpenAI-kompatibel), siehe scripts/llm-server/.
|
|
|
|
|
|
local_llm_base_url: str = "http://127.0.0.1:8001/v1"
|
2026-06-17 01:48:56 +02:00
|
|
|
|
local_llm_api_key: str = "dummy"
|
2026-06-18 02:57:57 +02:00
|
|
|
|
local_llm_model: str = "va_llm" # = --alias des llama.cpp-Servers
|
|
|
|
|
|
# Sprach-Assistent: knappe, vorlesbare Antworten + Reasoning aus = deutlich schneller.
|
|
|
|
|
|
local_llm_system_prompt: str = (
|
|
|
|
|
|
"Du bist ein gesprochener Sprachassistent. Antworte kurz und natuerlich "
|
|
|
|
|
|
"(in der Regel 1-3 Saetze), in reinem Fliesstext ohne Markdown, ohne "
|
|
|
|
|
|
"Aufzaehlungen, ohne Emojis. Formuliere so, wie man es laut vorliest."
|
|
|
|
|
|
)
|
|
|
|
|
|
local_llm_disable_reasoning: bool = True # Qwen3 /no_think: spart die Denkphase
|
|
|
|
|
|
local_llm_max_tokens: int = 0 # 0 = serverseitiges Limit (-n)
|
|
|
|
|
|
local_llm_temperature: float = 0.3
|
2026-06-20 18:46:33 +02:00
|
|
|
|
local_llm_top_p: float = 0.9 # Nucleus-Sampling (0.0–1.0)
|
2026-06-17 11:28:20 +02:00
|
|
|
|
faster_whisper_model: str = "base" # tiny|base|small|medium|large-v3
|
|
|
|
|
|
faster_whisper_device: str = "auto" # auto|cpu|cuda
|
|
|
|
|
|
faster_whisper_compute_type: str = "default" # default|int8|float16|int8_float16
|
2026-06-17 22:11:20 +02:00
|
|
|
|
# --- Lokales TTS (piper) -------------------------------------------------
|
|
|
|
|
|
piper_bin: str = "piper" # Pfad/Name des piper-Binaries
|
|
|
|
|
|
piper_voices_dir: str = str(Path.home() / ".local" / "share" / "piper" / "voices")
|
|
|
|
|
|
piper_voice: str = "de_DE-thorsten-high" # Stimmmodell-Name (ohne .onnx) oder voller Pfad
|
|
|
|
|
|
tts_sample_rate: int = 24000 # Ziel-Sample-Rate (das Gateway erwartet 24000 Hz)
|
feat(tts): Chatterbox-Provider (hohe Qualitaet + Voice-Cloning) anbinden
Loest #7. Der chatterbox-Stub wird durch eine echte Anbindung an den lokalen
Chatterbox-HTTP-Dienst ersetzt (POST /speak -> /status pollen -> GET /audio, WAV).
no_playback=true -> der Dienst spielt nicht lokal ab, liefert nur Bytes. WAV->PCM
(24 kHz, Resampling bei Bedarf). Waehlbar via tts_provider=chatterbox; piper bleibt
der schnelle Default (chatterbox ist ~echtzeit-langsam, dafuer klonbare Stimme).
- config: CHATTERBOX_BASE_URL/_VOICE/_LANG/_SPEED/_TIMEOUT; Registry verdrahtet
- Tests: gemockter httpx (Synthese, WAV/Resample, Job-Fehler, Stimmenwahl)
- Doku: README, Architektur (#7), deploy/README (GPU-Pinning per UUID, no_playback)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 09:44:31 +02:00
|
|
|
|
# --- Chatterbox-TTS (hohe Qualitaet + Voice-Cloning, eigener HTTP-Dienst) -
|
2026-06-24 22:01:38 +02:00
|
|
|
|
# --- Cartesia TTS (Cloud, sehr niedrige Latenz) -------------------------
|
|
|
|
|
|
cartesia_api_key: str = ""
|
|
|
|
|
|
cartesia_voice_id: str = "" # UUID aus cartesia.ai/voices (Fallback)
|
|
|
|
|
|
cartesia_tts_model: str = "sonic-turbo"
|
|
|
|
|
|
cartesia_voices: str = "" # "lang:gender:uuid:name" je Zeile, # = Kommentar # sonic-2 | sonic-multilingual
|
feat(tts): Chatterbox-Provider (hohe Qualitaet + Voice-Cloning) anbinden
Loest #7. Der chatterbox-Stub wird durch eine echte Anbindung an den lokalen
Chatterbox-HTTP-Dienst ersetzt (POST /speak -> /status pollen -> GET /audio, WAV).
no_playback=true -> der Dienst spielt nicht lokal ab, liefert nur Bytes. WAV->PCM
(24 kHz, Resampling bei Bedarf). Waehlbar via tts_provider=chatterbox; piper bleibt
der schnelle Default (chatterbox ist ~echtzeit-langsam, dafuer klonbare Stimme).
- config: CHATTERBOX_BASE_URL/_VOICE/_LANG/_SPEED/_TIMEOUT; Registry verdrahtet
- Tests: gemockter httpx (Synthese, WAV/Resample, Job-Fehler, Stimmenwahl)
- Doku: README, Architektur (#7), deploy/README (GPU-Pinning per UUID, no_playback)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 09:44:31 +02:00
|
|
|
|
chatterbox_base_url: str = "http://127.0.0.1:9999"
|
|
|
|
|
|
chatterbox_voice: str = "" # Pfad zu Referenz-WAV (Voice-Cloning) oder leer
|
|
|
|
|
|
chatterbox_lang: str = "de"
|
|
|
|
|
|
chatterbox_speed: float = 1.0
|
|
|
|
|
|
chatterbox_timeout: int = 180
|
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
|
|
|
|
# Verzeichnis mit nativen Referenz-WAVs je Sprache (Konvention <lang>.wav, z. B. fr.wav).
|
|
|
|
|
|
# Greift cross-lingual: pro Sprache eine muttersprachliche Stimme. Leer -> nur chatterbox_voice.
|
|
|
|
|
|
chatterbox_voices_dir: str = str(BASE_DIR / "config" / "voices")
|
2026-06-17 02:14:25 +02:00
|
|
|
|
db_path: str = str(BASE_DIR / "data" / "voice-assistant.db")
|
|
|
|
|
|
admin_api_key: str = ""
|
|
|
|
|
|
auth_enabled: bool = True
|
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
|
|
|
|
# --- Forward-/Trusted-Header-Auth (Reverse-Proxy / YunoHost-SSO) ---------
|
|
|
|
|
|
# Ist trusted_auth_header gesetzt UND die Quell-IP in trusted_proxy_ips, wird die
|
|
|
|
|
|
# Identitaet aus diesem Header gelesen (SSO-User) und ein interner Nutzer
|
|
|
|
|
|
# automatisch angelegt. Sonst gilt die normale Token-/Anonymous-Auth.
|
|
|
|
|
|
trusted_auth_header: str = ""
|
2026-06-18 08:44:33 +02:00
|
|
|
|
# Alternativ zur Header-Variante: Identitaet aus einem (signierten) JWT-Cookie lesen.
|
|
|
|
|
|
# YunoHost reicht den Usernamen nicht als Header durch, sondern im Cookie
|
|
|
|
|
|
# "yunohost.portal" (JWT, Claim "user"). Nur von der Proxy-IP akzeptiert.
|
|
|
|
|
|
trusted_auth_cookie: str = "" # Cookie-Name (z. B. yunohost.portal)
|
|
|
|
|
|
trusted_auth_cookie_claim: str = "user" # JWT-Claim mit dem Usernamen
|
|
|
|
|
|
trusted_auth_jwt_secret: str = "" # optional: HS256-Secret -> Signatur pruefen
|
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
|
|
|
|
trusted_proxy_ips: str = "" # kommasepariert; IP(s) des Reverse-Proxys
|
|
|
|
|
|
admin_users: str = "" # kommaseparierte SSO-Usernamen mit Admin-Rechten
|
|
|
|
|
|
sso_logout_url: str = "" # Logout-Link fuers Frontend (SSO-Portal)
|
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
|
|
|
|
# Login-/Portal-URL: unauthentifizierte Seitenaufrufe werden hierhin umgeleitet
|
|
|
|
|
|
# (Defense-in-Depth zusaetzlich zu SSOwat). Leer -> stattdessen HTTP 401.
|
|
|
|
|
|
sso_login_url: str = ""
|
2026-06-17 04:16:35 +02:00
|
|
|
|
history_max_messages: int = 10
|
feat(memory): automatische Erinnerungs-Extraktion aus Gespraechen
- app/core/memory_extractor.py: LLM destilliert nach je N Turns dauerhafte
Fakten/Vorlieben aus dem Verlauf, dedupliziert gegen vorhandene Erinnerungen
und legt sie ab - best-effort, nicht-blockierend (Hintergrund-Task), eigener
Extraktions-Prompt (JSON, Reasoning aus), Cap-Begrenzung
- Trigger in /api/chat und /ws/voice nach dem Persistieren des Turns
- Konfig: MEMORY_EXTRACTION_ENABLED/_EVERY_N_TURNS/_MAX/_PROVIDER
- Tests: Extraktion, Dedup, kaputtes JSON, Cap, leeres Gespraech, Scheduling
- Doku: README + Architektur-Roadmap (Punkt 3 erledigt)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 03:15:08 +02:00
|
|
|
|
# Automatische Erinnerungs-Extraktion: das LLM destilliert dauerhafte Fakten
|
|
|
|
|
|
# aus dem Gespraech und legt sie als Nutzer-Erinnerungen ab (best-effort,
|
|
|
|
|
|
# nicht-blockierend). Leerer Provider = Default-LLM-Provider.
|
|
|
|
|
|
memory_extraction_enabled: bool = True
|
|
|
|
|
|
memory_extraction_every_n_turns: int = 3
|
|
|
|
|
|
memory_extraction_max: int = 50
|
|
|
|
|
|
memory_extraction_provider: str = ""
|
2026-06-17 18:07:36 +02:00
|
|
|
|
audio_stream_default: bool = True # satzweises TTS als Default (Admin kann abschalten)
|
feat(tts): Aussprache-Normalisierung vor Piper (Ordinalia/Einheiten/Abk./Lexikon)
In-Process-Layer ausgebaut statt neuer Lib/CLI. Leitprinzip: nicht duplizieren,
was espeak-ng schon kann (Kardinal-/Dezimalzahlen bleiben unangetastet) -- nur die
belegten Luecken fuellen.
- german_numbers.py: deutsche Ordinalzahlen 1.-31. (attributiv/adverbial)
- tts_normalizer.py: Ordinalia (Datum '1. Mai'->'erster Mai', Folgen '1. 2. 3.'->
'erstens, zweitens, ...'), Einheiten nach Zahl (kg/km/km-h/...), Abkuerzungen
(Dr./z.B./usw.), optionales YAML-Lexikon (config/pronunciation.<lang>.yaml).
Provider-abhaengige Stufen auto|full|light|off (TTS_NORMALIZE_LEVEL): piper=full,
Cloud=light (laesst Zahlen/Abk. fuer das Cloud-Modell in Ruhe).
- spoken_response_adapter.py: nummerierte Listen -> Ordinalwoerter statt Loeschen.
- sentence_chunker.py: trennt nicht mehr nach Ziffer+Punkt, Einzelbuchstabe+Punkt
('z. B.', Initialen) oder bekannten Abkuerzungen -> behebt das Streaming-Symptom
('1.' wurde als eigener 'Satz' zu 'eins').
- orchestrator/dependencies: normalize_level durchgereicht (auto: piper->full).
- Tests: tests/test_tts_normalizer.py + Chunker-Faelle (85 gruen).
- Doku: BEDIENUNGSANLEITUNG (Aussprache verbessern), .env.example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 23:01:41 +02:00
|
|
|
|
# TTS-Text-Normalisierung: auto|full|light|off. "auto" = piper -> full, Cloud -> light.
|
|
|
|
|
|
tts_normalize_level: str = "auto"
|
2026-06-17 05:19:07 +02:00
|
|
|
|
stt_fallback: str = "" # kommaseparierte Provider-Namen (Fallback-Kette)
|
|
|
|
|
|
llm_fallback: str = ""
|
|
|
|
|
|
tts_fallback: str = ""
|
2026-06-17 05:29:30 +02:00
|
|
|
|
daily_request_limit: int = 0 # 0 = unbegrenzt; Anfragen pro Nutzer pro Tag
|
|
|
|
|
|
emergency_webhook_url: str = "" # optionaler Eskalations-Webhook
|
2026-06-18 03:24:25 +02:00
|
|
|
|
# LLM-Notfall-Klassifikation (zweite Stufe, faengt was die Stichwoerter verpassen).
|
|
|
|
|
|
# Laeuft als Hintergrund-Task NUR wenn der Keyword-Filter nichts fand -> keine
|
|
|
|
|
|
# zusaetzliche Antwortlatenz. Leerer Provider = Default-LLM-Provider.
|
|
|
|
|
|
emergency_llm_enabled: bool = True
|
|
|
|
|
|
emergency_llm_provider: str = ""
|
|
|
|
|
|
emergency_llm_min_confidence: float = 0.6
|
2026-06-17 01:48:56 +02:00
|
|
|
|
model_config = SettingsConfigDict(
|
|
|
|
|
|
env_file=ENV_FILE, case_sensitive=False, extra="ignore"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def settings_customise_sources(
|
|
|
|
|
|
cls,
|
|
|
|
|
|
settings_cls,
|
|
|
|
|
|
init_settings,
|
|
|
|
|
|
env_settings,
|
|
|
|
|
|
dotenv_settings,
|
|
|
|
|
|
file_secret_settings,
|
|
|
|
|
|
):
|
|
|
|
|
|
# Praezedenz (frueher = hoeher): init > ENV > .env > TOML/Profil > Defaults
|
|
|
|
|
|
return (
|
|
|
|
|
|
init_settings,
|
|
|
|
|
|
env_settings,
|
|
|
|
|
|
dotenv_settings,
|
|
|
|
|
|
TomlProfileSource(settings_cls),
|
|
|
|
|
|
file_secret_settings,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
settings = Settings()
|