mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 21:54:42 +02:00
feat(stt): HUD avancé — design Claude Design câblé au backend
Remplace le HUD minimal (cercle CSS) par le design Claude Design : avatar
portrait réactif (anneau + ping d'écoute + spinner de réflexion), transcript
à bulles (tag modèle), indicateur de connexion + reconnexion auto, et un
drawer Réglages.
- HUD (stt/hud/index.html) : HTML/CSS/JS sans dépendance externe, DEMO=false
branché sur le websocket existant (états idle/listening/thinking/speaking,
user/assistant/error). Portraits chargés depuis hud/avatars/ (repli « あ »
si absent — voir avatars/README.md).
- Backend (ui/app.py) : le HUD renvoie {"type":"settings",...} ; le serveur
applique à chaud reset / mode cerveau (modèle LiteLLM) / mot de réveil via
_apply_settings (best-effort, non bloquant).
- engine/cli : tag du modèle courant ajouté aux réponses (events assistant).
- Docs : hud/README + avatars/README + admin/ia/stt.md (phase 4 ✅) + config.
Phase 4 de la roadmap STT. Reste : test sur poste + portraits réels.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qjq5jHiqNepnobJpHYpCa
This commit is contained in:
parent
7821651cce
commit
08529b825a
10 changed files with 1209 additions and 115 deletions
|
|
@ -11,8 +11,11 @@ Objectif ultérieur : « agir sur le homelab » via les outils de Hermes (phase
|
|||
|
||||
> ✅ **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). Reste : test audio bout-en-bout sur le poste (phase 1), HUD avancé (4), auto-start (6),
|
||||
> outils Hermes (7).
|
||||
> (5e).
|
||||
> ✅ **2026-06-18** — HUD avancé (4) implémenté (design **Claude Design**) : avatar portrait
|
||||
> réactif (anneau/ping/spinner), transcript à bulles, drawer Réglages **câblé au backend**
|
||||
> (reset / mode cerveau / mot de réveil à chaud). Reste : test audio bout-en-bout sur le poste
|
||||
> (phase 1), portraits réels + test HUD sur cible, auto-start (6), outils Hermes (7).
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -128,7 +131,7 @@ au prompt système (`STT_DISABLE_THINKING`, défaut `true` ; inoffensif hors Qwe
|
|||
| 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, 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 d'états ; HUD embarqué dans le package |
|
||||
| — UI/HUD | `stt/client/stt/ui/` + `hud/` | HTTP statique + websocket bidirectionnel (états → HUD, réglages → 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`) |
|
||||
|
|
@ -162,7 +165,7 @@ Côté client, pilotable depuis `stt/client/config/` + l'écran de réglages du
|
|||
| **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é** | visualiseur arc-reactor + thèmes + écran réglages | ⏳ |
|
||||
| **4 — HUD avancé** | avatar portrait réactif (anneau/ping/spinner) + transcript + 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) |
|
||||
|
|
@ -175,8 +178,9 @@ Côté client, pilotable depuis `stt/client/config/` + l'écran de réglages du
|
|||
|
||||
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`). Reste côté poste : test audio
|
||||
bout-en-bout (micro → STT → serveur → TTS) et HUD avancé.
|
||||
« Felix », `points_count` confirmé via `/v1/memory/health`). HUD avancé (4) **implémenté**
|
||||
(design Claude Design, câblé au websocket backend). Reste côté poste : test audio bout-en-bout
|
||||
(micro → STT → serveur → TTS) et test du HUD avec portraits réels.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -5,21 +5,23 @@ Tout ce qui se change **sans recompiler**. `stt --setup` copie `stt.example.toml
|
|||
|
||||
```
|
||||
config/
|
||||
├── stt.example.toml # config de référence (modes cerveau, voix, thème, mémoire)
|
||||
├── themes/ # thèmes HUD : couleurs, fond, style du visualiseur (CSS/JSON)
|
||||
├── avatars/ # images / avatars affichés dans le HUD
|
||||
└── voices/ # modèles Piper .onnx (TTS) — fr_FR par défaut
|
||||
├── stt.example.toml # config de référence (serveur/modèle, voix, ui, mémoire)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
> Les **portraits de l'avatar** et le **thème** ne vivent plus dans `config/` : le HUD les
|
||||
> gère lui-même. Avatars → `stt/hud/avatars/` (servis par le HUD — voir son README).
|
||||
> Thème → réglé en direct dans le drawer Réglages (intensité d'accent, mémorisé en localStorage).
|
||||
|
||||
## Points de personnalisation
|
||||
|
||||
| Quoi | Où | Note |
|
||||
|---|---|---|
|
||||
| Image / avatar | `avatars/` + `ui.avatar` | swappable à chaud |
|
||||
| Thème / design | `themes/` + `ui.theme` | couleurs, fond, visualiseur |
|
||||
| Voix | `voices/*.onnx` + `voice.piper_voice` | géré par `stt --setup` |
|
||||
| Wake word | `voice.wake_word` | défaut « Ok Hermès » |
|
||||
| Cerveau | `[brain]` | mode + override + endpoints |
|
||||
| Avatar (portraits) | `stt/hud/avatars/<set>-{idle,think,speak}.png` | jeu choisi dans Réglages → Avatar |
|
||||
| Thème / accent | Réglages → intensité (HUD) | rose doux → rouge, persisté en localStorage |
|
||||
| Voix TTS | `~/.local/share/piper/<voix>.onnx` + `voice.piper_voice` | Piper |
|
||||
| Mot de réveil | `voice.wake_word` (ou Réglages → mot de réveil) | défaut « hermes » |
|
||||
| Modèle (cerveau) | `server.model` · `stt --model` · Réglages → mode cerveau | alias LiteLLM |
|
||||
|
||||
> 🔒 La clé Anthropic (mode `claude-direct`) passe par la variable d'env `ANTHROPIC_API_KEY`,
|
||||
> **jamais** dans ce fichier (qui peut finir versionné).
|
||||
> 🔒 Pas de clé API côté client : l'inférence (et donc Claude) passe par le STT-server
|
||||
> in-cluster, jamais par le poste.
|
||||
|
|
|
|||
|
|
@ -18,12 +18,14 @@ silence_sec = 1.2
|
|||
min_speech_sec = 0.6
|
||||
chat_timeout_sec = 60
|
||||
|
||||
# theme / avatar sont désormais réglés dans le HUD (drawer Réglages, mémorisé en
|
||||
# localStorage) — ces deux clés sont gardées pour compat mais ignorées par le HUD.
|
||||
[ui]
|
||||
theme = "arc-reactor"
|
||||
avatar = "default"
|
||||
kiosk = true
|
||||
kiosk = true # ouvre le HUD en plein écran (kiosk) au lancement
|
||||
ws_host = "127.0.0.1"
|
||||
ws_port = 9300 # websocket états → HUD (doit matcher hud/index.html)
|
||||
ws_port = 9300 # websocket états ↔ HUD (doit matcher hud/index.html)
|
||||
http_port = 9301 # HTTP statique qui sert le HUD
|
||||
|
||||
[memory]
|
||||
|
|
|
|||
|
|
@ -95,11 +95,12 @@ def _cmd_voice(args) -> int:
|
|||
|
||||
def make_engine(emit):
|
||||
return VoiceEngine(
|
||||
cfg["voice"], client.ask, emit, memory=memory, no_tts=args.no_tts
|
||||
cfg["voice"], client.ask, emit, memory=memory, no_tts=args.no_tts,
|
||||
model_label=lambda: client.model,
|
||||
)
|
||||
|
||||
try:
|
||||
asyncio.run(serve(cfg, make_engine))
|
||||
asyncio.run(serve(cfg, make_engine, client))
|
||||
except KeyboardInterrupt:
|
||||
print("\n👋 Au revoir")
|
||||
finally:
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|||
"chat_timeout_sec": 60,
|
||||
},
|
||||
"ui": {
|
||||
# theme / avatar : désormais pilotés dans le HUD (Réglages → localStorage),
|
||||
# conservés pour compat mais ignorés par le HUD actuel.
|
||||
"theme": "arc-reactor",
|
||||
"avatar": "default",
|
||||
"kiosk": True,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,54 @@
|
|||
# stt/hud — interface HUD web (à implémenter, phase 3)
|
||||
# stt/hud — interface HUD web (implémentée)
|
||||
|
||||
Front statique servi localement par le backend, ouvert en kiosk (`ui.kiosk = true`).
|
||||
Se connecte au serveur websocket du backend.
|
||||
Front statique **HTML/CSS/JS sans dépendance externe** (tout inline, SVG natifs),
|
||||
servi localement par le backend (`stt/ui/app.py` — `SimpleHTTPRequestHandler` sur
|
||||
`ui.http_port`), ouvert en kiosk si `ui.kiosk = true`. Se connecte au serveur
|
||||
websocket du backend (`ui.ws_port`, défaut **9300**) pour recevoir les états et
|
||||
renvoyer les réglages.
|
||||
|
||||
## Contenu prévu
|
||||
## Ce qu'affiche le HUD
|
||||
|
||||
- **Visualiseur** : canvas/WebGL animé (arc-reactor / waveform) réagissant aux états
|
||||
`idle / listening / thinking / speaking`.
|
||||
- **Conversation** : transcript utilisateur + réponses de l'assistant.
|
||||
- **Réglages** : thème, avatar, voix, wake word, mode cerveau — écrit dans `~/.config/stt/stt.toml`.
|
||||
- **Avatar portrait réactif** (disque) — 3 images crossfadées selon l'état
|
||||
(`idle/listening → -idle`, `thinking → -think`, `speaking → -speak`), avec
|
||||
anneau fin, « ping » d'écoute et spinner de réflexion. Repli sur l'initiale
|
||||
du personnage si aucune image (voir [`avatars/README.md`](avatars/README.md)).
|
||||
- **Label d'état** : veille / écoute / réflexion / réponse.
|
||||
- **Transcript** : bulles utilisateur / assistant (+ tag du modèle) / erreur.
|
||||
- **Indicateur de connexion** websocket (point + libellé), reconnexion auto.
|
||||
- **Drawer Réglages** (engrenage) : intensité de l'accent (thème), avatar, voix,
|
||||
mot de réveil, mode cerveau (local/avancé), no-think, reset conversation.
|
||||
|
||||
## Design
|
||||
## Protocole websocket
|
||||
|
||||
S'inspire de [AlexandreSajus/JARVIS](https://github.com/AlexandreSajus/JARVIS) (interface web)
|
||||
et de l'esthétique Iron Man. Thèmes pilotés par `stt/config/themes/`, avatar par
|
||||
`stt/config/avatars/`.
|
||||
Reçus du backend (diffusés à tous les HUD connectés) :
|
||||
|
||||
Pas de framework lourd imposé — HTML/JS + canvas suffit pour le MVP. Le choix (vanilla vs
|
||||
petit framework) sera tranché en phase 3.
|
||||
| Événement | Effet |
|
||||
|---|---|
|
||||
| `{"type":"state","state":"idle\|listening\|thinking\|speaking"}` | animation + label |
|
||||
| `{"type":"user","text":…}` | bulle utilisateur |
|
||||
| `{"type":"assistant","text":…,"mode":…}` | bulle assistant (+ tag modèle) |
|
||||
| `{"type":"error","text":…}` | bulle d'erreur |
|
||||
|
||||
Envoyés au backend à chaque changement de réglage :
|
||||
|
||||
```json
|
||||
{"type":"settings","data":{"brainMode":"local","wakeword":"…","action":"reset"}}
|
||||
```
|
||||
|
||||
Le serveur applique `action:reset`, `brainMode` (→ modèle LiteLLM) et `wakeword`
|
||||
**à chaud** (`_apply_settings` dans `stt/ui/app.py`). Les réglages purement
|
||||
visuels (intensité d'accent, avatar) sont persistés côté navigateur (localStorage).
|
||||
|
||||
## Prévisualiser sans backend
|
||||
|
||||
Le HUD embarque un **mode démo** : passer `const DEMO = false` à `true` en tête
|
||||
du `<script>` rejoue une boucle d'événements simulés — pratique pour itérer le
|
||||
design sans lancer le serveur. Défaut livré : `false` (branché sur le backend).
|
||||
|
||||
## Design & personnalisation
|
||||
|
||||
Esthétique douce (blanc + accent rose → rouge), origine **Claude Design**. Les
|
||||
variables CSS en tête du `<style>` pilotent le rendu (`--accent-rest`,
|
||||
`--accent-active`, `--avatar`, `--ring-gap`…) ; le slider « intensité » interpole
|
||||
l'accent au repos. Portraits dans [`avatars/`](avatars/) (jeux `asa` / `mira` /
|
||||
`kai`, swappables à chaud depuis les Réglages).
|
||||
|
|
|
|||
34
stt/client/stt/hud/avatars/README.md
Normal file
34
stt/client/stt/hud/avatars/README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# hud/avatars — portraits de l'avatar (servis par le HUD)
|
||||
|
||||
Le HUD affiche le portrait du personnage et change d'image selon l'état :
|
||||
|
||||
| État HUD | Image attendue |
|
||||
|---|---|
|
||||
| `idle` / `listening` | `<set>-idle.png` |
|
||||
| `thinking` | `<set>-think.png` |
|
||||
| `speaking` | `<set>-speak.png` |
|
||||
|
||||
`<set>` est le jeu choisi dans **Réglages → Avatar** (`asa`, `mira`, `kai`…),
|
||||
mémorisé côté navigateur (localStorage). Exemple pour le jeu `asa` :
|
||||
|
||||
```
|
||||
avatars/
|
||||
├── asa-idle.png
|
||||
├── asa-think.png
|
||||
└── asa-speak.png
|
||||
```
|
||||
|
||||
## Format conseillé
|
||||
|
||||
- **Carré**, ~512×512 px (affiché en disque, recadrage `object-fit: cover`).
|
||||
- PNG (transparence acceptée). Le HUD applique le masque circulaire lui-même.
|
||||
|
||||
## Repli sans image
|
||||
|
||||
Si une image manque (404), le HUD **dégrade proprement** : il affiche l'initiale
|
||||
du personnage (« あ ») dans la couleur d'accent — aucune erreur, le HUD reste
|
||||
pleinement fonctionnel. Dépose simplement tes portraits ici pour les activer
|
||||
(pas de redémarrage : recharge la page).
|
||||
|
||||
> Ces fichiers sont livrés **dans le package** (`pyproject.toml` → package-data
|
||||
> `hud/**/*`), donc inclus par `pipx install`. Garde-les légers.
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -25,8 +25,12 @@ def _start_http(host: str, port: int) -> ThreadingHTTPServer:
|
|||
return httpd
|
||||
|
||||
|
||||
async def serve(cfg: dict[str, Any], make_engine) -> None:
|
||||
"""make_engine(emit) -> VoiceEngine. emit est thread-safe (depuis le thread voix)."""
|
||||
async def serve(cfg: dict[str, Any], make_engine, client) -> None:
|
||||
"""Sert le HUD + diffuse les états ; applique les réglages renvoyés par le HUD.
|
||||
|
||||
make_engine(emit) -> VoiceEngine. emit est thread-safe (depuis le thread voix).
|
||||
client : ServerClient — muté à chaud par les réglages (modèle, reset).
|
||||
"""
|
||||
import websockets
|
||||
|
||||
ui = cfg["ui"]
|
||||
|
|
@ -56,8 +60,14 @@ async def serve(cfg: dict[str, Any], make_engine) -> None:
|
|||
clients.add(ws)
|
||||
try:
|
||||
await ws.send(json.dumps(last_state, ensure_ascii=False))
|
||||
async for _ in ws: # le HUD n'envoie rien pour l'instant (phase 5 : réglages)
|
||||
pass
|
||||
# Le HUD renvoie {"type":"settings","data":{...}} quand on change un réglage.
|
||||
async for raw in ws:
|
||||
try:
|
||||
msg = json.loads(raw)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
if isinstance(msg, dict) and msg.get("type") == "settings":
|
||||
_apply_settings(client, engine, msg.get("data") or {}, loop)
|
||||
finally:
|
||||
clients.discard(ws)
|
||||
|
||||
|
|
@ -94,3 +104,34 @@ def _open_browser(url: str, kiosk: bool) -> None:
|
|||
subprocess.Popen([browser, flag, url])
|
||||
return
|
||||
webbrowser.open(url)
|
||||
|
||||
|
||||
def _apply_settings(client, engine, data: dict, loop) -> None:
|
||||
"""Applique les réglages renvoyés par le HUD — best-effort, jamais bloquant ni fatal.
|
||||
|
||||
Les réglages purement visuels (intensité d'accent, avatar) sont gérés côté
|
||||
navigateur (localStorage). Ici on n'applique que ce qui touche le backend :
|
||||
• action "reset" → vide la mémoire de session côté STT-server
|
||||
• brainMode → bascule le modèle (local = Qwen gratuit / advanced = Claude)
|
||||
• wakeword → mot de réveil, relu à chaud par le moteur vocal
|
||||
La voix Piper et le no-think ne sont pas applicables à chaud (ignorés ici).
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return
|
||||
try:
|
||||
if data.get("action") == "reset":
|
||||
loop.run_in_executor(None, client.reset) # appel HTTP → hors boucle
|
||||
return
|
||||
brain = data.get("brainMode")
|
||||
if brain in ("local", "advanced"):
|
||||
from stt.config import resolve_model
|
||||
|
||||
client.model = resolve_model("hermes" if brain == "local" else "claude")
|
||||
wake = (data.get("wakeword") or "").strip()
|
||||
if wake and engine is not None:
|
||||
from stt.voice.engine import _deaccent
|
||||
|
||||
parts = wake.split()
|
||||
engine.cfg["wake_word"] = _deaccent(parts[-1] if parts else wake)
|
||||
except Exception: # noqa: BLE001 — un réglage invalide ne doit jamais tuer la connexion
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -53,12 +53,15 @@ class VoiceEngine:
|
|||
emit: Callable[[dict], None],
|
||||
memory=None,
|
||||
no_tts: bool = False,
|
||||
model_label: Callable[[], str] | None = None,
|
||||
):
|
||||
self.cfg = cfg
|
||||
self.responder = responder
|
||||
self.emit = emit
|
||||
self.memory = memory
|
||||
self.no_tts = no_tts
|
||||
# callable renvoyant l'alias modèle courant → affiché en tag dans le HUD
|
||||
self.model_label = model_label
|
||||
self._whisper = None
|
||||
self._stop = False
|
||||
|
||||
|
|
@ -130,7 +133,8 @@ class VoiceEngine:
|
|||
self.emit({"type": "error", "text": str(e)})
|
||||
self.emit({"type": "state", "state": "idle"})
|
||||
return
|
||||
self.emit({"type": "assistant", "text": resp})
|
||||
mode = self.model_label() if self.model_label else None
|
||||
self.emit({"type": "assistant", "text": resp, "mode": mode})
|
||||
if self.memory:
|
||||
self.memory.log("assistant", resp)
|
||||
self.emit({"type": "state", "state": "speaking"})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue