feat(voice): détection deux niveaux + service systemd

- VAD mode : Whisper small (~0.3s) pour détecter "ok hermes", large-v3 uniquement pour transcrire la commande
- PTT mode : large-v3 direct (inchangé)
- install-service.sh : installe hermes-voice comme service utilisateur systemd (auto-start à la session)
- doc mise à jour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
alkatrazz 2026-06-02 16:54:57 +02:00
parent a3ba3089e2
commit 69ea82c0e7
2 changed files with 120 additions and 37 deletions

View file

@ -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__":

View file

@ -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"