""" Wake word detection module using Porcupine. Listens for the "Alexandr" wake word. """ import pvporcupine import pyaudio import struct from config import PORCUPINE_ACCESS_KEY, PORCUPINE_KEYWORD_PATH class WakeWordDetector: """Detects wake word using Porcupine.""" def __init__(self): self.porcupine = None self.audio_stream = None self.pa = None def initialize(self): """Initialize Porcupine and audio stream.""" self.porcupine = pvporcupine.create( access_key=PORCUPINE_ACCESS_KEY, keyword_paths=[str(PORCUPINE_KEYWORD_PATH)] ) self.pa = pyaudio.PyAudio() self.audio_stream = self.pa.open( rate=self.porcupine.sample_rate, channels=1, format=pyaudio.paInt16, input=True, frames_per_buffer=self.porcupine.frame_length ) print("🎤 Ожидание wake word 'Alexandr'...") def wait_for_wakeword(self) -> bool: """ Blocks until wake word is detected. Returns True when wake word is detected. """ if not self.porcupine: self.initialize() # Ensure stream is open and active if self.audio_stream is None or not self.audio_stream.is_active(): # If closed or None, we might need to recreate it. # PyAudio streams once closed cannot be reopened usually? # We should probably recreate it. if self.audio_stream: try: self.audio_stream.close() except: pass self.audio_stream = self.pa.open( rate=self.porcupine.sample_rate, channels=1, format=pyaudio.paInt16, input=True, frames_per_buffer=self.porcupine.frame_length ) while True: pcm = self.audio_stream.read(self.porcupine.frame_length, exception_on_overflow=False) pcm = struct.unpack_from("h" * self.porcupine.frame_length, pcm) keyword_index = self.porcupine.process(pcm) if keyword_index >= 0: print("✅ Wake word обнаружен!") # Stop and CLOSE stream to release mic for STT self.audio_stream.stop_stream() self.audio_stream.close() return True def check_wakeword_once(self) -> bool: """ Non-blocking check for wake word. Returns True if wake word detected, False otherwise. """ if not self.porcupine: self.initialize() try: # Ensure stream is open/active if self.audio_stream is None or not self.audio_stream.is_active(): # Re-open if needed (similar to wait_for_wakeword logic) if self.audio_stream: try: self.audio_stream.close() except: pass self.audio_stream = self.pa.open( rate=self.porcupine.sample_rate, channels=1, format=pyaudio.paInt16, input=True, frames_per_buffer=self.porcupine.frame_length ) pcm = self.audio_stream.read(self.porcupine.frame_length, exception_on_overflow=False) pcm = struct.unpack_from("h" * self.porcupine.frame_length, pcm) keyword_index = self.porcupine.process(pcm) if keyword_index >= 0: print("🛑 Wake word обнаружен во время ответа!") return True return False except Exception: return False def cleanup(self): """Release resources.""" if self.audio_stream: self.audio_stream.close() if self.pa: self.pa.terminate() if self.porcupine: self.porcupine.delete() # Global instance _detector = None def get_detector() -> WakeWordDetector: """Get or create wake word detector instance.""" global _detector if _detector is None: _detector = WakeWordDetector() return _detector def wait_for_wakeword() -> bool: """Wait for wake word detection.""" return get_detector().wait_for_wakeword() def cleanup(): """Cleanup detector resources.""" global _detector if _detector: _detector.cleanup() _detector = None def check_wakeword_once() -> bool: """ Non-blocking check for wake word. Returns True if wake word detected, False otherwise. """ return get_detector().check_wakeword_once()