forked from platypush/platypush
[#284] Merged assistant.google
plugin and backend.
This removes the deprecated `assistant.google` backend and also adds a new `conversation_start_sound` parameter.
This commit is contained in:
parent
debb4f6f36
commit
cce6c4c5ad
8 changed files with 397 additions and 303 deletions
|
@ -8,7 +8,6 @@ Backends
|
|||
|
||||
platypush/backend/adafruit.io.rst
|
||||
platypush/backend/alarm.rst
|
||||
platypush/backend/assistant.google.rst
|
||||
platypush/backend/assistant.snowboy.rst
|
||||
platypush/backend/button.flic.rst
|
||||
platypush/backend/camera.pi.rst
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
``assistant.google``
|
||||
======================================
|
||||
|
||||
.. automodule:: platypush.backend.assistant.google
|
||||
:members:
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from platypush.backend.assistant import AssistantBackend
|
||||
from platypush.message.event.assistant import (
|
||||
ConversationStartEvent,
|
||||
ConversationEndEvent,
|
||||
ConversationTimeoutEvent,
|
||||
ResponseEvent,
|
||||
NoResponseEvent,
|
||||
SpeechRecognizedEvent,
|
||||
AlarmStartedEvent,
|
||||
AlarmEndEvent,
|
||||
TimerStartedEvent,
|
||||
TimerEndEvent,
|
||||
AlertStartedEvent,
|
||||
AlertEndEvent,
|
||||
MicMutedEvent,
|
||||
MicUnmutedEvent,
|
||||
)
|
||||
|
||||
|
||||
class AssistantGoogleBackend(AssistantBackend):
|
||||
"""
|
||||
Google Assistant backend.
|
||||
|
||||
It listens for voice commands and post conversation events on the bus.
|
||||
|
||||
**WARNING**: The Google Assistant library used by this backend has officially been deprecated:
|
||||
https://developers.google.com/assistant/sdk/reference/library/python/. This backend still works on most of the
|
||||
devices where I use it, but its correct functioning is not guaranteed as the assistant library is no longer
|
||||
maintained.
|
||||
"""
|
||||
|
||||
_default_credentials_file = os.path.join(
|
||||
os.path.expanduser('~/.config'), 'google-oauthlib-tool', 'credentials.json'
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
credentials_file=_default_credentials_file,
|
||||
device_model_id='Platypush',
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
:param credentials_file: Path to the Google OAuth credentials file
|
||||
(default: ~/.config/google-oauthlib-tool/credentials.json).
|
||||
See
|
||||
https://developers.google.com/assistant/sdk/guides/library/python/embed/install-sample#generate_credentials
|
||||
for instructions to get your own credentials file.
|
||||
|
||||
:type credentials_file: str
|
||||
|
||||
:param device_model_id: Device model ID to use for the assistant
|
||||
(default: Platypush)
|
||||
:type device_model_id: str
|
||||
"""
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self.credentials_file = credentials_file
|
||||
self.device_model_id = device_model_id
|
||||
self.credentials = None
|
||||
self.assistant = None
|
||||
self._has_error = False
|
||||
self._is_muted = False
|
||||
|
||||
self.logger.info('Initialized Google Assistant backend')
|
||||
|
||||
def _process_event(self, event):
|
||||
from google.assistant.library.event import EventType, AlertType
|
||||
|
||||
self.logger.info('Received assistant event: {}'.format(event))
|
||||
self._has_error = False
|
||||
|
||||
if event.type == EventType.ON_CONVERSATION_TURN_STARTED:
|
||||
self.bus.post(ConversationStartEvent(assistant=self))
|
||||
elif event.type == EventType.ON_CONVERSATION_TURN_FINISHED:
|
||||
if not event.args.get('with_follow_on_turn'):
|
||||
self.bus.post(ConversationEndEvent(assistant=self))
|
||||
elif event.type == EventType.ON_CONVERSATION_TURN_TIMEOUT:
|
||||
self.bus.post(ConversationTimeoutEvent(assistant=self))
|
||||
elif event.type == EventType.ON_NO_RESPONSE:
|
||||
self.bus.post(NoResponseEvent(assistant=self))
|
||||
elif (
|
||||
hasattr(EventType, 'ON_RENDER_RESPONSE')
|
||||
and event.type == EventType.ON_RENDER_RESPONSE
|
||||
):
|
||||
self.bus.post(
|
||||
ResponseEvent(assistant=self, response_text=event.args.get('text'))
|
||||
)
|
||||
tts, args = self._get_tts_plugin()
|
||||
|
||||
if tts and 'text' in event.args:
|
||||
self.stop_conversation()
|
||||
tts.say(text=event.args['text'], **args)
|
||||
elif (
|
||||
hasattr(EventType, 'ON_RESPONDING_STARTED')
|
||||
and event.type == EventType.ON_RESPONDING_STARTED
|
||||
and event.args.get('is_error_response', False) is True
|
||||
):
|
||||
self.logger.warning('Assistant response error')
|
||||
elif event.type == EventType.ON_RECOGNIZING_SPEECH_FINISHED:
|
||||
phrase = event.args['text'].lower().strip()
|
||||
self.logger.info('Speech recognized: {}'.format(phrase))
|
||||
self.bus.post(SpeechRecognizedEvent(assistant=self, phrase=phrase))
|
||||
elif event.type == EventType.ON_ALERT_STARTED:
|
||||
if event.args.get('alert_type') == AlertType.ALARM:
|
||||
self.bus.post(AlarmStartedEvent(assistant=self))
|
||||
elif event.args.get('alert_type') == AlertType.TIMER:
|
||||
self.bus.post(TimerStartedEvent(assistant=self))
|
||||
else:
|
||||
self.bus.post(AlertStartedEvent(assistant=self))
|
||||
elif event.type == EventType.ON_ALERT_FINISHED:
|
||||
if event.args.get('alert_type') == AlertType.ALARM:
|
||||
self.bus.post(AlarmEndEvent(assistant=self))
|
||||
elif event.args.get('alert_type') == AlertType.TIMER:
|
||||
self.bus.post(TimerEndEvent(assistant=self))
|
||||
else:
|
||||
self.bus.post(AlertEndEvent(assistant=self))
|
||||
elif event.type == EventType.ON_ASSISTANT_ERROR:
|
||||
self._has_error = True
|
||||
if event.args.get('is_fatal'):
|
||||
self.logger.error('Fatal assistant error')
|
||||
else:
|
||||
self.logger.warning('Assistant error')
|
||||
if event.type == EventType.ON_MUTED_CHANGED:
|
||||
self._is_muted = event.args.get('is_muted')
|
||||
event = MicMutedEvent() if self._is_muted else MicUnmutedEvent()
|
||||
self.bus.post(event)
|
||||
|
||||
def start_conversation(self):
|
||||
"""Starts a conversation."""
|
||||
if self.assistant:
|
||||
self.assistant.start_conversation()
|
||||
|
||||
def stop_conversation(self):
|
||||
"""Stops an active conversation."""
|
||||
if self.assistant:
|
||||
self.assistant.stop_conversation()
|
||||
|
||||
def set_mic_mute(self, muted):
|
||||
if not self.assistant:
|
||||
self.logger.warning('Assistant not running')
|
||||
return
|
||||
|
||||
self.assistant.set_mic_mute(muted)
|
||||
|
||||
def is_muted(self) -> bool:
|
||||
return self._is_muted
|
||||
|
||||
def send_text_query(self, query):
|
||||
if not self.assistant:
|
||||
self.logger.warning('Assistant not running')
|
||||
return
|
||||
|
||||
self.assistant.send_text_query(query)
|
||||
|
||||
def run(self):
|
||||
import google.oauth2.credentials
|
||||
from google.assistant.library import Assistant
|
||||
|
||||
super().run()
|
||||
|
||||
with open(self.credentials_file, 'r') as f:
|
||||
self.credentials = google.oauth2.credentials.Credentials(
|
||||
token=None, **json.load(f)
|
||||
)
|
||||
|
||||
while not self.should_stop():
|
||||
self._has_error = False
|
||||
|
||||
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'
|
||||
)
|
||||
time.sleep(5)
|
||||
break
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
|
@ -1,39 +0,0 @@
|
|||
manifest:
|
||||
events:
|
||||
platypush.message.event.assistant.AlarmEndEvent: when an alarm ends
|
||||
platypush.message.event.assistant.AlarmStartedEvent: when an alarm starts
|
||||
platypush.message.event.assistant.ConversationEndEvent: when a new conversation
|
||||
ends
|
||||
platypush.message.event.assistant.ConversationStartEvent: when a new conversation
|
||||
starts
|
||||
platypush.message.event.assistant.ConversationTimeoutEvent: when a conversation
|
||||
times out
|
||||
platypush.message.event.assistant.MicMutedEvent: when the microphone is muted.
|
||||
platypush.message.event.assistant.MicUnmutedEvent: when the microphone is un-muted.
|
||||
platypush.message.event.assistant.NoResponseEvent: when a conversation returned no
|
||||
response
|
||||
platypush.message.event.assistant.ResponseEvent: when the assistant is speaking
|
||||
a response
|
||||
platypush.message.event.assistant.SpeechRecognizedEvent: when a new voice command
|
||||
is recognized
|
||||
platypush.message.event.assistant.TimerEndEvent: when a timer ends
|
||||
platypush.message.event.assistant.TimerStartedEvent: when a timer starts
|
||||
install:
|
||||
apk:
|
||||
- py3-grpcio
|
||||
- py3-google-auth
|
||||
apt:
|
||||
- python3-grpcio
|
||||
- python3-google-auth
|
||||
dnf:
|
||||
- python-grpcio
|
||||
- python-google-auth
|
||||
pacman:
|
||||
- python-grpcio
|
||||
- python-google-auth
|
||||
pip:
|
||||
- google-assistant-library
|
||||
- google-assistant-sdk[samples]
|
||||
- google-auth
|
||||
package: platypush.backend.assistant.google
|
||||
type: backend
|
|
@ -1,8 +1,6 @@
|
|||
import logging
|
||||
import re
|
||||
import sys
|
||||
|
||||
from platypush.context import get_backend, get_plugin
|
||||
from platypush.message.event import Event
|
||||
|
||||
|
||||
|
@ -11,18 +9,7 @@ class AssistantEvent(Event):
|
|||
|
||||
def __init__(self, *args, assistant=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.logger = logging.getLogger('platypush:assistant')
|
||||
|
||||
if assistant:
|
||||
self._assistant = assistant
|
||||
else:
|
||||
try:
|
||||
self._assistant = get_backend('assistant.google')
|
||||
if not self._assistant:
|
||||
self._assistant = get_plugin('assistant.google.pushtotalk')
|
||||
except Exception as e:
|
||||
self.logger.debug('Could not initialize the assistant component: %s', e)
|
||||
self._assistant = None
|
||||
|
||||
|
||||
class ConversationStartEvent(AssistantEvent):
|
||||
|
|
|
@ -1,54 +1,76 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from threading import Event
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from platypush.context import get_backend
|
||||
from platypush.context import get_plugin
|
||||
from platypush.plugins import Plugin, action
|
||||
|
||||
|
||||
class AssistantPlugin(ABC, Plugin):
|
||||
"""
|
||||
Base class for assistant plugins
|
||||
Base class for assistant plugins.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def start_conversation(self, *args, language=None, tts_plugin=None, tts_args=None, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
tts_plugin: Optional[str] = None,
|
||||
tts_plugin_args: Optional[Dict[str, Any]] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Start a conversation.
|
||||
:param tts_plugin: If set, the assistant will use this plugin (e.g.
|
||||
``tts``, ``tts.google`` or ``tts.mimic3``) to render the responses,
|
||||
instead of using the built-in assistant voice.
|
||||
|
||||
:param tts_plugin_args: Optional arguments to be passed to the TTS
|
||||
``say`` action, if ``tts_plugin`` is set.
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.tts_plugin = tts_plugin
|
||||
self.tts_plugin_args = tts_plugin_args or {}
|
||||
self._detection_paused = Event()
|
||||
|
||||
@abstractmethod
|
||||
def start_conversation(self, *_, **__):
|
||||
"""
|
||||
Programmatically starts a conversation.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def stop_conversation(self, *args, **kwargs):
|
||||
def stop_conversation(self, *_, **__):
|
||||
"""
|
||||
Stop a conversation.
|
||||
Programmatically stops a conversation.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _get_assistant(self):
|
||||
return get_backend('assistant.snowboy')
|
||||
|
||||
@action
|
||||
def pause_detection(self):
|
||||
"""
|
||||
Put the assistant on pause. No new conversation events will be triggered.
|
||||
"""
|
||||
assistant = self._get_assistant()
|
||||
assistant.pause_detection()
|
||||
self._detection_paused.set()
|
||||
|
||||
@action
|
||||
def resume_detection(self):
|
||||
"""
|
||||
Resume the assistant hotword detection from a paused state.
|
||||
"""
|
||||
assistant = self._get_assistant()
|
||||
assistant.resume_detection()
|
||||
self._detection_paused.clear()
|
||||
|
||||
@action
|
||||
def is_detecting(self) -> bool:
|
||||
"""
|
||||
:return: True if the asistant is detecting, False otherwise.
|
||||
"""
|
||||
assistant = self._get_assistant()
|
||||
return assistant.is_detecting()
|
||||
return not self._detection_paused.is_set()
|
||||
|
||||
def _get_tts_plugin(self):
|
||||
if not self.tts_plugin:
|
||||
return None
|
||||
|
||||
return get_plugin(self.tts_plugin)
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -1,76 +1,330 @@
|
|||
from platypush.backend.assistant.google import AssistantGoogleBackend
|
||||
from platypush.context import get_backend
|
||||
from platypush.plugins import action
|
||||
import json
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from platypush.config import Config
|
||||
from platypush.context import get_bus, get_plugin
|
||||
from platypush.message.event.assistant import (
|
||||
ConversationStartEvent,
|
||||
ConversationEndEvent,
|
||||
ConversationTimeoutEvent,
|
||||
ResponseEvent,
|
||||
NoResponseEvent,
|
||||
SpeechRecognizedEvent,
|
||||
AlarmStartedEvent,
|
||||
AlarmEndEvent,
|
||||
TimerStartedEvent,
|
||||
TimerEndEvent,
|
||||
AlertStartedEvent,
|
||||
AlertEndEvent,
|
||||
MicMutedEvent,
|
||||
MicUnmutedEvent,
|
||||
)
|
||||
from platypush.plugins import RunnablePlugin, action
|
||||
from platypush.plugins.assistant import AssistantPlugin
|
||||
|
||||
|
||||
class AssistantGooglePlugin(AssistantPlugin):
|
||||
class AssistantGooglePlugin(AssistantPlugin, RunnablePlugin):
|
||||
"""
|
||||
Google assistant plugin.
|
||||
It acts like a wrapper around the :mod:`platypush.backend.assistant.google`
|
||||
backend to programmatically control the conversation status.
|
||||
Google Assistant plugin.
|
||||
|
||||
This plugin allows you to run the Google Assistant _directly_ on your
|
||||
device. It requires you to have an audio microphone and a speaker connected
|
||||
to the device.
|
||||
|
||||
.. warning:: The Google Assistant library used by this backend has
|
||||
been deprecated by Google:
|
||||
https://developers.google.com/assistant/sdk/reference/library/python/.
|
||||
This integration still works on all of my devices, but its future
|
||||
functionality is not guaranteed - Google may decide to turn off the
|
||||
API, the library may no longer be built against new architectures and
|
||||
it's unlikely to be updated.
|
||||
|
||||
.. note:: Since the Google Assistant library hasn't been updated in several
|
||||
years, some of its dependencies are quite old and may break more recent
|
||||
Python installations. Please refer to the comments in the [manifest
|
||||
file](https://git.platypush.tech/platypush/platypush/src/branch/master/platypush/plugins/assistant/google/manifest.yaml)
|
||||
for more information on how to install the required dependencies, if
|
||||
the automated ways fail.
|
||||
"""
|
||||
|
||||
_default_credentials_files = (
|
||||
os.path.join(Config.get_workdir(), 'credentials', 'google', 'assistant.json'),
|
||||
os.path.join(
|
||||
os.path.expanduser('~/.config'), 'google-oauthlib-tool', 'credentials.json'
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
credentials_file: Optional[str] = None,
|
||||
device_model_id: str = 'Platypush',
|
||||
conversation_start_sound: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
:param credentials_file: Path to the Google OAuth credentials file.
|
||||
See
|
||||
https://developers.google.com/assistant/sdk/guides/library/python/embed/install-sample#generate_credentials
|
||||
for instructions to get your own credentials file.
|
||||
By default, it will search for the credentials file under:
|
||||
|
||||
* ``~/.config/google-oauthlib-tool/credentials.json``: default
|
||||
location supported by the Google Assistant library
|
||||
|
||||
* ``<PLATYPUSH_WORKDIR>/credentials/google/assistant.json``:
|
||||
recommended location, under the Platypush working directory.
|
||||
|
||||
:param device_model_id: The device model ID that identifies the device
|
||||
where the assistant is running (default: Platypush). It can be a
|
||||
custom string.
|
||||
|
||||
:param conversation_start_sound: If set, the assistant will play this
|
||||
audio file when it detects a speech. The sound file will be played
|
||||
on the default audio output device. If not set, the assistant won't
|
||||
play any sound when it detects a speech.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._credentials_file = credentials_file
|
||||
self.device_model_id = device_model_id
|
||||
self.credentials = None
|
||||
self._assistant = None
|
||||
self._is_muted = False
|
||||
|
||||
def _get_assistant(self) -> AssistantGoogleBackend:
|
||||
backend = get_backend('assistant.google')
|
||||
assert backend, 'The assistant.google backend is not configured.'
|
||||
return backend
|
||||
if conversation_start_sound:
|
||||
self._conversation_start_sound = os.path.abspath(
|
||||
os.path.expanduser(conversation_start_sound)
|
||||
)
|
||||
|
||||
self.logger.info('Initialized Google Assistant plugin')
|
||||
|
||||
@property
|
||||
def credentials_file(self) -> str:
|
||||
if self._credentials_file:
|
||||
return self._credentials_file
|
||||
|
||||
f = None
|
||||
for default_file in self._default_credentials_files:
|
||||
f = default_file
|
||||
if os.path.isfile(default_file):
|
||||
break
|
||||
|
||||
assert f, 'No credentials_file provided and no default file found'
|
||||
return f
|
||||
|
||||
@property
|
||||
def assistant(self):
|
||||
if not self._assistant:
|
||||
self.logger.warning('The assistant is not running')
|
||||
return
|
||||
|
||||
return self._assistant
|
||||
|
||||
def _play_conversation_start_sound(self):
|
||||
if not self._conversation_start_sound:
|
||||
return
|
||||
|
||||
audio = get_plugin('sound')
|
||||
if not audio:
|
||||
self.logger.warning(
|
||||
'Unable to play conversation start sound: sound plugin not found'
|
||||
)
|
||||
return
|
||||
|
||||
audio.play(self._conversation_start_sound)
|
||||
|
||||
def _process_event(self, event):
|
||||
from google.assistant.library.event import EventType, AlertType
|
||||
|
||||
self.logger.info('Received assistant event: %s', event)
|
||||
|
||||
if event.type == EventType.ON_CONVERSATION_TURN_STARTED:
|
||||
get_bus().post(ConversationStartEvent(assistant=self))
|
||||
self._play_conversation_start_sound()
|
||||
elif event.type == EventType.ON_CONVERSATION_TURN_FINISHED:
|
||||
if not event.args.get('with_follow_on_turn'):
|
||||
get_bus().post(ConversationEndEvent(assistant=self))
|
||||
elif event.type == EventType.ON_CONVERSATION_TURN_TIMEOUT:
|
||||
get_bus().post(ConversationTimeoutEvent(assistant=self))
|
||||
elif event.type == EventType.ON_NO_RESPONSE:
|
||||
get_bus().post(NoResponseEvent(assistant=self))
|
||||
elif (
|
||||
hasattr(EventType, 'ON_RENDER_RESPONSE')
|
||||
and event.type == EventType.ON_RENDER_RESPONSE
|
||||
):
|
||||
get_bus().post(
|
||||
ResponseEvent(assistant=self, response_text=event.args.get('text'))
|
||||
)
|
||||
tts = self._get_tts_plugin()
|
||||
|
||||
if tts and event.args.get('text'):
|
||||
self.stop_conversation()
|
||||
tts.say(text=event.args['text'], **self.tts_plugin_args)
|
||||
elif (
|
||||
hasattr(EventType, 'ON_RESPONDING_STARTED')
|
||||
and event.type == EventType.ON_RESPONDING_STARTED
|
||||
and event.args.get('is_error_response', False) is True
|
||||
):
|
||||
self.logger.warning('Assistant response error')
|
||||
elif event.type == EventType.ON_RECOGNIZING_SPEECH_FINISHED:
|
||||
phrase = event.args['text'].lower().strip()
|
||||
self.logger.info('Speech recognized: %s', phrase)
|
||||
get_bus().post(SpeechRecognizedEvent(assistant=self, phrase=phrase))
|
||||
elif event.type == EventType.ON_ALERT_STARTED:
|
||||
if event.args.get('alert_type') == AlertType.ALARM:
|
||||
get_bus().post(AlarmStartedEvent(assistant=self))
|
||||
elif event.args.get('alert_type') == AlertType.TIMER:
|
||||
get_bus().post(TimerStartedEvent(assistant=self))
|
||||
else:
|
||||
get_bus().post(AlertStartedEvent(assistant=self))
|
||||
elif event.type == EventType.ON_ALERT_FINISHED:
|
||||
if event.args.get('alert_type') == AlertType.ALARM:
|
||||
get_bus().post(AlarmEndEvent(assistant=self))
|
||||
elif event.args.get('alert_type') == AlertType.TIMER:
|
||||
get_bus().post(TimerEndEvent(assistant=self))
|
||||
else:
|
||||
get_bus().post(AlertEndEvent(assistant=self))
|
||||
elif event.type == EventType.ON_ASSISTANT_ERROR:
|
||||
if event.args.get('is_fatal'):
|
||||
raise RuntimeError(f'Fatal assistant error: {json.dumps(event.args)}')
|
||||
|
||||
self.logger.warning('Assistant error: %s', json.dumps(event.args))
|
||||
elif event.type == EventType.ON_MUTED_CHANGED:
|
||||
self._is_muted = event.args.get('is_muted')
|
||||
event = MicMutedEvent() if self._is_muted else MicUnmutedEvent()
|
||||
get_bus().post(event)
|
||||
|
||||
@action
|
||||
def start_conversation(self):
|
||||
def start_conversation(self, *_, **__):
|
||||
"""
|
||||
Programmatically start a conversation with the assistant
|
||||
"""
|
||||
assistant = self._get_assistant()
|
||||
assistant.start_conversation()
|
||||
if self.assistant:
|
||||
self.assistant.start_conversation()
|
||||
|
||||
@action
|
||||
def stop_conversation(self):
|
||||
"""
|
||||
Programmatically stop a running conversation with the assistant
|
||||
"""
|
||||
assistant = self._get_assistant()
|
||||
assistant.stop_conversation()
|
||||
if self.assistant:
|
||||
self.assistant.stop_conversation()
|
||||
|
||||
@action
|
||||
def set_mic_mute(self, muted: bool = True):
|
||||
def mute(self):
|
||||
"""
|
||||
Mute the microphone. Alias for :meth:`.set_mic_mute` with
|
||||
``muted=True``.
|
||||
"""
|
||||
return self.set_mic_mute(muted=True)
|
||||
|
||||
@action
|
||||
def unmute(self):
|
||||
"""
|
||||
Unmute the microphone. Alias for :meth:`.set_mic_mute` with
|
||||
``muted=False``.
|
||||
"""
|
||||
return self.set_mic_mute(muted=False)
|
||||
|
||||
@action
|
||||
def set_mic_mute(self, muted: bool):
|
||||
"""
|
||||
Programmatically mute/unmute the microphone.
|
||||
|
||||
:param muted: Set to True or False.
|
||||
"""
|
||||
assistant = self._get_assistant()
|
||||
assistant.set_mic_mute(muted)
|
||||
|
||||
@action
|
||||
def toggle_mic_mute(self):
|
||||
"""
|
||||
Toggle the mic mute state.
|
||||
"""
|
||||
assistant = self._get_assistant()
|
||||
is_muted = assistant.is_muted()
|
||||
self.set_mic_mute(muted=not is_muted)
|
||||
if self.assistant:
|
||||
self.assistant.set_mic_mute(muted)
|
||||
|
||||
@action
|
||||
def is_muted(self) -> bool:
|
||||
"""
|
||||
:return: True if the microphone is muted, False otherwise.
|
||||
"""
|
||||
assistant = self._get_assistant()
|
||||
return assistant.is_muted()
|
||||
return self._is_muted
|
||||
|
||||
@action
|
||||
def toggle_mic_mute(self):
|
||||
"""
|
||||
Toggle the mic mute state.
|
||||
"""
|
||||
is_muted = self.is_muted()
|
||||
self.set_mic_mute(muted=not is_muted)
|
||||
|
||||
@action
|
||||
def send_text_query(self, query: str):
|
||||
"""
|
||||
Send a text query to the assistant.
|
||||
|
||||
This is equivalent to saying something to the assistant.
|
||||
|
||||
:param query: Query to be sent.
|
||||
"""
|
||||
assistant = self._get_assistant()
|
||||
assistant.send_text_query(query)
|
||||
if self.assistant:
|
||||
self.assistant.send_text_query(query)
|
||||
|
||||
def main(self):
|
||||
import google.oauth2.credentials
|
||||
from google.assistant.library import Assistant
|
||||
|
||||
last_sleep = 0
|
||||
|
||||
while not self.should_stop():
|
||||
try:
|
||||
with open(self.credentials_file, 'r') as f:
|
||||
self.credentials = google.oauth2.credentials.Credentials(
|
||||
token=None, **json.load(f)
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(
|
||||
'Error while loading Google Assistant credentials: %s', e
|
||||
)
|
||||
self.logger.info(
|
||||
'Please follow the instructions at '
|
||||
'https://developers.google.com/assistant/sdk/guides/library/python/embed/install-sample'
|
||||
'#generate_credentials to get your own credentials file'
|
||||
)
|
||||
self.logger.exception(e)
|
||||
break
|
||||
|
||||
try:
|
||||
with Assistant(
|
||||
self.credentials, self.device_model_id
|
||||
) as self._assistant:
|
||||
for event in self._assistant.start():
|
||||
last_sleep = 0
|
||||
|
||||
if not self.is_detecting():
|
||||
self.logger.info(
|
||||
'Assistant event received but detection is currently paused'
|
||||
)
|
||||
continue
|
||||
|
||||
self._process_event(event)
|
||||
except Exception as e:
|
||||
self.logger.exception(e)
|
||||
sleep_secs = min(60, max(5, last_sleep * 2))
|
||||
self.logger.warning(
|
||||
'Restarting the assistant in %d seconds after an unrecoverable error',
|
||||
sleep_secs,
|
||||
)
|
||||
|
||||
self.wait_stop(sleep_secs)
|
||||
last_sleep = sleep_secs
|
||||
continue
|
||||
|
||||
def stop(self):
|
||||
try:
|
||||
self.stop_conversation()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
if self._assistant:
|
||||
del self._assistant
|
||||
self._assistant = None
|
||||
|
||||
super().stop()
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -1,6 +1,74 @@
|
|||
manifest:
|
||||
events: {}
|
||||
install:
|
||||
pip: []
|
||||
package: platypush.plugins.assistant.google
|
||||
type: plugin
|
||||
events:
|
||||
- platypush.message.event.assistant.AlarmEndEvent
|
||||
- platypush.message.event.assistant.AlarmStartedEvent
|
||||
- platypush.message.event.assistant.ConversationEndEvent
|
||||
- platypush.message.event.assistant.ConversationStartEvent
|
||||
- platypush.message.event.assistant.ConversationTimeoutEvent
|
||||
- platypush.message.event.assistant.MicMutedEvent
|
||||
- platypush.message.event.assistant.MicUnmutedEvent
|
||||
- platypush.message.event.assistant.NoResponseEvent
|
||||
- platypush.message.event.assistant.ResponseEvent
|
||||
- platypush.message.event.assistant.SpeechRecognizedEvent
|
||||
- platypush.message.event.assistant.TimerEndEvent
|
||||
- platypush.message.event.assistant.TimerStartedEvent
|
||||
|
||||
install:
|
||||
apk:
|
||||
- ffmpeg
|
||||
- portaudio-dev
|
||||
- py3-cachetools
|
||||
- py3-grpcio
|
||||
- py3-google-auth
|
||||
- py3-numpy
|
||||
- py3-pathlib2
|
||||
- py3-tenacity
|
||||
- py3-urllib3
|
||||
apt:
|
||||
- ffmpeg
|
||||
- portaudio19-dev
|
||||
- python3-cachetools
|
||||
- python3-grpcio
|
||||
- python3-google-auth
|
||||
- python3-monotonic
|
||||
- python3-tenacity
|
||||
- python3-urllib3
|
||||
dnf:
|
||||
- ffmpeg
|
||||
- portaudio-devel
|
||||
- python-cachetools
|
||||
- python-grpcio
|
||||
- python-google-auth
|
||||
- python-monotonic
|
||||
- python-numpy
|
||||
- python-tenacity
|
||||
- python-urllib3
|
||||
pacman:
|
||||
- ffmpeg
|
||||
- portaudio
|
||||
- python-cachetools
|
||||
- python-grpcio
|
||||
- python-google-auth
|
||||
- python-monotonic
|
||||
- python-numpy
|
||||
- python-sounddevice
|
||||
- python-tenacity
|
||||
- python-urllib3
|
||||
pip:
|
||||
- google-assistant-library
|
||||
- google-assistant-sdk[samples]
|
||||
- google-auth
|
||||
- numpy
|
||||
- sounddevice
|
||||
after:
|
||||
# Uninstall old versions of packages that break things on recent versions
|
||||
# of Python, when the new versions work just fine
|
||||
- yes | pip uninstall --break-system-packages enum34 click urllib3 requests google-auth
|
||||
# Upgrade the dependencies (back) to the latest version.
|
||||
# NOTE: Be careful when running this command on older distros that may
|
||||
# not ship the latest versions of all the packages! This is a workaround
|
||||
# caused by the fact that google-assistant-library pulls in some old
|
||||
# breaking dependencies that need to be surgically removed.
|
||||
- pip install -U --no-input --break-system-packages click urllib3 requests google-auth
|
||||
|
|
Loading…
Reference in a new issue