From a3ba3089e263a5ea92409310d2f5874df8cefd3d Mon Sep 17 00:00:00 2001 From: alkatrazz Date: Tue, 2 Jun 2026 16:24:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(voice):=20am=C3=A9liorer=20hermes-voice=20?= =?UTF-8?q?+=20fix=20hermes-agent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hermes-voice : - mode SSH : appel `hermes -z` via storage-01 (soul + skills + RAG + mémoire) - mode litellm : fallback avec historique de session (10 échanges) - Whisper large-v3 : meilleure reconnaissance FR (distil-large-v3 dérivait vers l'anglais) - forcer français dans les requêtes SSH - historique conversationnel + reset vocal ("oublie tout") hermes-agent : - fix boucle de redémarrage (510 restarts) : ajouter --replace à ExecStart Co-Authored-By: Claude Sonnet 4.6 --- admin/ia/hermes-voice.md | 100 +++++++++++------- .../templates/hermes-agent.service.j2 | 2 +- tools/hermes-voice/hermes-voice.py | 62 +++++++++-- 3 files changed, 119 insertions(+), 45 deletions(-) diff --git a/admin/ia/hermes-voice.md b/admin/ia/hermes-voice.md index 0173f76..cd107b8 100644 --- a/admin/ia/hermes-voice.md +++ b/admin/ia/hermes-voice.md @@ -1,36 +1,44 @@ # Hermes Voice Client -Client vocal local pour interagir avec Hermes via la voix depuis le poste perso. +Client vocal local pour interagir avec Hermes depuis le poste perso. ## Architecture ``` -Micro → VAD (énergie) → faster-whisper (STT) → LiteLLM :4000 → piper (TTS) → Haut-parleur +Micro → VAD (énergie) → Whisper large-v3 (STT) → hermes -z via SSH → piper TTS → Haut-parleur ``` -- **STT** : faster-whisper `small` sur CPU (9950X3D) — ~0.3s de latence -- **Détection** : mot-clé "hermes" dans la transcription Whisper (pas de wake word model) -- **LLM** : modèle `hermes-default` via LiteLLM sur storage-01 -- **TTS** : piper avec voix `fr_FR-upmc-medium` +- **STT** : Whisper `large-v3` sur CPU (9950X3D) — ~2-3s, excellent en français +- **Détection** : mot-clé "hermes" détecté dans la transcription (pas de wake word model) +- **Backend** : `hermes --profile funk-ai -z` sur storage-01 via SSH — soul + skills + RAG + mémoire long-terme +- **TTS** : piper `fr_FR-upmc-medium` ## Installation -### Dépendances Python +### Dépendances système + +```bash +sudo dnf install alsa-utils # AlmaLinux / Fedora +# ou : sudo apt install alsa-utils +``` + +### Environnement Python ```bash cd tools/hermes-voice/ python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt +# Whisper large-v3 (~3GB) téléchargé automatiquement au premier lancement ``` ### Piper TTS ```bash -# Télécharger la release piper depuis https://github.com/rhasspy/piper/releases -# ex : piper_linux_x86_64.tar.gz - mkdir -p ~/.local/share/piper-runtime ~/.local/share/piper + +# Binaire piper — télécharger depuis https://github.com/rhasspy/piper/releases +# ex : piper_linux_x86_64.tar.gz tar xf piper_linux_x86_64.tar.gz -C /tmp cp -r /tmp/piper/* ~/.local/share/piper-runtime/ @@ -41,8 +49,7 @@ wget -O ~/.local/share/piper/fr_FR-upmc-medium.onnx.json \ "https://huggingface.co/rhasspy/piper-voices/resolve/main/fr/fr_FR/upmc/medium/fr_FR-upmc-medium.onnx.json" ``` -### Test piper - +Test piper : ```bash echo "Bonjour, je suis Hermes." | \ LD_LIBRARY_PATH=~/.local/share/piper-runtime \ @@ -51,18 +58,17 @@ echo "Bonjour, je suis Hermes." | \ --output_file /tmp/test.wav && aplay /tmp/test.wav ``` -Si `aplay` manque : `sudo dnf install alsa-utils` +### Accès réseau -### Réseau - -Le port 4000 (LiteLLM) doit être accessible depuis le LAN domestique (`192.168.1.0/24`). -Déjà ouvert dans `ansible/roles/gateway/templates/nftables.conf.j2` depuis le 2026-06-02. +Port 4000 (LiteLLM) ouvert depuis `192.168.1.0/24` dans nftables depuis le 2026-06-02. Vérification : ```bash curl -s http://192.168.1.200:4000/v1/models -H "Authorization: Bearer lm-studio" ``` +SSH vers storage-01 doit fonctionner sans mot de passe (clé dans `~/.ssh/config`, alias `s01`). + ## Utilisation ```bash @@ -72,48 +78,68 @@ source .venv/bin/activate # Mode VAD — parler naturellement, commencer par "Hermes, ..." python hermes-voice.py -# Mode push-to-talk — Entrée pour démarrer/terminer +# Mode push-to-talk — Entrée pour démarrer/terminer (plus simple) python hermes-voice.py --ptt -# Sans TTS (réponses texte uniquement — pour déboguer) +# Sans TTS — réponses texte uniquement (debug réseau/Hermes) python hermes-voice.py --ptt --no-tts ``` -## Mode VAD +## Modes de fonctionnement -Le client écoute en continu. Quand de l'énergie vocale est détectée : -1. Enregistrement jusqu'à 1.5s de silence -2. Whisper transcrit -3. Si "hermes" apparaît dans les 5 premiers mots → la commande est envoyée à LiteLLM -4. Sinon → ignoré (conversations ambiantes non captées) +### Mode VAD (défaut) + +Écoute continue. Quand de l'énergie vocale est détectée : +1. Enregistre jusqu'à 1.5s de silence +2. Whisper transcrit en français +3. Si "hermes" apparaît dans les 5 premiers mots → envoyé à Hermes +4. Sinon → ignoré Exemple : *"Hermes, quel est l'état du cluster ?"* +### Mode SSH (HERMES_MODE = "ssh", défaut) + +La requête passe par `hermes --profile funk-ai -z` sur storage-01. Hermes a accès à : +- Son **soul** (`funk-ai`) — connaissance complète du homelab +- Ses **skills** — agent-delegation, RAG docs +- Sa **mémoire long-terme** (`/srv/data/hermes/profiles/funk-ai/memories/`) + +### Mode LiteLLM (HERMES_MODE = "litellm") + +Appel direct LiteLLM :4000, historique de session conservé en RAM (10 échanges). Pas de soul ni de mémoire persistante — utile si SSH trop lent ou Hermes indisponible. + ## Configuration (`hermes-voice.py`) | Variable | Défaut | Description | |---|---|---| | `LITELLM_URL` | `http://192.168.1.200:4000/...` | Endpoint LiteLLM | -| `HERMES_MODEL` | `hermes-default` | Modèle LiteLLM | -| `WHISPER_SIZE` | `small` | Taille modèle Whisper | -| `VOICE_THRESH` | `0.012` | Seuil RMS détection voix | -| `SILENCE_SEC` | `1.5` | Délai silence pour terminer | -| `KEYWORD` | `hermes` | Mot-clé déclencheur | -| `KEYWORD_POS` | `5` | Position max du mot-clé | +| `HERMES_MODE` | `ssh` | `ssh` = vrai Hermes / `litellm` = LLM direct | +| `HERMES_SSH` | `s01` | Alias SSH storage-01 | +| `WHISPER_SIZE` | `large-v3` | Modèle Whisper (large-v3 recommandé pour le FR) | +| `VOICE_THRESH` | `0.012` | Seuil RMS détection voix (augmenter si bruit ambiant) | +| `SILENCE_SEC` | `1.5` | Délai silence pour terminer l'enregistrement | +| `KEYWORD` | `hermes` | Mot-clé déclencheur (mode VAD) | | `PIPER_VOICE` | `~/.local/share/piper/fr_FR-upmc-medium.onnx` | Voix TTS | ## Dépannage -**Port 4000 inaccessible** : ouvrir un tunnel SSH temporaire +**Whisper transcrit en anglais** : utiliser `large-v3` (pas `distil-large-v3` qui dérive vers l'anglais). + +**Modèle bloqué au chargement** : il télécharge depuis HuggingFace. Surveiller : ```bash -ssh -L 4000:localhost:4000 user@192.168.1.200 +watch -n2 'du -sh ~/.cache/huggingface/hub/' +``` +`large-v3` = ~3GB, `small` = ~250MB. + +**Port 4000 inaccessible** : ouvrir un tunnel SSH temporaire : +```bash +ssh -L 4000:localhost:4000 s01 # puis changer LITELLM_URL → http://localhost:4000/... ``` -**libpiper_phonemize.so introuvable** : tous les `.so` de piper doivent être dans +**libpiper_phonemize.so introuvable** : tous les fichiers piper doivent être dans `~/.local/share/piper-runtime/` (le script gère `LD_LIBRARY_PATH` automatiquement). -**Whisper ne comprend pas** : parler plus lentement, augmenter `WHISPER_SIZE` → `medium`. -Ou baisser `VOICE_THRESH` si le micro est peu sensible. +**Hermes via SSH lent** : basculer `HERMES_MODE = "litellm"` pour bypasser SSH. -**Faux positifs VAD** (bruit ambiant déclenche) : augmenter `VOICE_THRESH` (ex: `0.02`). +**Faux positifs VAD** : augmenter `VOICE_THRESH` (ex: `0.02`). diff --git a/ansible/roles/hermes_agent/templates/hermes-agent.service.j2 b/ansible/roles/hermes_agent/templates/hermes-agent.service.j2 index 517c847..35b0904 100644 --- a/ansible/roles/hermes_agent/templates/hermes-agent.service.j2 +++ b/ansible/roles/hermes_agent/templates/hermes-agent.service.j2 @@ -14,7 +14,7 @@ Environment=LM_BASE_URL={{ hermes_lm_base_url }} Environment=LM_API_KEY={{ hermes_lm_api_key }} Environment=PATH={{ hermes_data_dir }}/hermes-agent/venv/bin:/usr/local/bin:/usr/bin:/bin -ExecStart={{ hermes_home }}/.local/bin/hermes gateway run +ExecStart={{ hermes_home }}/.local/bin/hermes gateway run --replace Restart=on-failure RestartSec=10s diff --git a/tools/hermes-voice/hermes-voice.py b/tools/hermes-voice/hermes-voice.py index 5237c7f..bb2725c 100644 --- a/tools/hermes-voice/hermes-voice.py +++ b/tools/hermes-voice/hermes-voice.py @@ -39,7 +39,7 @@ VOICE_THRESH = 0.012 # seuil RMS normalisé pour détecter la parole SILENCE_SEC = 1.5 # silence pour terminer l'enregistrement MIN_SPEECH = 0.8 # durée minimale traitée (filtre les bruits courts) -WHISPER_SIZE = "small" # base=rapide, small=équilibré, medium=précis +WHISPER_SIZE = "large-v3" # large-v3 : meilleure reconnaissance FR (distil-large-v3 dérive vers l'anglais) KEYWORD = "hermes" KEYWORD_POS = 5 # le mot-clé doit être dans les N premiers mots @@ -48,8 +48,18 @@ KEYWORD_POS = 5 # le mot-clé doit être dans les N premiers mots PIPER_BIN = Path.home() / ".local/share/piper-runtime/piper" PIPER_VOICE = Path.home() / ".local/share/piper/fr_FR-upmc-medium.onnx" +MAX_HISTORY = 10 # nombre d'échanges (user+assistant) conservés en mémoire + +# Mode "full Hermes" via SSH (soul + skills + RAG + mémoire long-terme) +# Passer HERMES_MODE = "ssh" pour router par `hermes -z` sur storage-01 +# Passer HERMES_MODE = "litellm" pour appel direct LiteLLM (session history uniquement) +HERMES_MODE = "ssh" # "litellm" | "ssh" +HERMES_SSH = "s01" # alias SSH défini dans ~/.ssh/config + # ────────────────────────────────────────────────────────────────────────────── +_history: list[dict] = [] # historique session (mode litellm uniquement) + def beep(freq: int = 880, duration: float = 0.12): t = np.linspace(0, duration, int(SAMPLE_RATE * duration), endpoint=False) @@ -80,20 +90,56 @@ def detect_keyword(text: str) -> tuple[bool, str]: def transcribe(whisper_model, audio: np.ndarray) -> str: audio_f32 = audio.astype(np.float32) / 32768.0 segments, _ = whisper_model.transcribe( - audio_f32, language="fr", beam_size=3, vad_filter=True + audio_f32, + language="fr", + task="transcribe", + beam_size=5, + vad_filter=True, + initial_prompt="Transcription en français.", ) return " ".join(seg.text for seg in segments).strip() def ask_hermes(text: str) -> str: + if HERMES_MODE == "ssh": + return _ask_via_ssh(text) + return _ask_via_litellm(text) + + +def _ask_via_ssh(text: str) -> str: + """Route par le vrai Hermes sur storage-01 (soul + skills + RAG + mémoire).""" + import subprocess + # Forcer le français + échapper les guillemets simples pour bash + query = f"Réponds en français. {text}" + safe = query.replace("'", "'\\''") + cmd = [ + "ssh", HERMES_SSH, + f"sudo -i -u hermes bash -c 'HERMES_HOME=/srv/data/hermes hermes --profile funk-ai -z \"{safe}\"'" + ] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) + output = result.stdout.strip() + if not output and result.returncode != 0: + raise RuntimeError(result.stderr.strip() or "hermes -z a échoué") + return output + + +def _ask_via_litellm(text: str) -> str: + """Appel direct LiteLLM avec historique de session.""" + global _history + + lower = text.lower().strip() + if any(w in lower for w in ("reset", "nouveau contexte", "oublie tout", "efface")): + _history.clear() + return "Contexte effacé, on repart de zéro." + + _history.append({"role": "user", "content": text}) + window = _history[-(MAX_HISTORY * 2):] + resp = requests.post( LITELLM_URL, json={ "model": HERMES_MODEL, - "messages": [ - {"role": "system", "content": SYSTEM_PROMPT}, - {"role": "user", "content": text}, - ], + "messages": [{"role": "system", "content": SYSTEM_PROMPT}] + window, "max_tokens": 300, "temperature": 0.7, }, @@ -101,7 +147,9 @@ def ask_hermes(text: str) -> str: timeout=30, ) resp.raise_for_status() - return resp.json()["choices"][0]["message"]["content"].strip() + response = resp.json()["choices"][0]["message"]["content"].strip() + _history.append({"role": "assistant", "content": response}) + return response def speak(text: str):