Add some docs and doctests to things that have 0% coverage.
[python_utils.git] / google_assistant.py
1 #!/usr/bin/env python3
2
3 # © Copyright 2021-2022, Scott Gasch
4
5 """A module to serve as a local client library around HTTP calls to
6 the Google Assistant via a local gateway.
7
8 """
9
10 import logging
11 import warnings
12 from dataclasses import dataclass
13 from typing import Optional
14
15 import requests
16 import speech_recognition as sr  # type: ignore
17
18 import config
19
20 logger = logging.getLogger(__name__)
21
22 parser = config.add_commandline_args(
23     f"Google Assistant ({__file__})",
24     "Args related to contacting the Google Assistant",
25 )
26 parser.add_argument(
27     "--google_assistant_bridge",
28     type=str,
29     default="http://kiosk.house:3000",
30     metavar="URL",
31     help="How to contact the Google Assistant bridge",
32 )
33 parser.add_argument(
34     "--google_assistant_username",
35     type=str,
36     metavar="GOOGLE_ACCOUNT",
37     default="scott.gasch",
38     help="The user account for talking to Google Assistant",
39 )
40
41
42 @dataclass
43 class GoogleResponse:
44     """A response wrapper."""
45
46     success: bool = False
47     response: str = ''
48     audio_url: str = ''
49     audio_transcription: Optional[str] = None  # None if not available.
50
51     def __repr__(self):
52         return f"""
53 success: {self.success}
54 response: {self.response}
55 audio_transcription: {self.audio_transcription}
56 audio_url: {self.audio_url}"""
57
58
59 def tell_google(cmd: str, *, recognize_speech=True) -> GoogleResponse:
60     """Alias for ask_google."""
61     return ask_google(cmd, recognize_speech=recognize_speech)
62
63
64 def ask_google(cmd: str, *, recognize_speech=True) -> GoogleResponse:
65     """Send a command string to Google via the google_assistant_bridge as the
66     user google_assistant_username and return the response.  If recognize_speech
67     is True, perform speech recognition on the audio response from Google so as
68     to translate it into text (best effort, YMMV).
69     """
70     logging.debug("Asking google: '%s'", cmd)
71     payload = {
72         "command": cmd,
73         "user": config.config['google_assistant_username'],
74     }
75     url = f"{config.config['google_assistant_bridge']}/assistant"
76     r = requests.post(url, json=payload)
77     success = False
78     response = ""
79     audio = ""
80     audio_transcription: Optional[str] = ""
81     if r.status_code == 200:
82         j = r.json()
83         logger.debug(j)
84         success = bool(j["success"])
85         response = j["response"] if success else j["error"]
86         if success:
87             logger.debug('Google request succeeded.')
88             if len(response) > 0:
89                 logger.debug("Google said: '%s'", response)
90         audio = f"{config.config['google_assistant_bridge']}{j['audio']}"
91         if recognize_speech:
92             recognizer = sr.Recognizer()
93             r = requests.get(audio)
94             if r.status_code == 200:
95                 raw = r.content
96                 speech = sr.AudioData(
97                     frame_data=raw,
98                     sample_rate=24000,
99                     sample_width=2,
100                 )
101                 try:
102                     audio_transcription = recognizer.recognize_google(
103                         speech,
104                     )
105                     logger.debug("Transcription: '%s'", audio_transcription)
106                 except sr.UnknownValueError as e:
107                     logger.exception(e)
108                     msg = 'Unable to parse Google assistant\'s response.'
109                     logger.warning(msg)
110                     warnings.warn(msg, stacklevel=3)
111                     audio_transcription = None
112         return GoogleResponse(
113             success=success,
114             response=response,
115             audio_url=audio,
116             audio_transcription=audio_transcription,
117         )
118     else:
119         message = f'HTTP request to {url} with {payload} failed; code {r.status_code}'
120         logger.error(message)
121         return GoogleResponse(
122             success=False,
123             response=message,
124             audio_url=audio,
125             audio_transcription=audio_transcription,
126         )