import logging from abc import ABC, abstractmethod from queue import Empty, Queue from threading import Event, Thread, get_ident from typing import Optional, Sequence from platypush.message.event.assistant import AssistantEvent from .._context import ConversationContext class BaseProcessor(ABC, Thread): """ Base speech processor class. It is implemented by the ``SttProcessor`` and the ``IntentProcessor`` classes. """ def __init__( self, *args, stop_event: Event, conversation_timeout: Optional[float] = None, **kwargs, ): super().__init__(*args, name=f'picovoice:{self.__class__.__name__}', **kwargs) self.logger = logging.getLogger(self.name) self._audio_queue = Queue() self._stop_event = stop_event self._ctx = ConversationContext(timeout=conversation_timeout) self._event_queue = Queue() # This event is set if the upstream processor is waiting for an event # from this processor self._event_wait = Event() # This event is set when the processor is done with the audio # processing and it's ready to accept a new audio frame self._processing_done = Event() self._processing_done.set() def should_stop(self) -> bool: return self._stop_event.is_set() def wait_stop(self, timeout: Optional[float] = None) -> bool: return self._stop_event.wait(timeout) def enqueue(self, audio: Sequence[int]): self._event_wait.set() self._processing_done.clear() self._audio_queue.put_nowait(audio) @property def processing_done(self) -> Event: return self._processing_done @property @abstractmethod def _model_path(self) -> Optional[str]: """ Return the model path. """ @property @abstractmethod def sample_rate(self) -> int: """ :return: The sample rate wanted by Cheetah/Rhino. """ @property @abstractmethod def frame_length(self) -> int: """ :return: The frame length wanted by Cheetah/Rhino. """ def last_event(self) -> Optional[AssistantEvent]: """ :return: The latest event that was processed by the processor. """ evt = None try: while True: evt = self._event_queue.get_nowait() except Empty: pass if evt: self._event_wait.clear() return evt def clear_wait(self): self._event_wait.clear() @abstractmethod def process(self, audio: Sequence[int]) -> Optional[AssistantEvent]: """ Process speech events from a raw audio input. """ def run(self): super().run() self._ctx.reset() self._processing_done.clear() self.logger.info('Processor started: %s', self.name) while not self.should_stop(): audio = self._audio_queue.get() # The thread is stopped when it receives a None object if audio is None: break # Don't process the audio if the upstream processor is not waiting # for an event if not self._event_wait.is_set(): continue try: self._processing_done.clear() event = self.process(audio) if event: self._event_queue.put_nowait(event) self._processing_done.set() except Exception as e: self.logger.error( 'An error occurred while processing the audio on %s: %s', self.name, e, exc_info=e, ) self.wait_stop(timeout=1) self._processing_done.set() continue self._ctx.reset() self.logger.info('Processor stopped: %s', self.name) def stop(self): self._audio_queue.put_nowait(None) if self.is_alive() and self.ident != get_ident(): self.logger.debug('Stopping %s', self.name) self.join() def on_conversation_start(self): self._ctx.start() def on_conversation_end(self): self._ctx.stop() def on_conversation_reset(self): self._ctx.reset()