# 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 `:8080` n'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 un `session_id` par 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 du `funk-docs` du RAG), embeddings via Qwen3 `:1234` (dim 4096, Cosine), souvenirs pertinents (top-k) injectés au prompt. Qdrant en IP directe `192.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_count` confirmé via `/v1/memory/health`. - **`upsert ?wait=true`** : l'écriture Qdrant est synchrone, donc un souvenir est *immédiatement* cherchable (sans `wait`, 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. - **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ôle `llama_server` (`llama_embed_enabled`). STT : `STT_EMBED_URL=…:1238`, `STT_EMBED_MODEL=nomic-embed-text`. - **Migration auto de la collection** : `_ensure_collection` détecte le changement de dimension (4096 → 768) et **recrée** `stt-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. - **Réparation Qdrant (5c, fait)** : crash-loop depuis 05/06 (segment `funk-docs` corrompu). Réparé le 17/06 sur s01 : `systemctl stop qdrant && rm -rf /srv/data/qdrant/storage/collections/funk-docs && systemctl start qdrant`. > ⚠️ **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 `, `stt --model ` | | — voix | `stt/client/stt/voice/` | wake word, STT faster-whisper, TTS Piper | | — 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 | | 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é ⇒ éventuel `imagePullSecret`. - **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 ») : ```bash 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) : ```bash 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) : ```bash 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) | | **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-` 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](https://github.com/AlexandreSajus/JARVIS) — Voice→LLM→Speech, interface web (proche du front visé). - [InterGenJLU/jarvis](https://github.com/InterGenJLU/jarvis) — AMD ROCm (comme la RX 6700XT), HUD santé, streaming TTS. - [novik133/jarvis](https://github.com/novik133/jarvis) — whisper.cpp + Piper (même TTS) + monitoring système. - [rhasspy/wyoming-addons](https://github.com/rhasspy/wyoming-addons) — faster-whisper / Piper conteneurisés (option phase 7).