3 import websockets,asyncio
5 from pyaudio import PyAudio, Stream, paInt16
6 from contextlib import asynccontextmanager, contextmanager, AsyncExitStack, ExitStack
7 from typing import AsyncGenerator, Generator
9 from urllib.parse import urlencode, quote
10 import urllib3, base64, json
13 from os.path import expanduser
14 from streamp3 import MP3Decoder
16 from time import time, sleep
20 from pprint import pprint
23 def _pyaudio() -> Generator[PyAudio, None, None]:
28 print('Terminating PyAudio object')
32 def _pyaudio_open_stream(p: PyAudio, *args, **kwargs) -> Generator[Stream, None, None]:
33 s = p.open(*args, **kwargs)
37 print('Closing PyAudio Stream')
41 async def _polite_websocket(ws: websockets.WebSocketClientProtocol) -> AsyncGenerator[websockets.WebSocketClientProtocol, None]:
45 print('Terminating connection')
46 await ws.send('{"eof" : 1}')
47 print(await ws.recv())
49 def SkipSource(source,seconds):
53 print("Skipping: ", seconds)
54 bufs = int((seconds)*source._rate/source._frames_per_buffer)
56 buffer = source.read(source._frames_per_buffer)
57 except KeyboardInterrupt:
62 def PlayBack(pyaud, text, mic = None):
63 global config, last_time
65 http = urllib3.PoolManager()
67 playback_url = config["tts_url"]
68 playback_param = config["tts_param"]
70 if playback_url and text:
75 url = playback_url.format(urlencode({playback_param:text}))
77 url = playback_url+quote(text)
79 req = http.request('GET', url, preload_content=False)
80 decoder = MP3Decoder(req)
82 speaker = pyaud.open(output=True, format=paInt16, channels=decoder.num_channels, rate=decoder.sample_rate)
92 elapsed = time() - last_time
96 SkipSource(mic, elapsed + 0.2)
100 except KeyboardInterrupt:
109 def RunCommand(command, pyaud, mic = None):
113 http = urllib3.PoolManager()
115 command_url = config["command_url"]
116 reply_url = config["reply_url"]
117 command_user = config["api_user"]
118 command_pwd = config["api_pwd"]
119 api_attempts = config["api_attempts"]
124 print('Preparing command')
126 my_headers = urllib3.util.make_headers(basic_auth=command_user+':'+command_pwd)
128 my_headers = urllib3.util.make_headers()
129 my_headers['Content-Type']='text/plain'
130 my_headers['Accept']='apllication/json'
132 print('Sending command')
134 for i in range(api_attempts):
136 http.request('POST',command_url,headers=my_headers,body=command.encode('UTF-8'))
139 except Exception as e:
140 print('Exception: '+str(e))
144 print('Command sent')
148 for i in range(api_attempts):
151 my_headers = urllib3.util.make_headers(basic_auth=command_user+':'+command_pwd)
153 my_headers = urllib3.util.make_headers()
154 req=http.request('GET',reply_url,headers=my_headers).data
155 res = json.loads(req)['state'].strip()
158 if not(res == 'NULL'):
161 except KeyboardInterrupt:
163 except Exception as e:
164 print('Exception: '+str(e))
166 if res and not(res=="NULL"):
167 PlayBack(pyaud, res, mic=mic)
169 PlayBack(pyaud, "Сервер не ответил", mic=mic)
171 my_headers = urllib3.util.make_headers(basic_auth=command_user+':'+command_pwd)
173 my_headers = urllib3.util.make_headers()
174 my_headers['Content-Type']='text/plain'
175 my_headers['Accept']='apllication/json'
177 http.request('POST',command_url, headers=my_headers, body=command.encode('UTF-8'))
179 PlayBack(pyaud, "Сервер недоступен", mic=mic)
180 except KeyboardInterrupt:
182 except Exception as e:
184 print('Exception: '+str(e))
185 http.request('POST',command_url, headers=my_headers, body="")
189 async def ListenPhrase(mic, server):
190 global config,last_time, vad
192 frame = 30/1000 # 30 ms
194 sz = int(mic._rate*frame)
195 sp = int(pause/frame)
204 vd = vad.is_speech(data, mic._rate)
219 await server.send(data)
220 datatxt = await server.recv()
221 data = json.loads(datatxt)
223 phrase = data["text"]
231 async def main_loop(uri):
233 global config, last_time
235 keyphrase = config["keyphrase"]
236 rec_attempts = config["rec_attempts"]
237 commands = config["commands"]
240 with ExitStack() as audio_stack:
241 p = audio_stack.enter_context(_pyaudio())
243 s = audio_stack.enter_context(_pyaudio_open_stream(p,
248 frames_per_buffer = 2000))
252 async with AsyncExitStack() as web_stack:
253 ws = await web_stack.enter_async_context(websockets.connect(uri))
254 print('Type Ctrl-C to exit')
255 phrases = [] + config["commands"]
256 phrases.append(config["keyphrase"])
257 phrases = json.dumps(phrases, ensure_ascii=False)
258 await ws.send('{"config" : { "phrase_list" : '+phrases+', "sample_rate" : 16000.0}}')
260 ws = await web_stack.enter_async_context(_polite_websocket(ws))
262 phrase = await ListenPhrase(s, ws)
265 if phrase == keyphrase :
267 PlayBack(p, "Слушаю!", mic=s)
270 for i in range(rec_attempts):
271 phrase = await ListenPhrase(s, ws)
274 if (not commands) or (phrase in commands):
276 print("Command: ", phrase)
278 RunCommand(command, p, s)
281 PlayBack(p, "Не знаю такой команды: "+phrase, mic=s)
283 PlayBack(p, "Не поняла, слишком неразборчиво", mic=s)
286 PlayBack(p, "Так команду и не поняла...", mic=s)
287 except KeyboardInterrupt:
289 except Exception as e:
290 print('Exception: '+str(e))
293 def get_config(path):
295 config = configparser.ConfigParser()
299 keyphrase = config['vosk']['keyphrase']
301 print ("Обязательный параметр - ключевое слово - не задан!")
305 rec_attempts = int(config['vosk']['attempts'])
310 vosk_server = config['vosk']['server']
312 print ("Обязательный параметр - сервер распознавания - не задан!")
316 command_file=config['commands']['command_file']
317 with open(command_file) as file:
318 commands = file.read().splitlines()
323 tts_url=config['rest']['tts_url']
328 tts_param=config['rest']['tts_param']
333 api_attempts=int(config['rest']['attempts'])
338 api_user=config['rest']['api_user']
339 api_pwd=config['rest']['api_pwd']
345 command_url=config['rest']['command_url']
350 reply_url=config['rest']['reply_url']
355 vad_mode=config['vad']['agressive']
360 debug = (config['system']['debug'].lower() == "true")
365 with open(command_file) as file:
366 commands = file.read().splitlines()
369 "asr_server": vosk_server,
370 "keyphrase": keyphrase,
371 "rec_attempts": rec_attempts,
373 "tts_param": tts_param,
374 "api_attempts": api_attempts,
375 "api_user": api_user,
377 "command_url": command_url,
378 "reply_url": reply_url,
380 "commands": commands,
385 if len(sys.argv) == 2:
386 conf_file = sys.argv[1]
388 conf_file = expanduser("~")+"/.config/voicecontrol.ini"
390 config = get_config(conf_file)
392 server = config['asr_server']
394 vad = webrtcvad.Vad(config['vad_mode'])
401 loop = asyncio.get_event_loop()
402 loop.run_until_complete(
403 main_loop(f'ws://' + server))
405 except (Exception, KeyboardInterrupt) as e:
407 loop.run_until_complete(
408 loop.shutdown_asyncgens())
409 if isinstance(e, KeyboardInterrupt):
415 print('Restarting process...')