mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 19:14:43 +02:00
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>
409 lines
30 KiB
Markdown
409 lines
30 KiB
Markdown
# 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 <service> », « é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 », 0–200 %)
|
||
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).
|