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