konan_voice.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. #!/usr/bin/env python3
  2. """
  3. Konan Voice — interface vocale directe sur le serveur.
  4. Parlez naturellement, Konan répond à voix haute.
  5. Ctrl+C pour quitter.
  6. """
  7. import warnings
  8. warnings.filterwarnings("ignore")
  9. import logging
  10. logging.getLogger("whisper").setLevel(logging.ERROR)
  11. import sounddevice as sd
  12. import numpy as np
  13. import whisper
  14. import requests
  15. import subprocess
  16. import tempfile
  17. import os
  18. import scipy.io.wavfile as wav
  19. SAMPLE_RATE = 16000
  20. CHUNK_SIZE = int(SAMPLE_RATE * 0.3) # 300 ms par chunk
  21. SILENCE_THRESH = 1200 # seuil énergie RMS (calibré au bruit ambiant)
  22. SILENCE_DUR = 1.5 # secondes de silence pour arrêter
  23. MIN_SPEECH_SEC = 0.5 # durée min pour traiter
  24. KONAN_API = "http://localhost:8765/api/chat"
  25. USER_ID = "voice_local"
  26. PIPER_BIN = os.path.expanduser("~/agent/venv/bin/piper")
  27. VOICE_MODEL = os.path.expanduser("~/agent/voices/fr_FR-upmc-medium.onnx")
  28. def rms(chunk):
  29. return np.sqrt(np.mean(chunk.astype(np.float32) ** 2))
  30. def speak(text):
  31. print(f"\nKonan : {text}\n")
  32. try:
  33. proc = subprocess.Popen(
  34. [PIPER_BIN, "--model", VOICE_MODEL, "--output-raw"],
  35. stdin=subprocess.PIPE,
  36. stdout=subprocess.PIPE,
  37. stderr=subprocess.DEVNULL,
  38. )
  39. raw, _ = proc.communicate(input=text.encode())
  40. audio = np.frombuffer(raw, dtype=np.int16)
  41. sd.play(audio, samplerate=22050)
  42. sd.wait()
  43. except Exception as e:
  44. print(f"[TTS erreur] {e}")
  45. def record_utterance():
  46. silence_chunks_needed = int(SILENCE_DUR / 0.3)
  47. recorded = []
  48. silence_count = 0
  49. speech_started = False
  50. with sd.InputStream(samplerate=SAMPLE_RATE, channels=1, dtype="int16") as stream:
  51. while True:
  52. chunk, _ = stream.read(CHUNK_SIZE)
  53. energy = rms(chunk)
  54. if energy > SILENCE_THRESH:
  55. if not speech_started:
  56. print("🎙️ Écoute...")
  57. speech_started = True
  58. recorded.append(chunk.copy())
  59. silence_count = 0
  60. elif speech_started:
  61. recorded.append(chunk.copy())
  62. silence_count += 1
  63. if silence_count >= silence_chunks_needed:
  64. break
  65. if not speech_started or len(recorded) < int(MIN_SPEECH_SEC / 0.3):
  66. return None
  67. return np.concatenate(recorded, axis=0).flatten()
  68. def transcribe(audio):
  69. with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
  70. wav.write(f.name, SAMPLE_RATE, audio)
  71. result = model.transcribe(f.name, language="fr")
  72. os.unlink(f.name)
  73. return result["text"].strip()
  74. def ask_konan(text):
  75. try:
  76. r = requests.post(KONAN_API, json={"text": text, "user_id": USER_ID}, timeout=120)
  77. return r.json().get("response", "Pas de réponse.")
  78. except Exception as e:
  79. return f"Erreur connexion au bot : {e}"
  80. print("Chargement de Whisper tiny...")
  81. model = whisper.load_model("tiny")
  82. print("Whisper prêt.\n")
  83. speak("Konan est prêt. Je vous écoute.")
  84. print("▶ Parlez naturellement. Ctrl+C pour quitter.\n")
  85. try:
  86. while True:
  87. audio = record_utterance()
  88. if audio is None:
  89. continue
  90. print("Transcription...")
  91. text = transcribe(audio)
  92. if not text:
  93. continue
  94. print(f"Vous : {text}")
  95. print("Konan réfléchit...")
  96. response = ask_konan(text)
  97. speak(response)
  98. except KeyboardInterrupt:
  99. speak("À bientôt.")
  100. print("\nAu revoir.")