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
21 def _pyaudio() -> Generator[PyAudio, None, None]:
26 print('Terminating PyAudio object')
30 def _pyaudio_open_stream(p: PyAudio, *args, **kwargs) -> Generator[Stream, None, None]:
31 s = p.open(*args, **kwargs)
35 print('Closing PyAudio Stream')
39 async def _polite_websocket(ws: websockets.WebSocketClientProtocol) -> AsyncGenerator[websockets.WebSocketClientProtocol, None]:
43 print('Terminating connection')
44 await ws.send('{"eof" : 1}')
45 print(await ws.recv())
47 def SkipSource(source,seconds):
51 print("Skipping: ", seconds)
52 bufs = int((seconds)*source._rate/source._frames_per_buffer)
54 buffer = source.read(source._frames_per_buffer)
55 except KeyboardInterrupt:
60 def Silence(speaker, seconds):
61 buf = bytes(speaker._frames_per_buffer)
62 bufs = int((seconds)*speaker._rate/speaker._frames_per_buffer)
66 def PlayBack(pyaud, text, mic = None):
67 global config, last_time
69 http = urllib3.PoolManager()
71 playback_url = config["tts_url"]
72 playback_param = config["tts_param"]
74 if playback_url and text:
79 url = playback_url.format(urlencode({playback_param:text}))
81 url = playback_url+quote(text)
83 req = http.request('GET', url, preload_content=False)
84 decoder = MP3Decoder(req)
86 speaker = pyaud.open(output=True, format=paInt16, channels=decoder.num_channels, rate=decoder.sample_rate)
96 elapsed = time() - last_time
100 SkipSource(mic, elapsed + 0.5)
104 except KeyboardInterrupt:
113 def RunCommand(command, pyaud, mic = None):
117 http = urllib3.PoolManager()
119 command_url = config["command_url"]
120 reply_url = config["reply_url"]
121 command_user = config["api_user"]
122 command_pwd = config["api_pwd"]
123 api_attempts = config["api_attempts"]
128 print('Preparing command')
130 my_headers = urllib3.util.make_headers(basic_auth=command_user+':'+command_pwd)
132 my_headers = urllib3.util.make_headers()
133 my_headers['Content-Type']='text/plain'
134 my_headers['Accept']='apllication/json'
136 print('Sending command')
138 for i in range(api_attempts):
140 http.request('POST',command_url,headers=my_headers,body=command.encode('UTF-8'))
143 except Exception as e:
144 print('Exception: '+str(e))
148 print('Command sent')
152 for i in range(api_attempts):
155 my_headers = urllib3.util.make_headers(basic_auth=command_user+':'+command_pwd)
157 my_headers = urllib3.util.make_headers()
158 req=http.request('GET',reply_url,headers=my_headers).data
159 res = json.loads(req)['state'].strip()
162 if not(res == 'NULL'):
165 except KeyboardInterrupt:
167 except Exception as e:
168 print('Exception: '+str(e))
170 if res and not(res=="NULL"):
171 PlayBack(pyaud, res, mic=mic)
173 PlayBack(pyaud, "Сервер не ответил", mic=mic)
175 my_headers = urllib3.util.make_headers(basic_auth=command_user+':'+command_pwd)
177 my_headers = urllib3.util.make_headers()
178 my_headers['Content-Type']='text/plain'
179 my_headers['Accept']='apllication/json'
181 http.request('POST',command_url, headers=my_headers, body=command.encode('UTF-8'))
183 PlayBack(pyaud, "Сервер недоступен", mic=mic)
184 except KeyboardInterrupt:
186 except Exception as e:
188 print('Exception: '+str(e))
189 http.request('POST',command_url, headers=my_headers, body="")
193 async def ListenPhrase(mic, server):
194 global config,last_time, vad
196 frame = 30/1000 # 30 ms
198 sz = int(mic._rate*frame)
199 sp = int(pause/frame)
210 vd = vad.is_speech(data, mic._rate)
225 await server.send(data)
226 datatxt = await server.recv()
227 data = json.loads(datatxt)
229 phrase = data["text"]
230 confidence = min(map(lambda x: x["conf"], data["result"]))
236 return phrase, confidence
238 except KeyboardInterrupt:
240 except websockets.exceptions.ConnectionClosedError:
246 async def main_loop(uri):
248 global config, last_time
250 keyphrase = config["keyphrase"]
251 confidence_treshold = config["confidence_treshold"]
252 rec_attempts = config["rec_attempts"]
253 commands = config["commands"]
256 with ExitStack() as audio_stack:
257 p = audio_stack.enter_context(_pyaudio())
258 s = audio_stack.enter_context(_pyaudio_open_stream(p,
263 frames_per_buffer = 2000))
267 async with AsyncExitStack() as web_stack:
268 ws = await web_stack.enter_async_context(websockets.connect(uri))
269 print('Type Ctrl-C to exit')
270 phrases = [] + config["commands"]
271 phrases.append(config["keyphrase"])
272 phrases = json.dumps(phrases, ensure_ascii=False)
273 await ws.send('{"config" : { "phrase_list" : '+phrases+', "sample_rate" : 16000.0}}')
275 ws = await web_stack.enter_async_context(_polite_websocket(ws))
277 phrase, confidence = await ListenPhrase(s, ws)
279 print(phrase,confidence)
280 if phrase == keyphrase and confidence>=confidence_treshold :
281 PlayBack(p, "Я жду команду", mic=s)
284 for i in range(rec_attempts):
285 phrase, confidence = await ListenPhrase(s, ws)
287 print(phrase,confidence)
288 if confidence > confidence_treshold:
289 if (not commands) or (phrase in commands):
291 print("Command: ", phrase)
293 RunCommand(command, p, s)
296 PlayBack(p, "Не знаю такой команды: "+phrase, mic=s)
298 PlayBack(p, "Не поняла, слишком неразборчиво", mic=s)
301 PlayBack(p, "Так команду и не поняла...", mic=s)
302 except KeyboardInterrupt:
304 except Exception as e:
305 print('Exception: '+str(e))
308 def get_config(path):
310 config = configparser.ConfigParser()
314 keyphrase = config['vosk']['keyphrase']
316 print ("Обязательный параметр - ключевое слово - не задан!")
320 rec_attempts = int(config['vosk']['attempts'])
325 confidence_treshold = float(config['vosk']['confidence_treshold'])
327 confidence_treshold = 0.4
330 vosk_server = config['vosk']['server']
332 print ("Обязательный параметр - сервер распознавания - не задан!")
336 command_file=config['commands']['command_file']
337 with open(command_file) as file:
338 commands = file.read().splitlines()
343 tts_url=config['rest']['tts_url']
348 tts_param=config['rest']['tts_param']
353 api_attempts=int(config['rest']['attempts'])
358 api_user=config['rest']['api_user']
359 api_pwd=config['rest']['api_pwd']
365 command_url=config['rest']['command_url']
370 reply_url=config['rest']['reply_url']
375 vad_mode=config['vad']['agressive']
380 debug = (config['system']['debug'].lower() == "true")
385 with open(command_file) as file:
386 commands = file.read().splitlines()
389 "asr_server": vosk_server,
390 "keyphrase": keyphrase,
391 "rec_attempts": rec_attempts,
392 "confidence_treshold": confidence_treshold,
394 "tts_param": tts_param,
395 "api_attempts": api_attempts,
396 "api_user": api_user,
398 "command_url": command_url,
399 "reply_url": reply_url,
401 "commands": commands,
406 if len(sys.argv) == 2:
407 conf_file = sys.argv[1]
409 conf_file = expanduser("~")+"/.config/voicecontrol.ini"
411 config = get_config(conf_file)
413 server = config['asr_server']
415 vad = webrtcvad.Vad(config['vad_mode'])
421 loop = asyncio.get_event_loop()
422 loop.run_until_complete(
423 main_loop(f'ws://' + server))
425 except (Exception, KeyboardInterrupt) as e:
426 loop.run_until_complete(
427 loop.shutdown_asyncgens())
428 if isinstance(e, KeyboardInterrupt):
434 print('Restarting process...')