#!/usr/bin/env python3 import websockets,asyncio import sys from pyaudio import PyAudio, Stream, paInt16 from contextlib import asynccontextmanager, contextmanager, AsyncExitStack, ExitStack from typing import AsyncGenerator, Generator from urllib.parse import urlencode, quote import urllib3, base64, json import configparser from os.path import expanduser from streamp3 import MP3Decoder from time import time, sleep import webrtcvad from pprint import pprint @contextmanager def _pyaudio() -> Generator[PyAudio, None, None]: p = PyAudio() try: yield p finally: print('Terminating PyAudio object') p.terminate() @contextmanager def _pyaudio_open_stream(p: PyAudio, *args, **kwargs) -> Generator[Stream, None, None]: s = p.open(*args, **kwargs) try: yield s finally: print('Closing PyAudio Stream') s.close() @asynccontextmanager async def _polite_websocket(ws: websockets.WebSocketClientProtocol) -> AsyncGenerator[websockets.WebSocketClientProtocol, None]: try: yield ws finally: print('Terminating connection') await ws.send('{"eof" : 1}') print(await ws.recv()) def SkipSource(source,seconds): global config try: if config["debug"]: print("Skipping: ", seconds) bufs = int((seconds)*source._rate/source._frames_per_buffer) for i in range(bufs): buffer = source.read(source._frames_per_buffer) except KeyboardInterrupt: raise except: pass def PlayBack(pyaud, text, mic = None): global config, last_time http = urllib3.PoolManager() playback_url = config["tts_url"] playback_param = config["tts_param"] if playback_url and text: try: if playback_param: url = playback_url.format(urlencode({playback_param:text})) else: url = playback_url+quote(text) req = http.request('GET', url, preload_content=False) decoder = MP3Decoder(req) speaker = pyaud.open(output=True, format=paInt16, channels=decoder.num_channels, rate=decoder.sample_rate) pprint(speaker) for chunk in decoder: speaker.write(chunk) sleep(0.1) speaker.stop_stream() speaker.close() elapsed = time() - last_time last_time = time() if mic: SkipSource(mic, elapsed + 0.2) return elapsed except KeyboardInterrupt: raise except: raise else: return 0 def RunCommand(command, pyaud, mic = None): global config http = urllib3.PoolManager() command_url = config["command_url"] reply_url = config["reply_url"] command_user = config["api_user"] command_pwd = config["api_pwd"] api_attempts = config["api_attempts"] if command_url: try: if config["debug"]: print('Preparing command') if command_user: my_headers = urllib3.util.make_headers(basic_auth=command_user+':'+command_pwd) else: my_headers = urllib3.util.make_headers() my_headers['Content-Type']='text/plain' my_headers['Accept']='apllication/json' if config["debug"]: print('Sending command') sent = False for i in range(api_attempts): try: http.request('POST',command_url,headers=my_headers,body=command.encode('UTF-8')) sent = True break except Exception as e: print('Exception: '+str(e)) sleep(0.5) if sent: if config["debug"]: print('Command sent') if reply_url: sleep(0.5) res="NULL" for i in range(api_attempts): try: if command_user: my_headers = urllib3.util.make_headers(basic_auth=command_user+':'+command_pwd) else: my_headers = urllib3.util.make_headers() req=http.request('GET',reply_url,headers=my_headers).data res = json.loads(req)['state'].strip() if config["debug"]: print(res) if not(res == 'NULL'): break sleep(1) except KeyboardInterrupt: raise except Exception as e: print('Exception: '+str(e)) sleep(1) if res and not(res=="NULL"): PlayBack(pyaud, res, mic=mic) elif res=="NULL": PlayBack(pyaud, "Сервер не ответил", mic=mic) if command_user: my_headers = urllib3.util.make_headers(basic_auth=command_user+':'+command_pwd) else: my_headers = urllib3.util.make_headers() my_headers['Content-Type']='text/plain' my_headers['Accept']='apllication/json' command="" http.request('POST',command_url, headers=my_headers, body=command.encode('UTF-8')) else: PlayBack(pyaud, "Сервер недоступен", mic=mic) except KeyboardInterrupt: raise except Exception as e: try: print('Exception: '+str(e)) http.request('POST',command_url, headers=my_headers, body="") except: pass async def ListenPhrase(mic, server): global config,last_time, vad frame = 30/1000 # 30 ms pause = 2 sz = int(mic._rate*frame) sp = int(pause/frame) phrase = "" voice = False while not phrase: data = mic.read(sz) if len(data) == 0: break vd = vad.is_speech(data, mic._rate) if vd and not voice: voice = True if config["debug"]: print("+", end="") cnt = 0 if voice and not vd: cnt = cnt + 1 if cnt > sp: cnt = 0 voice = False if config["debug"]: print("-") if voice: print("*",end="") await server.send(data) datatxt = await server.recv() data = json.loads(datatxt) try: phrase = data["text"] except: pass last_time = time() return phrase async def main_loop(uri): global config, last_time keyphrase = config["keyphrase"] rec_attempts = config["rec_attempts"] commands = config["commands"] with ExitStack() as audio_stack: p = audio_stack.enter_context(_pyaudio()) s = audio_stack.enter_context(_pyaudio_open_stream(p, format = paInt16, channels = 1, rate = 16000, input = True, frames_per_buffer = 2000)) while True: try: async with AsyncExitStack() as web_stack: ws = await web_stack.enter_async_context(websockets.connect(uri)) print('Type Ctrl-C to exit') phrases = [] + config["commands"] phrases.append(config["keyphrase"]) phrases = json.dumps(phrases, ensure_ascii=False) await ws.send('{"config" : { "phrase_list" : '+phrases+', "sample_rate" : 16000.0}}') ws = await web_stack.enter_async_context(_polite_websocket(ws)) while True: phrase = await ListenPhrase(s, ws) if config["debug"]: print(phrase) if phrase == keyphrase : print("COMMAND!") PlayBack(p, "Слушаю!", mic=s) command = "" for i in range(rec_attempts): phrase = await ListenPhrase(s, ws) if config["debug"]: print(phrase) if (not commands) or (phrase in commands): if config["debug"]: print("Command: ", phrase) command = phrase RunCommand(command, p, s) break else: PlayBack(p, "Не знаю такой команды: "+phrase, mic=s) else: PlayBack(p, "Не поняла, слишком неразборчиво", mic=s) if not command: PlayBack(p, "Так команду и не поняла...", mic=s) except KeyboardInterrupt: raise except Exception as e: print('Exception: '+str(e)) pass def get_config(path): config = configparser.ConfigParser() config.read(path) try: keyphrase = config['vosk']['keyphrase'] except: print ("Обязательный параметр - ключевое слово - не задан!") raise try: rec_attempts = int(config['vosk']['attempts']) except: rec_attempts = 4 try: vosk_server = config['vosk']['server'] except: print ("Обязательный параметр - сервер распознавания - не задан!") raise try: command_file=config['commands']['command_file'] with open(command_file) as file: commands = file.read().splitlines() except: commands = None try: tts_url=config['rest']['tts_url'] except: tts_url = None try: tts_param=config['rest']['tts_param'] except: tts_param = None try: api_attempts=int(config['rest']['attempts']) except: api_attempts = 2 try: api_user=config['rest']['api_user'] api_pwd=config['rest']['api_pwd'] except: api_user = None api_pwd = None try: command_url=config['rest']['command_url'] except: command_url = None try: reply_url=config['rest']['reply_url'] except: reply_url = None try: vad_mode=config['vad']['agressive'] except: vad_mode = 3 try: debug = (config['system']['debug'].lower() == "true") except: debug = False if command_file: with open(command_file) as file: commands = file.read().splitlines() return { "asr_server": vosk_server, "keyphrase": keyphrase, "rec_attempts": rec_attempts, "tts_url": tts_url, "tts_param": tts_param, "api_attempts": api_attempts, "api_user": api_user, "api_pwd": api_pwd, "command_url": command_url, "reply_url": reply_url, "debug": debug, "commands": commands, "vad_mode": vad_mode } if len(sys.argv) == 2: conf_file = sys.argv[1] else: conf_file = expanduser("~")+"/.config/voicecontrol.ini" config = get_config(conf_file) server = config['asr_server'] vad = webrtcvad.Vad(config['vad_mode']) last_time = time() while True: try: loop = asyncio.get_event_loop() loop.run_until_complete( main_loop(f'ws://' + server)) except (Exception, KeyboardInterrupt) as e: raise loop.run_until_complete( loop.shutdown_asyncgens()) if isinstance(e, KeyboardInterrupt): loop.stop() print('Bye') exit(0) else: print(f'Oops! {e}') print('Restarting process...') sleep(10)