From 6c797b0ad910f9ee810d24def6bdda266da6e596 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Sun, 19 Jan 2020 16:21:29 +0100 Subject: [PATCH] Implemented support for assistant pause/resume conversation (closes #109) --- platypush/backend/assistant/__init__.py | 21 ++++++++++++ platypush/backend/assistant/google.py | 13 +++++--- .../backend/assistant/snowboy/__init__.py | 8 +++-- platypush/plugins/assistant/__init__.py | 28 ++++++++++++++++ platypush/plugins/assistant/echo/__init__.py | 20 +++++++---- .../plugins/assistant/google/__init__.py | 33 +++++++++++++++---- .../plugins/assistant/google/pushtotalk.py | 19 ++++++++++- 7 files changed, 122 insertions(+), 20 deletions(-) diff --git a/platypush/backend/assistant/__init__.py b/platypush/backend/assistant/__init__.py index e69de29bb2..6ac54b9572 100644 --- a/platypush/backend/assistant/__init__.py +++ b/platypush/backend/assistant/__init__.py @@ -0,0 +1,21 @@ +import threading + +from platypush.backend import Backend + + +class AssistantBackend(Backend): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._detection_paused = threading.Event() + + def pause_detection(self): + self._detection_paused.set() + + def resume_detection(self): + self._detection_paused.clear() + + def is_detecting(self): + return not self._detection_paused.is_set() + + +# vim:sw=4:ts=4:et: diff --git a/platypush/backend/assistant/google.py b/platypush/backend/assistant/google.py index 858e149663..3f6d3b6d16 100644 --- a/platypush/backend/assistant/google.py +++ b/platypush/backend/assistant/google.py @@ -7,7 +7,7 @@ import json import os import time -from platypush.backend import Backend +from platypush.backend.assistant import AssistantBackend from platypush.message.event.assistant import \ ConversationStartEvent, ConversationEndEvent, ConversationTimeoutEvent, \ ResponseEvent, NoResponseEvent, SpeechRecognizedEvent, AlarmStartedEvent, \ @@ -15,7 +15,7 @@ from platypush.message.event.assistant import \ AlertEndEvent -class AssistantGoogleBackend(Backend): +class AssistantGoogleBackend(AssistantBackend): """ Google Assistant backend. @@ -158,12 +158,15 @@ class AssistantGoogleBackend(Backend): with Assistant(self.credentials, self.device_model_id) as assistant: self.assistant = assistant for event in assistant.start(): + if not self.is_detecting(): + self.logger.info('Assistant event received but detection is currently paused') + continue + self._process_event(event) if self._has_error: - self.logger.info('Restarting the assistant after ' + - 'an unrecoverable error') + self.logger.info('Restarting the assistant after an unrecoverable error') time.sleep(5) - continue + break # vim:sw=4:ts=4:et: diff --git a/platypush/backend/assistant/snowboy/__init__.py b/platypush/backend/assistant/snowboy/__init__.py index 99eac3b417..991f6f9d9a 100644 --- a/platypush/backend/assistant/snowboy/__init__.py +++ b/platypush/backend/assistant/snowboy/__init__.py @@ -6,12 +6,12 @@ import os import threading -from platypush.backend import Backend +from platypush.backend.assistant import AssistantBackend from platypush.context import get_plugin from platypush.message.event.assistant import HotwordDetectedEvent -class AssistantSnowboyBackend(Backend): +class AssistantSnowboyBackend(AssistantBackend): """ Backend for detecting custom voice hotwords through Snowboy. The purpose of this component is only to detect the hotword specified in your Snowboy voice @@ -140,6 +140,10 @@ class AssistantSnowboyBackend(Backend): snowboydecoder.play_audio_file(sound) def callback(): + if not self.is_detecting(): + self.logger.info('Hotword detected but assistant response currently paused') + return + self.bus.post(HotwordDetectedEvent(hotword=hotword)) model = self.models[hotword] diff --git a/platypush/plugins/assistant/__init__.py b/platypush/plugins/assistant/__init__.py index d14adcf118..8ea4b36356 100644 --- a/platypush/plugins/assistant/__init__.py +++ b/platypush/plugins/assistant/__init__.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from platypush.plugins import Plugin + class AssistantPlugin(ABC, Plugin): """ Base class for assistant plugins @@ -9,10 +10,37 @@ class AssistantPlugin(ABC, Plugin): @abstractmethod def start_conversation(self, *args, language=None, **kwargs): + """ + Start a conversation. + """ raise NotImplementedError @abstractmethod def stop_conversation(self, *args, **kwargs): + """ + Stop a conversation. + """ + raise NotImplementedError + + @abstractmethod + def pause_detection(self, *args, **kwargs): + """ + Put the assistant on pause. No new conversation events will be triggered. + """ + raise NotImplementedError + + @abstractmethod + def resume_detection(self, *args, **kwargs): + """ + Resume the assistant hotword detection from a paused state. + """ + raise NotImplementedError + + @abstractmethod + def is_detecting(self) -> bool: + """ + :return: True if the asistant is detecting, False otherwise. + """ raise NotImplementedError diff --git a/platypush/plugins/assistant/echo/__init__.py b/platypush/plugins/assistant/echo/__init__.py index d474bd586e..8716ed6e52 100644 --- a/platypush/plugins/assistant/echo/__init__.py +++ b/platypush/plugins/assistant/echo/__init__.py @@ -3,6 +3,7 @@ """ import os +import threading from platypush.context import get_bus from platypush.plugins import action @@ -70,6 +71,7 @@ class AssistantEchoPlugin(AssistantPlugin): self.audio = Audio(device_name=audio_device) self.alexa = Alexa(avs_config_file, audio_player=audio_player) self._ready = False + self._detection_paused = threading.Event() self.alexa.state_listener.on_ready = self._on_ready() self.alexa.state_listener.on_listening = self._on_listening() @@ -115,9 +117,6 @@ class AssistantEchoPlugin(AssistantPlugin): @action def start_conversation(self, **kwargs): - """ - Programmatically start a conversation with the assistant - """ if not self._ready: raise RuntimeError('Echo assistant not ready') @@ -126,11 +125,20 @@ class AssistantEchoPlugin(AssistantPlugin): @action def stop_conversation(self): - """ - Programmatically stop a running conversation with the assistant - """ self.audio.stop() self._on_finished()() + @action + def pause_detection(self): + self._detection_paused.set() + + @action + def resume_detection(self): + self._detection_paused.clear() + + @action + def is_detecting(self) -> bool: + return not self._detection_paused.is_set() + # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/assistant/google/__init__.py b/platypush/plugins/assistant/google/__init__.py index 296f5fae71..a01459b91b 100644 --- a/platypush/plugins/assistant/google/__init__.py +++ b/platypush/plugins/assistant/google/__init__.py @@ -3,24 +3,30 @@ """ from platypush.context import get_backend -from platypush.plugins import Plugin, action +from platypush.plugins import action +from platypush.plugins.assistant import AssistantPlugin -class AssistantGooglePlugin(Plugin): + +class AssistantGooglePlugin(AssistantPlugin): """ Google assistant plugin. It acts like a wrapper around the :mod:`platypush.backend.assistant.google` backend to programmatically control the conversation status. """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, **kwargs): + super().__init__(**kwargs) + + @staticmethod + def _get_assistant(): + return get_backend('assistant.google') @action def start_conversation(self): """ Programmatically start a conversation with the assistant """ - assistant = get_backend('assistant.google') + assistant = self._get_assistant() assistant.start_conversation() @action @@ -28,8 +34,23 @@ class AssistantGooglePlugin(Plugin): """ Programmatically stop a running conversation with the assistant """ - assistant = get_backend('assistant.google') + assistant = self._get_assistant() assistant.stop_conversation() + @action + def pause_detection(self): + assistant = self._get_assistant() + assistant.pause_detection() + + @action + def resume_detection(self): + assistant = self._get_assistant() + assistant.resume_detection() + + @action + def is_detecting(self) -> bool: + assistant = self._get_assistant() + return assistant.is_detecting() + # vim:sw=4:ts=4:et: diff --git a/platypush/plugins/assistant/google/pushtotalk.py b/platypush/plugins/assistant/google/pushtotalk.py index 9ea709a414..d046f6256b 100644 --- a/platypush/plugins/assistant/google/pushtotalk.py +++ b/platypush/plugins/assistant/google/pushtotalk.py @@ -4,6 +4,7 @@ import json import os +import threading from platypush.context import get_bus from platypush.message.event.assistant import ConversationStartEvent, \ @@ -85,6 +86,7 @@ class AssistantGooglePushtotalkPlugin(AssistantPlugin): self.play_response = play_response self.assistant = None self.interactions = [] + self._detection_paused = threading.Event() with open(self.device_config) as f: device = json.load(f) @@ -219,6 +221,10 @@ class AssistantGooglePushtotalkPlugin(AssistantPlugin): from platypush.plugins.assistant.google.lib import SampleAssistant + if not self.is_detecting(): + self.logger.info('Conversation start event received but detection is currently paused') + return + if not language: language = self.language @@ -253,7 +259,6 @@ class AssistantGooglePushtotalkPlugin(AssistantPlugin): @action def stop_conversation(self): - """ Stop a conversation """ if self.assistant: self.assistant.play_response = False @@ -274,5 +279,17 @@ class AssistantGooglePushtotalkPlugin(AssistantPlugin): device_model_id=self.device_model_id, on=on)) + @action + def pause_detection(self): + self._detection_paused.set() + + @action + def resume_detection(self): + self._detection_paused.clear() + + @action + def is_detecting(self) -> bool: + return not self._detection_paused.is_set() + # vim:sw=4:ts=4:et: