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>
19 KiB
STT — Assistant vocal "Jarvis" du homelab Funk
STT est l'interface vocale et graphique de Funk : un assistant type Jarvis / Iron Man
qui écoute, parle et affiche un HUD animé. Architecture client-serveur : un client stt
sur le poste (voix + HUD) qui interroge un STT-server in-cluster (orchestration AI).
Objectif ultérieur : « agir sur le homelab » via les outils de Hermes (phase 7).
Nom de code :
STT. À l'origine « Speech-To-Text », mais le projet couvre toute la chaîne voix → cerveau → voix + interface + mémoire. Renommable plus tard sans impact technique (FRIDAY, etc.) — c'est juste un identifiant de répertoire/commande.
✅ 2026-06-17 — déployé in-cluster (ArgoCD), mémoire court-terme (5a) + long-terme (5b) validées sur cible, embeddings dédiés nomic
:1238(5d) + fix thinking Qwen3/no_think(5e). ✅ 2026-06-18 — HUD avancé (4) + auto-start (6). HUD : design Claude Design (avatar portrait réactif, transcript à bulles, drawer Réglages câblé au backend : reset / mode cerveau / mot de réveil à chaud). Auto-start :stt --install-service(systemd --user). App de bureau :stt --install-desktop(fenêtre type Discord) ; auto-update :stt --update. Reste : test audio bout-en-bout (phase 1), portraits réels + test sur cible, outils Hermes (7).
Principe directeur : réutiliser, ne pas réinventer
~70 % du backend existe déjà dans Funk. STT n'ajoute que le visage (HUD), la personnalisation, la mémoire multi-tiers et le packaging en commande.
| Besoin Jarvis | Brique Funk réutilisée | État |
|---|---|---|
| Modèle local ou Claude | LiteLLM :4000 + hermes-default + hermes-switch qwen|claude |
✅ opérationnel |
| Agir sur le homelab | Hermes Agent (:8080, profils funk-ai/system/monitor/brain) |
✅ opérationnel |
| Voix (STT + TTS + wake word) | tools/hermes-voice/ — faster-whisper + Piper + webrtcvad |
✅ existe (CLI only) |
| Démarrage auto au boot | stt --install-service (systemd --user) — repris du pattern tools/hermes-voice/ |
✅ |
| Mémoire sémantique | Qdrant :6333 + RAG (rag-query/rag-ingest) sur s01 |
✅ Qdrant réparé 17/06 (RAG funk-docs à re-ingérer) |
Maillon réellement manquant : l'interface graphique HUD + le packaging + la mémoire perso.
Décisions d'architecture (verrouillées 2026-06-17)
| Décision | Choix retenu |
|---|---|
| Architecture | Client-serveur : client sur le poste, STT-server in-cluster (révise le tout-local) |
| Interface | HUD web sur-mesure côté client (canvas/WebGL) |
| STT / TTS | Local sur le poste (faster-whisper CPU + Piper) — le serveur ne touche pas à l'audio |
| Packaging | Client : commande stt via pipx (#subdirectory=stt/client). Serveur : conteneur (ghcr) déployé par ArgoCD |
| Cerveau | Côté serveur : route vers LiteLLM :4000 (Qwen3 / Claude). Outils Hermes = phase ultérieure |
| Client → serveur | Client serveur-only (pas de repli Claude). URL serveur paramétrable |
| Mémoire | Côté serveur (futur) : Qdrant s01 + distillée GitHub. Client : cache local SQLite |
Pivot 2026-06-17 (post-test) : on est passé du tout-local à un modèle client-serveur. Le « cerveau » (ex-routeur 3 modes côté client) a migré côté serveur.
Architecture
LAN / *.lab.local
┌─ POSTE — client `stt` (pipx) ─┐ ┌─ CLUSTER k8s (namespace ai) ─────────┐
│ • micro + VAD + wake word │ HTTP │ STT-server (Deployment + Service) │
│ • faster-whisper (STT) │ ─────▶ │ GET /healthz │
│ • Piper (TTS) │ POST │ POST /v1/ask {text} → {reply} │
│ • HUD web (ui/ + hud/) │ /v1/ask│ brain → LiteLLM (httpx) │
│ • api.py → ServerClient │ ◀───── │ IngressRoute : stt.lab.local │
└────────────────────────────────┘ reply │ → LiteLLM s01 192.168.10.1:4000 │
└──────────────┬─────────────────────────┘
▼
LiteLLM :4000 (storage-01, hors cluster)
→ Qwen3 (g01) / Claude (hermes-default)
Le cerveau — côté serveur
Le STT-server appelle LiteLLM :4000 (OpenAI-compatible), joint depuis le cluster en
IP directe 192.168.10.1:4000 (même pattern qu'open-webui). LiteLLM
route lui-même vers Qwen3 (g01) ou Claude selon l'alias hermes-default / hermes-switch.
« Agir sur Funk » (outils de l'agent Hermes via le gateway
:8080) n'est pas dans le MVP : l'API:8080n'est pas spécifiée, et depuis un pod le SSH vers Hermes est impraticable. MVP = inférence/chat. L'intégration Hermes est une étape ultérieure.
Choix du modèle : le client envoie un model (alias LiteLLM) par requête à
/v1/ask ; le serveur le valide contre STT_ALLOWED_MODELS et le passe à LiteLLM. Pas de
switch global type hermes-switch (pas de restart, chaque client choisit le sien).
GET /v1/models liste le défaut + les alias autorisés. Noms courts client : hermes
(=hermes-default), qwen, claude (=claude-sonnet-4-6), opus.
Caveat Qwen3 — mode « thinking » (corrigé) : Qwen3 est un modèle de raisonnement. Sans
précaution, il dépense tout le budget max_tokens (200) dans reasoning_content → content
vide, ou réfléchit trop longtemps → timeout 502 (upstream LiteLLM : au message vide —
signature d'un timeout httpx). Diagnostiqué via appel direct LiteLLM : réponse en 0.87s mais
content="" et tout dans reasoning_content. Fix : brain.py ajoute le token /no_think
au prompt système (STT_DISABLE_THINKING, défaut true ; inoffensif hors Qwen) et récupère
reasoning_content en filet de sécurité si content est vide. Caveat partagé : admin/ia/llama_server.md.
La mémoire — côté serveur
- Court-terme (5a, fait) : le serveur garde l'historique par
session_id(en mémoire, bornémax_turns+ TTL) et l'injecte dans l'appel LLM./v1/ask {session_id}+/v1/reset. Le client génère unsession_idpar run. Deployment en 1 worker (cohérence mémoire process). - Long-terme (5b, ✅ validé 17/06) : collection Qdrant dédiée
stt-memory(indépendante dufunk-docsdu RAG), embeddings via Qwen3:1234(dim 4096, Cosine), souvenirs pertinents (top-k) injectés au prompt. Qdrant en IP directe192.168.10.1:6333. Dégrade proprement si Qdrant/embeddings injoignables (la mémoire court-terme tient).- Validé bout-en-bout : « mon chat s'appelle Felix » (session A) rappelé dans une
nouvelle session B (« comment s'appelle mon chat ? » → « Felix »).
points_countconfirmé via/v1/memory/health. upsert ?wait=true: l'écriture Qdrant est synchrone, donc un souvenir est immédiatement cherchable (sanswait, l'écriture est mise en file → un rappel cross-session immédiat pouvait la rater).GET /v1/memory/health: sonde active embed + Qdrant + collection, expose les erreurs (recall/remember dégradent en silence → indébogables sans cet endpoint). Sert au diagnostic.
- Validé bout-en-bout : « mon chat s'appelle Felix » (session A) rappelé dans une
nouvelle session B (« comment s'appelle mon chat ? » → « Felix »).
- Embeddings dédiés (5d, fait) : passage de Qwen3 (chat réutilisé, dim 4096, cosinus
uniformément hauts → peu discriminant) à
nomic-embed-text(modèle spécialisé, dim 768), servi par une instance llama-server dédiée sur gpu-01:1238(GPU), gérée par le rôlellama_server(llama_embed_enabled). STT :STT_EMBED_URL=…:1238,STT_EMBED_MODEL=nomic-embed-text.- Migration auto de la collection :
_ensure_collectiondétecte le changement de dimension (4096 → 768) et recréestt-memory(les anciens vecteurs sont incomparables dans le nouvel espace). Aucun drop manuel — la collection est reconstruite à la première requête après bascule.
- Migration auto de la collection :
- Réparation Qdrant (5c, fait) : crash-loop depuis 05/06 (segment
funk-docscorrompu). Réparé le 17/06 sur s01 :systemctl stop qdrant && rm -rf /srv/data/qdrant/storage/collections/funk-docs && systemctl start qdrant. - RAG documentaire / grounding (5f, fait) : le cerveau interroge aussi la collection
funk-docs(la docadmin/indexée) et injecte les passages pertinents au prompt → il répond à partir de la vraie doc du homelab au lieu d'halluciner des généralités (« stockage décentralisé », « worker nodes »…). Modulestt_server/knowledge.py, activé parSTT_DOCS_RAG.- Coût ~nul :
funk-docsetstt-memorypartagent le même embedder (nomic-embed-text:1238, dim 768) → le vecteur de requête calculé pour le recall mémoire est réutilisé pour la recherche doc (une seule embed, deux recherches Qdrant). Dégrade en silence (réponse sans doc si Qdrant/embed down). - Réglages :
STT_DOCS_TOPK(6),STT_DOCS_MIN_SCORE(0.45),STT_DOCS_TIMEOUT(4 s),STT_DOCS_COLLECTION(funk-docs). Lefunk-docsdoit être ingéré (rag-ingest) avec le même modèle nomic — sinon dimensions incompatibles (cf. rôlerag). - Validé : « c'est quoi le nom des nœuds ? » → sans RAG « worker/master nodes » (inventé) ;
avec RAG « Kubernetes ; nœuds storage-01, compute-01, compute-02 » (tiré de la doc). Le log
/v1/askexposedocs=<n>(nb d'extraits injectés).
- Coût ~nul :
⚠️ Vie privée : seule la mémoire distillée serait committée. Repo privé impératif.
Composants
| Composant | Emplacement | Description |
|---|---|---|
Client stt |
stt/client/ (pipx) |
stt (voix+HUD), stt --text (chat texte), stt --setup, stt --server <url>, stt --model <hermes|claude|qwen|opus> |
| — voix | stt/client/stt/voice/ |
wake word, ASR enfichable (voice/asr/), TTS Piper |
| — ASR | stt/client/stt/voice/asr/ |
backend enfichable : whisper (faster-whisper CPU, défaut) | onnx (Parakeet/Canary/Nemotron via onnx-asr, multilingue, streaming-ready). Choisi par [voice] asr_engine |
| — api | stt/client/stt/api.py |
ServerClient → POST /v1/ask |
| — UI/HUD | stt/client/stt/ui/ + hud/ |
HTTP statique + websocket bidirectionnel (états → HUD ; réglages et messages texte → backend) ; HUD embarqué dans le package |
| — portail | stt/client/stt/portal/ |
registre de services (registry.py) piloté par [[services]] de stt.toml → panneau « Services » du HUD (tuiles cliquables) et commande vocale « ouvre <service> ». Ouvre l'URL dans le navigateur normal |
| STT-server | stt/server/ (conteneur) |
FastAPI : /healthz, /v1/ask ; brain.py → LiteLLM |
| Image | ghcr.io/alkatrazz24/funk-stt-server |
construite par .github/workflows/build-stt-server.yml |
| Manifests | k8s/apps/stt/ |
Deployment, Service, IngressRoute (stt.lab.local) |
| Application ArgoCD | k8s/apps-of-apps/apps/stt.yaml |
déploie depuis main |
Personnalisation (« l'image, le design, la voix »)
Côté client, pilotable depuis stt/client/config/ + l'écran de réglages du HUD :
- Avatar / image (
config/avatars/), thème (config/themes/), voix Piper.onnx(config/voices/), wake word, URL serveur.
Prérequis / dépendances
- Poste : micro, Python 3.11+, pipx, navigateur. Piper + voix dans
~/.local/share/piper/,aplay. - Cluster : image poussée sur ghcr ; ArgoCD déploie depuis
main(donc merge requis) ; ghcr privé ⇒ éventuelimagePullSecret. - LiteLLM joignable depuis le cluster (
192.168.10.1:4000).
Auto-start (service systemd --user)
Lancer le HUD + voix automatiquement à l'ouverture de session (écran « Jarvis ») :
stt --install-service # écrit ~/.config/systemd/user/stt.service, enable + start
stt --uninstall-service # retire le service
stt --stop # éteint l'instance en cours (service systemd OU process lancé à la main)
Le unit est lié à graphical-session.target (démarre avec la session graphique, s'arrête avec).
Suivi : systemctl --user status|restart stt, journalctl --user -u stt -f.
stt --stop arrête proprement : si le service stt.service (systemd --user) est actif il fait
systemctl --user stop, sinon il envoie un SIGTERM aux process stt vocaux trouvés via /proc
(en s'excluant lui-même et les sous-commandes utilitaires --update/--version/…).
Caveat DISPLAY : un service systemd --user n'hérite de DISPLAY/WAYLAND_DISPLAY que si la
session graphique les a importés dans le gestionnaire user. --install-service exécute un
systemctl --user import-environment DISPLAY WAYLAND_DISPLAY XAUTHORITY (best-effort). Si le kiosk
ne s'ouvre pas : vérifier que le bureau peuple graphical-session.target (GNOME/KDE le font), ou
lancer stt via les « applications au démarrage » du bureau. Le kiosk est piloté par [ui].kiosk
(défaut true).
Boot sans login (borne dédiée) : sudo loginctl enable-linger "$USER" + auto-login du bureau —
la session graphique démarre au boot et tire le service.
Application de bureau & mise à jour
Lancer STT comme une appli (fenêtre dédiée type Discord, pas un onglet) :
stt --install-desktop # entrée de menu (.desktop) + icône → épinglable au dock
stt --uninstall-desktop # la retirer
Le lanceur exécute stt --window app : le HUD s'ouvre en fenêtre chromeless (--app= de
Chromium/Brave). Modes via --window ou [ui].window_mode : app (fenêtre), kiosk (plein
écran, défaut du service), none (n'ouvre rien). L'icône est stt/hud/icon.svg (aussi favicon).
La fenêtre tourne dans un profil + une WM class dédiés (--class=STT-Funk, --user-data-dir
sous ~/.local/share/stt/app-profile) → instance autonome avec sa propre icône/entrée de barre
des tâches (associée au .desktop via StartupWMClass), séparée de ta session Brave. (Une vraie
fenêtre native sans navigateur — via pywebview — reste possible plus tard si besoin.)
⚠️ Le lanceur de bureau et le service systemd (kiosk) utilisent les mêmes ports (9300/9301) : n'en faire tourner qu'un à la fois. Borne dédiée → service kiosk ; usage à la demande → lanceur de bureau (et
stt --uninstall-service).
Mettre à jour (sans désinstaller/réinstaller à la main) :
stt --update # pipx reinstall depuis la source git (via ta clé SSH) — affiche ancienne → nouvelle
stt --version # version installée
--update re-récupère la dernière version depuis la branche/source d'installation. Pré-requis :
avoir installé via pipx depuis git. Astuce : toujours bumper la version du paquet à chaque
release, sinon le cache de build pipx/uv peut resservir l'ancienne (cf. pyproject.toml).
Compatibilité distros : tout le client repose sur des standards communs — systemd --user,
lanceurs .desktop (XDG ~/.local/share/applications/), pipx — donc identique sur
AlmaLinux/Fedora (RHEL) et Debian/Ubuntu. Seul le navigateur diffère : _open_browser teste
les binaires natifs (rpm + deb) puis Flatpak (com.brave.Browser, org.chromium.Chromium…),
sinon repli sur le navigateur par défaut. update-desktop-database (desktop-file-utils) est
optionnel — son absence n'empêche rien.
Roadmap
| Phase | Objectif | État |
|---|---|---|
| 0 — Cadrage | Conception validée | ✅ |
| 1 — Client voix + HUD | stt : voix locale + HUD + websocket |
✅ |
| 2 — STT-server | FastAPI /v1/ask → LiteLLM |
✅ |
| 3 — Déploiement cluster | image ghcr + manifests k8s + ArgoCD (LiteLLM en IP directe) | ✅ déployé |
| 4 — HUD avancé | avatar portrait réactif (anneau/ping/spinner) + transcript + composer texte (chat clavier sans micro) + boutons stop (couper la réponse) / mute micro + drawer réglages (modèle/reset/wake à chaud) + thème accent | ✅ (à tester sur poste) |
| 5a — Mémoire court-terme | historique de session côté serveur | ✅ |
| 5b — Mémoire long-terme | Qdrant stt-memory + embeddings (dégrade si down) |
✅ validé 17/06 (rappel cross-session OK) |
| 5c — Réparation Qdrant | drop funk-docs corrompu + restart (s01) |
✅ (17/06) |
| 5d — Embeddings dédiés | nomic-embed-text (dim 768) sur gpu-01 :1238 → recherche plus précise |
✅ (rôle llama_server + migration auto collection) |
| 5e — Fix thinking Qwen3 | /no_think (content vide / timeout 502) + fallback reasoning_content |
✅ (17/06) |
| 5f — RAG documentaire (grounding) | le cerveau interroge funk-docs (doc admin/) et injecte les passages au prompt → répond depuis la vraie doc au lieu d'halluciner ; réutilise le vecteur nomic du recall |
✅ (knowledge.py, STT_DOCS_RAG) |
| 6 — Auto-start client | stt --install-service (systemd --user, graphical-session.target) + kiosk |
✅ (à tester sur poste) |
| 7 — Outils Hermes | « agir sur Funk » via gateway :8080 (API à spécifier) |
⏳ |
État (validé sur cible)
Serveur déployé in-cluster via ArgoCD (image sha-<commit> gérée par CI). Mémoire 5a + 5b
validées en prod : court-terme (historique de session) et long-terme (rappel cross-session
« Felix », points_count confirmé via /v1/memory/health). HUD avancé (4) implémenté
(design Claude Design, câblé au websocket backend) ; auto-start (6) via stt --install-service.
Le HUD inclut un composer texte ({"type":"text"} → VoiceEngine.respond_text) pour
dialoguer au clavier sans micro — utile tant que le bout-en-bout audio n'est pas validé.
Reste côté poste : test audio bout-en-bout (micro → STT → serveur → TTS) et test du HUD/kiosk
avec portraits réels.
Projets de référence (inspiration, non forkés)
Aucun ne se branche proprement sur LiteLLM + Hermes (ils embarquent leur propre LLM/agent), mais bons pour le design et le code à piocher :
- AlexandreSajus/JARVIS — Voice→LLM→Speech, interface web (proche du front visé).
- InterGenJLU/jarvis — AMD ROCm (comme la RX 6700XT), HUD santé, streaming TTS.
- novik133/jarvis — whisper.cpp + Piper (même TTS) + monitoring système.
- rhasspy/wyoming-addons — faster-whisper / Piper conteneurisés (option phase 7).