Funk-lab/stt/client/stt/config.py
alkatrazz e01318fb5f feat(stt-client): portail de services dans le HUD — tuiles cliquables + registre config-driven
Ajoute un panneau « Services » au HUD du client STT : une tuile par service du
homelab (Ghostfolio, Grafana, ArgoCD, n8n, Open WebUI, Prometheus, Alertmanager,
Traefik), un clic ouvre l'URL dans le navigateur habituel.

- stt/portal/ : registre piloté par [[services]] de stt.toml (id, name, url, icon,
  aliases) + résolution floue (registry.match_service) prête pour la voix.
  Ajouter un service = quelques lignes de config, zéro code.
- config.py : défauts homelab + doc dans stt.example.toml.
- ui/app.py : pousse la liste au HUD à la connexion, action de contrôle
  open-service → _open_url_external (xdg-open, session navigateur normale) + toast.
- hud/index.html : bouton header, drawer Services (voile partagé avec Réglages),
  grille de tuiles, toast de confirmation.
- bump 0.7.0 → 0.8.0.

Validé : registre (fuzzy-match FR/fautes/négatif), wiring backend, et HUD via
Playwright (rendu tuiles + clic émet open-service + toast, aucune erreur JS).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 22:07:41 +02:00

149 lines
5.4 KiB
Python

"""Configuration du client STT.
Défauts embarqués (robuste pour pipx). `stt --setup` les écrit dans
~/.config/stt/stt.toml ; au lancement on fusionne le fichier utilisateur par-dessus.
Le client ne contient PLUS le cerveau : il envoie le texte au STT-server (URL paramétrable).
"""
from __future__ import annotations
import tomllib
from pathlib import Path
from typing import Any
CONFIG_DIR = Path.home() / ".config" / "stt"
CONFIG_PATH = CONFIG_DIR / "stt.toml"
DATA_DIR = Path.home() / ".local" / "share" / "stt"
# Noms courts → alias LiteLLM (pour --model et la commande /model)
MODEL_PRESETS = {
"hermes": "hermes-default", # Qwen3-8B local (défaut, gratuit)
"qwen": "qwen3-8b",
"claude": "claude-sonnet-4-6", # Claude (facturé)
"opus": "claude-opus-4-7",
}
def resolve_model(name: str) -> str:
"""Nom court → alias LiteLLM (ou renvoie tel quel si déjà un alias)."""
return MODEL_PRESETS.get(name, name)
DEFAULT_CONFIG: dict[str, Any] = {
"server": {
# STT-server (in-cluster) — joignable via Traefik / wildcard *.lab.local
"url": "http://stt.lab.local",
"timeout_sec": 90,
# Modèle demandé au serveur (alias LiteLLM, ou nom court ci-dessous)
"model": "hermes-default",
},
"voice": {
"wake_word": "hermes",
# ASR : "whisper" (faster-whisper CPU, défaut) | "onnx" (Parakeet/Canary/Nemotron)
"asr_engine": "whisper",
"whisper_model": "large-v3",
# onnx-asr (si asr_engine="onnx") — modèle multilingue + execution providers.
# CPU par défaut ; pour le GPU AMD : ["ROCMExecutionProvider", "CPUExecutionProvider"].
"onnx_model": "nemo-parakeet-tdt-0.6b-v3",
"onnx_providers": ["CPUExecutionProvider"],
"piper_voice": "fr_FR-upmc-medium",
"language": "fr",
"silence_sec": 1.2,
"min_speech_sec": 0.6,
"chat_timeout_sec": 60,
},
"ui": {
# theme / avatar : désormais pilotés dans le HUD (Réglages → localStorage),
# conservés pour compat mais ignorés par le HUD actuel.
"theme": "arc-reactor",
"avatar": "default",
"kiosk": True,
# window_mode : "app" (fenêtre type Discord) | "kiosk" (plein écran) | "none".
# Si absent, repli sur kiosk ci-dessus (compat).
"window_mode": "kiosk",
"ws_host": "127.0.0.1",
"ws_port": 9300,
"http_port": 9301,
},
"memory": {
# cache local de conversation (brut, gitignoré) — la vraie mémoire est côté serveur
"enabled": True,
"local_db": str(DATA_DIR / "memory.sqlite"),
},
# Portail de services — alimente le panneau « Services » du HUD ET la commande
# vocale « ouvre <service> ». Ajouter un service = ajouter une entrée ici (ou
# un bloc [[services]] dans stt.toml, qui remplace alors toute cette liste).
# Champs : id (unique), name, url, icon (emoji), aliases (mots déclencheurs voix).
"services": [
{
"id": "ghostfolio", "name": "Ghostfolio",
"url": "http://ghostfolio.lab.local", "icon": "💰",
"aliases": ["portefeuille", "portfolio", "ghosto", "bourse", "investissements"],
},
{
"id": "grafana", "name": "Grafana",
"url": "http://grafana.lab.local", "icon": "📊",
"aliases": ["graphana", "metriques", "monitoring", "dashboard", "graphiques"],
},
{
"id": "argocd", "name": "ArgoCD",
"url": "http://argocd.lab.local", "icon": "🚢",
"aliases": ["argo", "argo cd", "deploiements", "gitops"],
},
{
"id": "n8n", "name": "n8n",
"url": "http://n8n.lab.local", "icon": "🔀",
"aliases": ["workflows", "automatisation", "nodes", "huit n huit"],
},
{
"id": "openwebui", "name": "Open WebUI",
"url": "http://openwebui.lab.local", "icon": "💬",
"aliases": ["open web ui", "webui", "chat", "llm", "interface ia"],
},
{
"id": "prometheus", "name": "Prometheus",
"url": "http://prometheus.lab.local", "icon": "🔥",
"aliases": ["prometheus", "metriques brutes", "tsdb"],
},
{
"id": "alertmanager", "name": "Alertmanager",
"url": "http://alertmanager.lab.local", "icon": "🚨",
"aliases": ["alertes", "alert manager", "alarmes"],
},
{
"id": "traefik", "name": "Traefik",
"url": "http://traefik.lab.local", "icon": "🛣️",
"aliases": ["reverse proxy", "ingress", "routeur", "proxy"],
},
],
}
def _deep_merge(base: dict, over: dict) -> dict:
out = dict(base)
for k, v in over.items():
if isinstance(v, dict) and isinstance(out.get(k), dict):
out[k] = _deep_merge(out[k], v)
else:
out[k] = v
return out
def load_config() -> dict[str, Any]:
if CONFIG_PATH.exists():
with CONFIG_PATH.open("rb") as f:
user = tomllib.load(f)
return _deep_merge(DEFAULT_CONFIG, user)
return DEFAULT_CONFIG
def write_default_config(force: bool = False) -> Path:
import tomli_w
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
DATA_DIR.mkdir(parents=True, exist_ok=True)
if CONFIG_PATH.exists() and not force:
return CONFIG_PATH
with CONFIG_PATH.open("wb") as f:
tomli_w.dump(DEFAULT_CONFIG, f)
return CONFIG_PATH