mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-12 07:54:42 +02:00
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:
parent
a3ba3089e2
commit
69ea82c0e7
2 changed files with 120 additions and 37 deletions
|
|
@ -1,12 +1,12 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Hermes Voice Client
|
Hermes Voice Client
|
||||||
VAD → Whisper STT → keyword "hermes" → LiteLLM → piper TTS
|
VAD → Whisper STT (2 niveaux) → keyword "hermes" → hermes -z SSH → piper TTS
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python hermes-voice.py # parlez naturellement, commencez par "Hermes, ..."
|
python hermes-voice.py # dites "Ok Hermes, ..." — écoute en permanence
|
||||||
python hermes-voice.py --ptt # Entrée pour commencer/terminer (plus simple)
|
python hermes-voice.py --ptt # Entrée pour commencer/terminer
|
||||||
python hermes-voice.py --no-tts # réponses texte seulement (pour tester)
|
python hermes-voice.py --no-tts # réponses texte seulement (debug)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
@ -181,41 +181,60 @@ def speak(text: str):
|
||||||
out.unlink(missing_ok=True)
|
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:
|
if len(audio) / SAMPLE_RATE < MIN_SPEECH:
|
||||||
return
|
return
|
||||||
|
|
||||||
print(" ⏳ Transcription...", end=" ", flush=True)
|
print(" ⏳ Transcription...", end=" ", flush=True)
|
||||||
text = transcribe(whisper_model, audio)
|
text = transcribe(whisper_full, audio)
|
||||||
if not text:
|
if not text:
|
||||||
print("(rien compris)")
|
print("(rien compris)")
|
||||||
return
|
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}")
|
print(f"\n 🗣️ Vous : {text}")
|
||||||
|
_respond(text, no_tts)
|
||||||
|
|
||||||
|
|
||||||
|
def _respond(query: str, no_tts: bool):
|
||||||
print(" ⏳ Hermes...", end=" ", flush=True)
|
print(" ⏳ Hermes...", end=" ", flush=True)
|
||||||
try:
|
try:
|
||||||
response = ask_hermes(query)
|
response = ask_hermes(query)
|
||||||
except requests.RequestException as e:
|
except (requests.RequestException, RuntimeError) as e:
|
||||||
print(f"\n ❌ Réseau : {e}")
|
print(f"\n ❌ Erreur : {e}")
|
||||||
return
|
return
|
||||||
print(f"\n 🤖 Hermes : {response}\n")
|
print(f"\n 🤖 Hermes : {response}\n")
|
||||||
|
|
||||||
if not no_tts:
|
if not no_tts:
|
||||||
speak(response)
|
speak(response)
|
||||||
|
|
||||||
|
|
||||||
def run_vad(whisper_model, no_tts: bool):
|
def run_vad(whisper_fast, whisper_full, no_tts: bool):
|
||||||
"""Écoute continue par VAD — commencer par 'Hermes, ...' pour déclencher."""
|
"""Écoute continue — dites 'Ok Hermes, ...' pour déclencher."""
|
||||||
print(f"🎙️ En écoute — commencez par \"{KEYWORD.capitalize()}, ...\" (Ctrl+C pour quitter)\n")
|
print(f"🎙️ En écoute — dites \"Ok {KEYWORD.capitalize()}, ...\" (Ctrl+C pour quitter)\n")
|
||||||
|
|
||||||
audio_q: queue.Queue = queue.Queue()
|
audio_q: queue.Queue = queue.Queue()
|
||||||
pre_roll: deque = deque(maxlen=PRE_ROLL)
|
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
|
# Voix détectée — enregistrer jusqu'au silence
|
||||||
print(" 🔴 ", end="", flush=True)
|
print(" 🔴 ", end="", flush=True)
|
||||||
captured = list(pre_roll)
|
captured = list(pre_roll)
|
||||||
silent_blocks = 0
|
silent_blocks = 0
|
||||||
required_silent = int(SILENCE_SEC * SAMPLE_RATE / BLOCK_SIZE)
|
required_silent = int(SILENCE_SEC * SAMPLE_RATE / BLOCK_SIZE)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -255,10 +274,10 @@ def run_vad(whisper_model, no_tts: bool):
|
||||||
|
|
||||||
print()
|
print()
|
||||||
beep(440)
|
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."""
|
"""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")
|
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:
|
if not chunks:
|
||||||
continue
|
continue
|
||||||
beep(440)
|
beep(440)
|
||||||
handle(whisper_model, np.concatenate(chunks), no_tts, require_keyword=False)
|
handle_ptt(whisper_full, np.concatenate(chunks), no_tts)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
@ -292,18 +311,29 @@ def main():
|
||||||
|
|
||||||
print("Hermes Voice Client")
|
print("Hermes Voice Client")
|
||||||
print("=" * 40)
|
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:
|
from faster_whisper import WhisperModel
|
||||||
if args.ptt:
|
|
||||||
run_ptt(whisper, args.no_tts)
|
if args.ptt:
|
||||||
else:
|
print(f" ⏳ Chargement Whisper ({WHISPER_SIZE})...", end=" ", flush=True)
|
||||||
run_vad(whisper, args.no_tts)
|
whisper_full = WhisperModel(WHISPER_SIZE, device="cpu", compute_type="int8")
|
||||||
except KeyboardInterrupt:
|
print("✓\n")
|
||||||
print("\n👋 Au revoir")
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
53
tools/hermes-voice/install-service.sh
Executable file
53
tools/hermes-voice/install-service.sh
Executable 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"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue