diff --git a/tools/hermes-voice/hermes-voice.py b/tools/hermes-voice/hermes-voice.py index bb2725c..a977e60 100644 --- a/tools/hermes-voice/hermes-voice.py +++ b/tools/hermes-voice/hermes-voice.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 """ Hermes Voice Client -VAD → Whisper STT → keyword "hermes" → LiteLLM → piper TTS +VAD → Whisper STT (2 niveaux) → keyword "hermes" → hermes -z SSH → piper TTS Usage: - python hermes-voice.py # parlez naturellement, commencez par "Hermes, ..." - python hermes-voice.py --ptt # Entrée pour commencer/terminer (plus simple) - python hermes-voice.py --no-tts # réponses texte seulement (pour tester) + python hermes-voice.py # dites "Ok Hermes, ..." — écoute en permanence + python hermes-voice.py --ptt # Entrée pour commencer/terminer + python hermes-voice.py --no-tts # réponses texte seulement (debug) """ import argparse @@ -181,41 +181,60 @@ def speak(text: str): out.unlink(missing_ok=True) -def handle(whisper_model, audio: np.ndarray, no_tts: bool, require_keyword: bool = True): +def handle_vad(whisper_fast, whisper_full, audio: np.ndarray, no_tts: bool): + """VAD : détection rapide (small) puis transcription précise (large-v3).""" + if len(audio) / SAMPLE_RATE < MIN_SPEECH: + return + + # Étape 1 — vérification rapide du mot-clé avec le petit modèle + quick = transcribe(whisper_fast, audio) + found, _ = detect_keyword(quick) + if not found: + print(f"(ignoré : \"{quick[:60]}\")") + return + + # Étape 2 — transcription précise uniquement si mot-clé trouvé + print(" ⏳ Transcription...", end=" ", flush=True) + text = transcribe(whisper_full, audio) + if not text: + print("(rien compris)") + return + _, command = detect_keyword(text) + query = command or text + + print(f"\n 🗣️ Vous : {text}") + _respond(query, no_tts) + + +def handle_ptt(whisper_full, audio: np.ndarray, no_tts: bool): + """PTT : transcription directe sans filtre mot-clé.""" if len(audio) / SAMPLE_RATE < MIN_SPEECH: return print(" ⏳ Transcription...", end=" ", flush=True) - text = transcribe(whisper_model, audio) + text = transcribe(whisper_full, audio) if not text: print("(rien compris)") return - - if require_keyword: - found, command = detect_keyword(text) - if not found: - print(f"(ignoré : \"{text[:60]}\")") - return - query = command - else: - query = text - print(f"\n 🗣️ Vous : {text}") + _respond(text, no_tts) + + +def _respond(query: str, no_tts: bool): print(" ⏳ Hermes...", end=" ", flush=True) try: response = ask_hermes(query) - except requests.RequestException as e: - print(f"\n ❌ Réseau : {e}") + except (requests.RequestException, RuntimeError) as e: + print(f"\n ❌ Erreur : {e}") return print(f"\n 🤖 Hermes : {response}\n") - if not no_tts: speak(response) -def run_vad(whisper_model, no_tts: bool): - """Écoute continue par VAD — commencer par 'Hermes, ...' pour déclencher.""" - print(f"🎙️ En écoute — commencez par \"{KEYWORD.capitalize()}, ...\" (Ctrl+C pour quitter)\n") +def run_vad(whisper_fast, whisper_full, no_tts: bool): + """Écoute continue — dites 'Ok Hermes, ...' pour déclencher.""" + print(f"🎙️ En écoute — dites \"Ok {KEYWORD.capitalize()}, ...\" (Ctrl+C pour quitter)\n") audio_q: queue.Queue = queue.Queue() pre_roll: deque = deque(maxlen=PRE_ROLL) @@ -236,7 +255,7 @@ def run_vad(whisper_model, no_tts: bool): # Voix détectée — enregistrer jusqu'au silence print(" 🔴 ", end="", flush=True) captured = list(pre_roll) - silent_blocks = 0 + silent_blocks = 0 required_silent = int(SILENCE_SEC * SAMPLE_RATE / BLOCK_SIZE) while True: @@ -255,10 +274,10 @@ def run_vad(whisper_model, no_tts: bool): print() beep(440) - handle(whisper_model, np.concatenate(captured), no_tts, require_keyword=True) + handle_vad(whisper_fast, whisper_full, np.concatenate(captured), no_tts) -def run_ptt(whisper_model, no_tts: bool): +def run_ptt(whisper_full, no_tts: bool): """Push-to-talk : Entrée pour commencer, Entrée pour terminer.""" print("🎙️ Mode push-to-talk — Entrée pour parler, Entrée pour terminer (Ctrl+C pour quitter)\n") @@ -281,7 +300,7 @@ def run_ptt(whisper_model, no_tts: bool): if not chunks: continue beep(440) - handle(whisper_model, np.concatenate(chunks), no_tts, require_keyword=False) + handle_ptt(whisper_full, np.concatenate(chunks), no_tts) def main(): @@ -292,18 +311,29 @@ def main(): print("Hermes Voice Client") print("=" * 40) - print(f" ⏳ Chargement Whisper ({WHISPER_SIZE})...", end=" ", flush=True) - from faster_whisper import WhisperModel - whisper = WhisperModel(WHISPER_SIZE, device="cpu", compute_type="int8") - print("✓\n") - try: - if args.ptt: - run_ptt(whisper, args.no_tts) - else: - run_vad(whisper, args.no_tts) - except KeyboardInterrupt: - print("\n👋 Au revoir") + from faster_whisper import WhisperModel + + if args.ptt: + print(f" ⏳ Chargement Whisper ({WHISPER_SIZE})...", end=" ", flush=True) + whisper_full = WhisperModel(WHISPER_SIZE, device="cpu", compute_type="int8") + print("✓\n") + try: + run_ptt(whisper_full, args.no_tts) + except KeyboardInterrupt: + print("\n👋 Au revoir") + else: + # Mode VAD : deux modèles — small pour détection rapide, large pour transcription + print(" ⏳ Chargement Whisper rapide (small)...", end=" ", flush=True) + whisper_fast = WhisperModel("small", device="cpu", compute_type="int8") + print("✓") + print(f" ⏳ Chargement Whisper précis ({WHISPER_SIZE})...", end=" ", flush=True) + whisper_full = WhisperModel(WHISPER_SIZE, device="cpu", compute_type="int8") + print("✓\n") + try: + run_vad(whisper_fast, whisper_full, args.no_tts) + except KeyboardInterrupt: + print("\n👋 Au revoir") if __name__ == "__main__": diff --git a/tools/hermes-voice/install-service.sh b/tools/hermes-voice/install-service.sh new file mode 100755 index 0000000..d49493a --- /dev/null +++ b/tools/hermes-voice/install-service.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Installe hermes-voice comme service utilisateur systemd +# Lance automatiquement au démarrage de session, écoute "Ok Hermes, ..." + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VENV_PYTHON="$SCRIPT_DIR/.venv/bin/python" +SERVICE_DIR="$HOME/.config/systemd/user" +SERVICE_FILE="$SERVICE_DIR/hermes-voice.service" + +# Vérifications +if [ ! -f "$VENV_PYTHON" ]; then + echo "❌ venv introuvable — lancez d'abord :" + echo " python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt" + exit 1 +fi + +mkdir -p "$SERVICE_DIR" + +cat > "$SERVICE_FILE" << EOF +[Unit] +Description=Hermes Voice Client +After=network-online.target +Wants=network-online.target + +[Service] +ExecStart=$VENV_PYTHON $SCRIPT_DIR/hermes-voice.py +WorkingDirectory=$SCRIPT_DIR +Restart=on-failure +RestartSec=10s +Environment=PYTHONUNBUFFERED=1 + +StandardOutput=journal +StandardError=journal +SyslogIdentifier=hermes-voice + +[Install] +WantedBy=default.target +EOF + +systemctl --user daemon-reload +systemctl --user enable hermes-voice +systemctl --user start hermes-voice + +echo "" +echo "✓ Service installé et démarré" +echo "" +echo "Commandes utiles :" +echo " systemctl --user status hermes-voice" +echo " journalctl --user -u hermes-voice -f" +echo " systemctl --user stop hermes-voice" +echo " systemctl --user restart hermes-voice"