import logging import os from threading import Event from time import time from typing import Any, Dict, Optional, Sequence import pvcheetah import pvleopard import pvporcupine import pvrhino from platypush.message.event.assistant import HotwordDetectedEvent from ._recorder import AudioRecorder class Assistant: """ A facade class that wraps the Picovoice engines under an assistant API. """ def __init__( self, access_key: str, stop_event: Event, hotword_enabled: bool = True, stt_enabled: bool = True, intent_enabled: bool = False, keywords: Optional[Sequence[str]] = None, keyword_paths: Optional[Sequence[str]] = None, keyword_model_path: Optional[str] = None, frame_expiration: float = 3.0, # Don't process audio frames older than this ): self.logger = logging.getLogger(__name__) self._access_key = access_key self._stop_event = stop_event self.hotword_enabled = hotword_enabled self.stt_enabled = stt_enabled self.intent_enabled = intent_enabled self.keywords = list(keywords or []) self.keyword_paths = None self.keyword_model_path = None self.frame_expiration = frame_expiration self._recorder = None if hotword_enabled: if keyword_paths: keyword_paths = [os.path.expanduser(path) for path in keyword_paths] missing_paths = [ path for path in keyword_paths if not os.path.isfile(path) ] if missing_paths: raise FileNotFoundError(f'Keyword files not found: {missing_paths}') self.keyword_paths = keyword_paths if keyword_model_path: keyword_model_path = os.path.expanduser(keyword_model_path) if not os.path.isfile(keyword_model_path): raise FileNotFoundError( f'Keyword model file not found: {keyword_model_path}' ) self.keyword_model_path = keyword_model_path self._cheetah: Optional[pvcheetah.Cheetah] = None self._leopard: Optional[pvleopard.Leopard] = None self._porcupine: Optional[pvporcupine.Porcupine] = None self._rhino: Optional[pvrhino.Rhino] = None def should_stop(self): return self._stop_event.is_set() def wait_stop(self): self._stop_event.wait() def _create_porcupine(self): if not self.hotword_enabled: return None args: Dict[str, Any] = {'access_key': self._access_key} if not (self.keywords or self.keyword_paths): raise ValueError( 'You need to provide either a list of keywords or a list of ' 'keyword paths if the wake-word engine is enabled' ) if self.keywords: args['keywords'] = self.keywords if self.keyword_paths: args['keyword_paths'] = self.keyword_paths if self.keyword_model_path: args['model_path'] = self.keyword_model_path return pvporcupine.create(**args) @property def porcupine(self) -> Optional[pvporcupine.Porcupine]: if not self._porcupine: self._porcupine = self._create_porcupine() return self._porcupine def __enter__(self): if self._recorder: self.logger.info('A recording stream already exists') elif self.porcupine: self._recorder = AudioRecorder( stop_event=self._stop_event, sample_rate=self.porcupine.sample_rate, frame_size=self.porcupine.frame_length, channels=1, ) self._recorder.__enter__() return self def __exit__(self, *_): if self._recorder: self._recorder.__exit__(*_) self._recorder = None if self._cheetah: self._cheetah.delete() self._cheetah = None if self._leopard: self._leopard.delete() self._leopard = None if self._porcupine: self._porcupine.delete() self._porcupine = None if self._rhino: self._rhino.delete() self._rhino = None def __iter__(self): return self def __next__(self): has_data = False if self.should_stop() or not self._recorder: raise StopIteration while not (self.should_stop() or has_data): if self.porcupine: # TODO also check current state data = self._recorder.read() if data is None: continue frame, t = data if time() - t > self.frame_expiration: self.logger.info( 'Skipping audio frame older than %ss', self.frame_expiration ) continue # The audio frame is too old keyword_index = self.porcupine.process(frame) if keyword_index is None: continue # No keyword detected if keyword_index >= 0 and self.keywords: return HotwordDetectedEvent(hotword=self.keywords[keyword_index]) raise StopIteration # vim:sw=4:ts=4:et: