Funk-lab/admin/ia/stt.md
ALI YESILKAYA ebb2eec184
feat(stt-client): page détail des services en pleine page + métriques live (#60)
La page détail d'un service (panneau « Services » du HUD) passe en PLEINE
PAGE : le drawer prend tout l'écran, le titre devient le nom du service, et
le contenu s'organise en 2 colonnes sur grand écran (données à gauche, état
+ actions à droite).

Nouvelle section « Métriques » par service, config-driven (comme `components`) :
- `[[services.metrics]]` → {label, prom, unit, hide_if_empty} évalué par le
  StatusPoller (dédup + parallèle avec les composants).
- Défauts via `config._pod_metrics(ns, pod_re)` pour les 7 services : redémarrages
  + uptime (kube-state-metrics, toujours dispo) ; mémoire + CPU (cAdvisor,
  `hide_if_empty` → carte masquée si non scrappé).
- Formatage HUD par unité : duration (8 j 9 h), bytes (497 Mo), cpu (30 mcœur),
  count.

Client 0.19.0. PromQL vérifié live (les 4 métriques renvoient des valeurs réelles
sur les 7 services). Rendu pleine page + formats validés via Playwright.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 00:18:39 +02:00

409 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).
Asa est désormais **agentique** : le STT-server fait du **function calling** (contexte `asa`) — elle
sait (doc + état live), cherche (web SearXNG) et **agit** (Hermes via `hermes-exec`, avec confirmation).
> **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`.
> ✅ **2026-06-21** — transcription partielle live + **veille/réveil vocal** (« Asa » / « Asa stop »).
> ✅ **2026-06-22** — **Asa agentique** : contexte `asa` par défaut (function calling local), outils
> doc + **état live enrichi** + **web (SearXNG)** + **actions Hermes** (`admin_action`, confirmation).
> Vérifié en prod. Voir `docs/stt-presentation.md` (+ schémas) et `docs/asa-commandes.md`.
> ✅ **2026-06-23** — **TTS enfichable** (Piper|Kokoro) + `stt --voices` / `--install-voice` / `--install-kokoro`.
> Reste : portraits d'avatar réels (le HUD montre le repli « あ »).
---
## 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éré (436 chunks, nomic dim 768) |
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) + **boucle d'outils** (function calling : doc, état live, web, actions Hermes) |
| 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 »** est désormais **en place** (2026-06-22) : pas via le gateway Hermes `:8080`
> (jamais spécifié) mais via **`hermes-exec`** (storage-01 `:9096`, jeton) appelé par l'outil
> `admin_action` du contexte `asa`, avec **confirmation vocale**. Détails plus bas.
**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`.
**Contextes présélectionnables (visualiseur + presets live)** : le client envoie un `context`
par requête à `/v1/ask` (`GET /v1/contexts` liste les profils). Chaque profil (`contexts.py`)
a son **system prompt** et ses **sources live** injectées dans le contexte (`sources.py`,
best-effort, env-config) :
- `asa`**boucle d'outils (function calling)**, **défaut prod** (`STT_DEFAULT_CONTEXT=asa`). Voir ci-dessous.
- `funk` — grounding strict + RAG `funk-docs` injecté d'office (comportement historique, sélectionnable)
- `ghostfolio` — valeur/positions du portefeuille (API Ghostfolio, `STT_GHOSTFOLIO_TOKEN`)
- `grafana` — métriques clés (Prometheus)
- `alerting` — alertes actives (Alertmanager, hors Watchdog)
- `cluster` — état pods/nœuds (Prometheus/kube-state-metrics) + RAG doc
**Contexte « asa » — boucle d'outils (function calling, local)** : au lieu d'un appel LLM unique
sur un contexte figé, Asa **décide elle-même** quels outils appeler (`tools.py`) puis répond à
partir de leurs résultats (`brain.ask_with_tools`, boucle bornée par `STT_TOOL_MAX_ITERS=4` ;
au-delà, réponse forcée sans outils). C'est ce qui débloque les questions à **état live** sans
présélection — ex. « gpu-01 tourne bien ? » → l'outil `host_health` est appelé → réponse réelle
(avant : le RAG seul disait « la doc ne le précise pas »). Outils **LECTURE SEULE** (`tools.py`) :
`search_docs` (RAG `funk-docs`), `host_health(gpu-01|storage-01)` (up/charge/RAM + llama-server,
Prometheus), `cluster_status` (nœuds/pods), `prometheus_query(expr)` (PromQL arbitraire), et
**`web_search(query)`** (recherche INTERNET via SearXNG, **Phase 2**). Modèle **local** Qwen3-8B
(le tool-calling natif de llama.cpp fonctionne, **même avec `/no_think`** — vérifié). Le `trace`
des outils appelés est renvoyé dans `context` → visualiseur HUD (un bloc par appel).
**Outil d'écriture `admin_action` (Phase 3)** : pour une demande explicite d'AGIR, le LLM appelle
`admin_action(description)` qui **n'exécute rien** — la boucle s'arrête (`confirm_tools` dans
`ask_with_tools`), Asa **propose** l'action et stocke un `pending` par session. Le tour suivant,
si l'utilisateur dit « confirme » (mots de `agent.is_confirmation`), `_handle_agentic` exécute via
`agent.run_action`**hermes-exec** (`hermes -z --yolo`) ; « annule » l'oublie. Réutilise le
handshake + le jeton du contexte `agent`. **Exposé seulement si `_actions_available()`** (opt-in
`STT_ACTIONS_ENABLED` + jeton) : sinon `admin_action` est retiré des schémas. La confirmation
protège des erreurs de transcription ; le jeton, des déclenchements non autorisés.
> **`cluster_status`** filtre les pods **terminés** (CronJob Succeeded/Failed) via un join
> `kube_pod_status_phase` → ne signale comme « non prêts » que les pods réellement actifs (sinon
> les pods cron finis, ex. `sacrifice-assign-renfort`, déclenchaient une fausse alarme).
**SearXNG (recherche web in-cluster, Phase 2)** : méta-moteur self-host, manifests
`k8s/apps/searxng/` (Deployment + Service + ConfigMap `settings.yml` + IngressRoute
`searxng.lab.local`), Application ArgoCD `searxng`. Namespace `ai`, **interne** (l'outil tape
`http://searxng:8080`). Points clés : `use_default_settings: true` (hérite des moteurs),
**`search.formats` inclut `json`** (sinon l'API JSON renvoie 403), `limiter:false`/`image_proxy:false`
(usage interne), image **pinnée**, conf copiée dans un emptyDir via initContainer (évite le mount RO).
Outil = `STT_SEARXNG_URL` (`tools._web_search`).
La réponse `/v1/ask` renvoie le **contexte assemblé** (`context`: system prompt + blocs live +
extraits RAG + mémoire) → alimente le **visualiseur** du HUD (« voir ce qu'on envoie à Asa »).
URLs in-cluster dans `k8s/apps/stt/deployment.yaml` (Prometheus/Alertmanager `monitoring`,
Ghostfolio `ai`). Jeton Ghostfolio = secret optionnel `stt-server-secrets/ghostfolio-token`.
**Contexte « agent » — actions via Hermes (OPT-IN)** : sélectionner le contexte `agent` fait
**agir** Asa sur le homelab. Flux dédié (`agent.py`, court-circuite le LLM) : la demande est
**confirmée** (handshake 2 temps « tu veux que… ? » → « confirme »/« annule »), puis exécutée par
le **vrai agent Hermes** via `hermes-exec` (storage-01:9096 → `hermes -z "<prompt>" --yolo`, profil
défaut, tous outils). Désactivé par défaut (`STT_ACTIONS_ENABLED=false`) ; le contexte n'apparaît
dans `/v1/contexts` que si activé **et** jeton présent. Jeton = `stt-server-secrets/hermes-exec-token`
côté serveur = `vault_hermes_exec_token` côté Ansible (rôle `hermes_exec`). La confirmation protège
des **erreurs de transcription** ; le jeton empêche tout déclenchement non autorisé depuis le cluster.
**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 nomic-embed-text `:1238` (dim 768, 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`.
- **RAG documentaire / grounding (5f, fait)** : le cerveau interroge aussi la collection
**`funk-docs`** (la doc `admin/` 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 »…). Module `stt_server/knowledge.py`, activé par `STT_DOCS_RAG`.
- **Coût ~nul** : `funk-docs` et `stt-memory` partagent 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`). Le `funk-docs` doit être ingéré (`rag-ingest`) **avec le
même modèle** nomic — sinon dimensions incompatibles (cf. rôle `rag`).
- **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/ask` expose `docs=<n>` (nb d'extraits injectés).
> ⚠️ **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/` | `registry.py` (services config-driven `[[services]]`) + `health.py` (StatusPoller). Panneau « Services » du HUD : tuiles + **pages détail** (description, **santé live**, composants, alertes, **aperçu enrichi**, bouton Ouvrir). Santé = probe HTTP + **Prometheus** `up{}`/kube-state-metrics + **Alertmanager**. Ouvre l'URL dans le navigateur normal |
| — aperçu enrichi (≥ 0.18.0) | `hud/index.html` (`renderServiceData`) + `ui/app.py` (`service-detail`) | Aperçu live spécifique au service dans sa page détail : **Ghostfolio** → valeur + performance + positions (alloc % et P/L par ligne, via `POST /v1/portfolio/details` du serveur, jeton client) ; **Alertmanager****toutes** les alertes actives (rendu HUD à partir de `all_alerts` poussé par le poller, auto-rafraîchi). Extensible : `DETAIL_PROVIDERS` (`server` = requête backend \| `alerts` = liste locale) |
| — page détail pleine page + métriques (≥ 0.19.0) | `hud/index.html` (`renderMetrics`, `is-detail`) + `registry.Metric` + `health.py` | La page détail passe en **pleine page** (le drawer prend tout l'écran, 2 colonnes sur grand écran : données ⟂ état). Section **Métriques** par service (config `[[services.metrics]]``{label, prom, unit, hide_if_empty}`) évaluée par le `StatusPoller` : redémarrages, uptime (kube-state-metrics) + mémoire, CPU (cAdvisor, masqués si absents). Défauts via `config._pod_metrics(ns, pod_re)`. Formatage HUD par unité (`duration`/`bytes`/`cpu`/`count`) |
| — intentions vocales | `stt/client/stt/portal/intents.py` + `ghostfolio.py` | lecture seule, court-circuitent le LLM (interceptées dans `engine._respond`) : « ouvre &lt;service&gt; », « état du cluster/services » (résumé santé parlé), « combien sur mon ghostfolio » (API Ghostfolio, `[ghostfolio] access_token`) |
| — contexte HUD | `stt/client/stt/ui/app.py` + `hud/` | sélecteur de contexte (Réglages) envoyé par requête + **visualiseur** (drawer) affichant le contexte assemblé renvoyé par le serveur ; affichage de la version installée |
| 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` |
---
## Interaction vocale — veille/réveil & transcription live (client ≥ 0.14.0)
Machine à états du moteur vocal (`stt/client/stt/voice/engine.py`) :
- **Veille (`asleep`)** — état par défaut. Asa transcrit chaque énoncé **uniquement** pour y
repérer le wake word ; elle ne répond à rien d'autre (HUD : portrait atténué, « SOMMEIL »).
- **Réveil** — dire **« Asa »** la passe en éveil. « Asa, <question> » exécute directement la
question. ⚠️ L'ASR (Parakeet FR) ne transcrit **pas** « Asa » comme un mot : il l'entend
« a so » / « à ça » / « ah ça »… La détection **joint les 1-2 premiers mots** (déaccentués,
sans espace) et compare en égalité exacte à `[voice].wake_aliases` (formes compactes :
`asa`, `aso`, `aca`, `ahca`…). Ajuster cette liste si l'ASR rend encore autrement — le log
`[stt.voice] (veille) entendu: '…'` (journalctl, cf. `STT_VOICE_LOG`) montre la transcription réelle.
- **Éveil (`listening`)** — elle répond à tout. **« Asa stop »** (ou « stop », « au revoir »,
« dors »… — `[voice].sleep_words`) la **remet en veille** ; un timeout d'inactivité
(`chat_timeout_sec`) aussi.
- **Transcription partielle live** — en éveil, le buffer en cours est re-transcrit toutes les
`partial_interval_sec` et poussé au HUD (`partial`) → le texte se forme en direct, puis la bulle
finale le remplace. Auto-activée seulement avec l'ASR **onnx** (Parakeet, rapide en CPU) ;
forçable via `[voice].partial_transcription = true|false`. Whisper large-v3 CPU est trop lent.
> Le wake word par défaut est désormais **`asa`** (était `hermes`). Les configs existantes
> (`~/.config/stt/stt.toml`) gardent leur valeur — changer `[voice].wake_word` ou via le HUD
> (réglage « mot de réveil ») / la commande `/wake`.
---
## 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**, **wake word**, **URL serveur**.
**Moteur TTS — `[voice].tts_engine`** : `piper` (défaut, rapide, voix `.onnx` dans
`~/.local/share/piper/`) **ou** `kokoro` (nettement plus naturel, ONNX/CPU). Pour Kokoro :
`stt --install-kokoro` (télécharge `kokoro-v1.0.onnx` + `voices-v1.0.bin` dans
`~/.local/share/kokoro/`), `pipx inject stt kokoro-onnx`, `espeak-ng` (phonémisation FR), puis
`tts_engine = "kokoro"` (voix FR `kokoro_voice = "ff_siwis"`). Le client **précharge** Kokoro au
démarrage et **retombe en silence** proprement si le modèle/paquet manque (`engine._synth_kokoro`).
La synthèse est enfichable (`engine._synthesize`) — Piper reste le repli.
**Volume de la voix** : réglable **à chaud** dans le HUD (Réglages → « Volume de la voix », 0200 %)
ou via `[voice].tts_volume` (0.0 muet … 2.0 amplifié). Gain appliqué au WAV avant lecture, commun
aux deux moteurs (`engine._apply_gain`) ; persisté côté HUD (localStorage).
**Fenêtrage** : `[ui].window_mode = "app"` (fenêtre déplaçable/redimensionnable, type Discord) |
`"kiosk"` (plein écran) | `"none"`. `stt --window app` **persiste** désormais le choix (relu par le
service au prochain `stt --restart`).
**Changer de voix Piper (homme/femme)** : `stt --voices` liste les voix FR ; `stt --install-voice
fr_FR-tom-medium` télécharge la voix (`.onnx` + `.onnx.json` depuis HuggingFace rhasspy/piper-voices)
**et la sélectionne** (`tts_engine=piper` + `piper_voice` écrits dans `stt.toml`). Puis `stt --restart`.
> ⚠️ Changer la voix nécessite d'écrire `tts_engine`/`piper_voice` dans le `stt.toml` **existant** :
> un simple `stt --update` ne bascule rien (la config perso prime sur le défaut) — d'où ces commandes.
---
## 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 --start # démarre le service
stt --restart # redémarre — recharge la config (modèle ASR, wake word…)
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`.
Ces actions sont aussi dispo **depuis le HUD** (Réglages → section « Service ») : **Redémarrer**,
**Mettre à jour** (`stt --update` puis `--restart`), **Arrêter**. Le HUD tournant *dans* le service,
ces actions sont lancées en **process détaché** (`start_new_session`) → le redémarrage va à son terme
même quand le process courant est tué (systemd possède le job).
`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) |
| **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](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).