Files
Alfredv2/alfred/app.backup.camera/services/wakeword.py
T
2026-10-26 00:06:24 +01:00

111 lines
5.3 KiB
Python

"""Détection permanente du mot d'activation d'Alfred.
Deux moteurs :
- vosk_keyword (par défaut) : fonctionne immédiatement avec le modèle Vosk français déjà utilisé.
- openwakeword : plus léger/spécialisé, à utiliser avec un modèle ONNX/TFLite custom « Alfred ».
Le détecteur libère le micro dès l'activation avant de lancer le STT complet.
"""
from __future__ import annotations
from services.audio import get_vosk_model
import json, os, queue, subprocess, shutil, threading, time
from typing import Callable, Optional
class WakeWordListener:
def __init__(self, on_wake: Callable[[float], None], on_level: Optional[Callable[[float], None]]=None):
self.on_wake=on_wake; self.on_level=on_level
self.stop_event=threading.Event(); self.pause_event=threading.Event(); self.thread=None
self.last_error=None; self.last_score=0.0
def start(self):
if self.thread and self.thread.is_alive(): return
self.stop_event.clear(); self.thread=threading.Thread(target=self._run, daemon=True); self.thread.start()
def stop(self): self.stop_event.set()
def pause(self): self.pause_event.set()
def resume(self): self.pause_event.clear()
def status(self):
return {"running": bool(self.thread and self.thread.is_alive()), "paused": self.pause_event.is_set(),
"engine": os.getenv("WAKE_ENGINE","vosk_keyword"), "word": os.getenv("WAKE_WORD","alfred"),
"last_score": self.last_score, "error": self.last_error}
def _run(self):
engine=os.getenv("WAKE_ENGINE","vosk_keyword").lower()
while not self.stop_event.is_set():
if self.pause_event.is_set(): time.sleep(.1); continue
try:
if engine == "openwakeword": self._openwakeword_session()
else: self._vosk_session()
except Exception as e:
self.last_error=str(e); time.sleep(1)
def _level(self, data):
if not self.on_level: return
try:
import numpy as np
a=np.frombuffer(data,dtype=np.int16).astype(np.float32)
rms=float(np.sqrt(np.mean(a*a))) if a.size else 0.0
self.on_level(min(1.0,rms/6000.0))
except Exception: pass
def _audio_chunks(self, blocksize=1280):
rate=int(os.getenv("VOSK_SAMPLE_RATE","16000")); q=queue.Queue()
try:
import sounddevice as sd
def cb(indata, frames, ti, status): q.put(bytes(indata))
dev=os.getenv("AUDIO_INPUT_DEVICE") or None
with sd.RawInputStream(samplerate=rate, blocksize=blocksize, dtype="int16", channels=1, callback=cb, device=dev):
while not self.stop_event.is_set() and not self.pause_event.is_set(): yield q.get()
return
except Exception: pass
if not shutil.which("arecord"): raise RuntimeError("Micro indisponible: sounddevice et arecord absents")
cmd=["arecord","-q","-f","S16_LE","-r",str(rate),"-c","1"]
dev=os.getenv("ALSA_CAPTURE_DEVICE","").strip()
if dev: cmd += ["-D",dev]
p=subprocess.Popen(cmd,stdout=subprocess.PIPE)
try:
while not self.stop_event.is_set() and not self.pause_event.is_set():
data=p.stdout.read(blocksize*2)
if data: yield data
finally:
if p.poll() is None: p.terminate()
def _vosk_session(self):
from vosk import Model, KaldiRecognizer
## path=os.getenv("VOSK_MODEL_PATH",""); word=os.getenv("WAKE_WORD","alfred").strip().lower()
##
path = os.getenv(
"WAKE_VOSK_MODEL_PATH",
os.getenv("VOSK_MODEL_PATH", "")
); word=os.getenv("WAKE_WORD","alfred").strip().lower()
if not os.path.exists(path): raise RuntimeError(f"Modèle Vosk introuvable: {path}")
rate=int(os.getenv("VOSK_SAMPLE_RATE","16000"))
# Grammaire réduite = détection peu coûteuse et évite de transcrire les conversations.
## rec=KaldiRecognizer(Model(path), rate, json.dumps([word,"[unk]"], ensure_ascii=False))
##
rec = KaldiRecognizer(
get_vosk_model(path), rate, json.dumps([word, "[unk]"], ensure_ascii=False)
)
for data in self._audio_chunks(4000):
self._level(data)
if rec.AcceptWaveform(data): txt=(json.loads(rec.Result() or "{}").get("text") or "").lower()
else: txt=(json.loads(rec.PartialResult() or "{}").get("partial") or "").lower()
if word in txt.split():
self.last_score=1.0; self.pause_event.set(); self.on_wake(1.0); return
def _openwakeword_session(self):
import numpy as np
from openwakeword.model import Model
model_path=os.getenv("WAKE_MODEL_PATH","").strip()
if not model_path or not os.path.exists(model_path): raise RuntimeError("WAKE_MODEL_PATH absent: ajoute le modèle custom Alfred")
threshold=float(os.getenv("WAKE_THRESHOLD","0.5"))
model=Model(wakeword_models=[model_path], inference_framework=os.getenv("WAKE_INFERENCE","onnx"))
for data in self._audio_chunks(1280):
self._level(data); audio=np.frombuffer(data,dtype=np.int16)
scores=model.predict(audio)
score=max((float(v) for v in scores.values()), default=0.0); self.last_score=score
if score >= threshold:
self.pause_event.set(); self.on_wake(score); return