From 792bede347a11be4ac28f8623db5cf5bfd76d76b Mon Sep 17 00:00:00 2001 From: MarkParker5 Date: Sun, 21 Feb 2021 21:06:00 +0200 Subject: [PATCH] add Response class, recursive voice assistant algorithm --- Command/Callback.py | 18 ++++- Command/Command.py | 10 +-- Command/Response.py | 6 ++ Command/__init__.py | 1 + Media/kinogo.py | 41 ++--------- QA/QA.py | 22 +++--- Raspi/gitpull.py | 22 ++---- Raspi/tv.py | 31 ++------- SmallTalk/ctime.py | 7 +- SmallTalk/hello.py | 7 +- SmartHome/main_light.py | 6 +- SmartHome/window.py | 12 +--- Zieit/myshedule.py | 6 +- archieapi/apps/api/views.py | 3 +- main.py | 4 +- modules.py | 2 +- voice_assistant.py | 135 ++++++++++++++++++++++-------------- 17 files changed, 147 insertions(+), 186 deletions(-) create mode 100644 Command/Response.py diff --git a/Command/Callback.py b/Command/Callback.py index c916afd..e6c7774 100644 --- a/Command/Callback.py +++ b/Command/Callback.py @@ -1,9 +1,12 @@ from .Command import Command +from .Response import Response import re class Callback: - def __init__(this, patterns): + def __init__(this, patterns, quiet = False, once = True): this.patterns = patterns + this.quiet = quiet + this.once = once def setStart(this, function): this.start = function @@ -14,5 +17,16 @@ class Callback: def answer(this, string): for pattern in this.patterns: if match := re.search(re.compile(Command.compilePattern(pattern)), string): - return this.start({**match.groupdict(), 'string':string}) + return this.start({**match.groupdict(), 'string':string}) return None + + @staticmethod + def background(answer = '', voice = ''): + def decorator(cmd): + def wrapper(text): + finish_event = Event() + thread = RThread(target=cmd, args=(text, finish_event)) + thread.start() + return Response(voice = voice, text = answer, thread = {'thread': thread, 'finish_event': finish_event} ) + return wrapper + return decorator diff --git a/Command/Command.py b/Command/Command.py index 76bc288..32e8136 100644 --- a/Command/Command.py +++ b/Command/Command.py @@ -252,14 +252,6 @@ class Command(ABC): finish_event = Event() thread = RThread(target=cmd, args=(text, finish_event)) thread.start() - return { - 'type': 'background', - 'text': answer, - 'voice': voice, - 'thread': { - 'thread': thread, - 'finish_event': finish_event, - } - } + return Response(voice = voice, text = answer, thread = {'thread': thread, 'finish_event': finish_event} ) return wrapper return decorator diff --git a/Command/Response.py b/Command/Response.py new file mode 100644 index 0000000..ba9df60 --- /dev/null +++ b/Command/Response.py @@ -0,0 +1,6 @@ +class Response: + def __init__(this, voice, text, callback = None, thread = None): + this.voice: str = voice + this.text: str = text + this.callback: Callback = callback + this.thread = thread diff --git a/Command/__init__.py b/Command/__init__.py index 4829b38..2e91fba 100644 --- a/Command/__init__.py +++ b/Command/__init__.py @@ -1,2 +1,3 @@ from .Command import * from .Callback import * +from .Response import * diff --git a/Media/kinogo.py b/Media/kinogo.py index b786df5..6092110 100644 --- a/Media/kinogo.py +++ b/Media/kinogo.py @@ -2,7 +2,7 @@ from .Media import * import requests from bs4 import BeautifulSoup as BS import os -from Command import Callback +from Command import Callback, Response ################################################################################ def findPage(name): query = name + ' site:kinogo.by' @@ -83,17 +83,8 @@ def film(params): else: voice = text = 'Какой фильм включить?' callback = kinogo_film_cb - return { - 'type': 'question', - 'text': text, - 'voice': voice, - 'callback': callback, - } - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice, callback = callback) + return Response(text = text, voice = voice) def start_film(params): name = params.get('text') @@ -104,12 +95,7 @@ def start_film(params): if url: startFilm(url, title) voice = text = 'Включаю' - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } - + return Response(text = text, voice = voice) def serial(params): name = params.get('text') @@ -124,17 +110,8 @@ def serial(params): else: voice = text = 'Какой сериал включить?' callback = kinogo_serial_cb - return { - 'type': 'question', - 'text': text, - 'voice': voice, - 'callback': callback, - } - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice, callback = callback) + return Response(text = text, voice = voice) def start_serial(params): name = params.get('text') @@ -145,11 +122,7 @@ def start_serial(params): if url: startSerial(url, title) voice = text = 'Включаю' - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice) kinogo_film_cb = Callback(['$text',]) kinogo_film_cb.setStart(start_film) diff --git a/QA/QA.py b/QA/QA.py index 7083941..0165bb4 100644 --- a/QA/QA.py +++ b/QA/QA.py @@ -1,5 +1,5 @@ from bs4 import BeautifulSoup as BS -from Command import Command +from Command import Command, Response import wikipedia as wiki import requests import random @@ -8,11 +8,11 @@ import re class QA(Command): def googleDictionary(this, word): - responce = requests.get(f'https://api.dictionaryapi.dev/api/v2/entries/ru/{word}') - if responce.status_code == 200: - responce = json.loads(responce.content) + response = requests.get(f'https://api.dictionaryapi.dev/api/v2/entries/ru/{word}') + if response.status_code == 200: + response = json.loads(response.content) text = '' - r = responce[0] + r = response[0] definition = r['meanings'][0]['definitions'][0] short = r['word'].lower().capitalize() + ' (' + ( r['meanings'][0]['partOfSpeech'].capitalize() if r['meanings'][0]['partOfSpeech'] != 'undefined' else 'Разговорный' ) + ') — ' + definition['definition'].lower().capitalize() + ( '. Синонимы: ' + ', '.join(definition['synonyms']) if definition['synonyms'] else '') short = short.replace(word[0].lower()+'.', word.lower()) @@ -21,7 +21,7 @@ class QA(Command): short = short.replace('потр.', 'потребляется') short = short.replace('знач.', 'значении') - for r in responce: + for r in response: text += '\n' + r['word'].lower().capitalize() + ' (' + (r['meanings'][0]['partOfSpeech'].capitalize() if r['meanings'][0]['partOfSpeech'] != 'undefined' else 'Разговорный') + ')\n' for definition in r['meanings'][0]['definitions']: text += '\t— ' + definition['definition'].lower().capitalize() @@ -46,8 +46,8 @@ class QA(Command): except: return '' def googleSearch(this, word): - responce = requests.get(f'https://www.google.ru/search?&q={word}&lr=lang_ru&lang=ru') - page = BS(responce.content, 'html.parser') + response = requests.get(f'https://www.google.ru/search?&q={word}&lr=lang_ru&lang=ru') + page = BS(response.content, 'html.parser') info = page.select('div.BNeawe>div>div.BNeawe') return info[0].get_text() if info else '' @@ -67,10 +67,6 @@ class QA(Command): try: search = this.googleSearch(query) except: search = '' voice = text = search or random.choice(['Не совсем понимаю, о чём вы.', 'Вот эта последняя фраза мне не ясна.', 'А вот это не совсем понятно.', 'Можете сказать то же самое другими словами?', 'Вот сейчас я совсем вас не понимаю.', 'Попробуйте выразить свою мысль по-другому',]) - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice) Command.QA = QA('QA', {}, []) diff --git a/Raspi/gitpull.py b/Raspi/gitpull.py index e78e122..cecb353 100644 --- a/Raspi/gitpull.py +++ b/Raspi/gitpull.py @@ -1,16 +1,12 @@ from .Raspi import * import os -from Command import Callback +from Command import Callback, Response import config ################################################################################ def reboot(params): if params['bool']: os.system('sudo reboot') - return { - 'text': 'Хорошо', - 'voice': 'Хорошо', - 'type': 'simple', - } + return Response(text = '', voice = '') reboot_cb = Callback(['$bool',]) reboot_cb.setStart(reboot) @@ -20,19 +16,11 @@ def method(params, finish_event): os.system('git -C '+config.path+' remote update') if not 'git pull' in os.popen('git -C '+config.path+' status -uno').read(): finish_event.set() - return { - 'text': 'Установлена последняя версия', - 'voice': 'Установлена последняя версия', - 'type': 'simple', - } + return Response(text = text, voice = voice) os.system('git -C '+config.path+' pull') finish_event.set() - return { - 'text': 'Обновления скачаны. Перезагрузиться?', - 'voice': 'Обновления скачаны. Перезагрузиться?', - 'type': 'question', - 'callback': reboot_cb, - } + voice = text = 'Обновления скачаны. Перезагрузиться?' + return Response(text = text, voice = voice, callback = reboot_cb) patterns = ['* обновись *', '* можешь обновиться *', '* обнови себя *', '* скачай обновлени* *', '* провер* обновлени* *'] gitpull = Raspi('git pull archie.git', [], patterns) diff --git a/Raspi/tv.py b/Raspi/tv.py index 7a307c9..d68bbac 100644 --- a/Raspi/tv.py +++ b/Raspi/tv.py @@ -1,14 +1,11 @@ from .Raspi import * +from Command import Response ################################################################################ def method(params): Raspi.hdmi_cec('on 0') Raspi.hdmi_cec('as') voice = text = '' - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice) keywords = {} patterns = ['* включи* (телевизор|экран) *'] @@ -18,11 +15,7 @@ tv_on.setStart(method) def method(params): Raspi.hdmi_cec('standby 0') voice = text = '' - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice) keywords = {} patterns = ['* (выключи|отключи)* (телевизор|экран) *'] @@ -33,11 +26,7 @@ def method(params): port = params['num'] + '0' if len(params['num']) == 1 else params['num'] Raspi.hdmi_cec(f'tx 4F:82:{port}:00') voice = text = '' - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice) keywords = {} patterns = ['* (выведи|вывести|покажи|открой|показать|открыть) * с (|провода|hdmi|кабеля|порта) * $num *'] @@ -47,11 +36,7 @@ tv_hdmi.setStart(method) def method(params): Raspi.hdmi_cec('tx 4F:82:20:00') voice = text = '' - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice) keywords = {} patterns = ['* (выведи|вывести|покажи|открой|показать|открыть) * с (ноута|ноутбука|провода|hdmi)'] @@ -61,11 +46,7 @@ tv_hdmi.setStart(method) def method(params): Raspi.hdmi_cec('as') voice = text = '' - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice) keywords = {} patterns = ['* (верни|вернуть|включи*|покажи|показать) [нормальн|стандартн|привычн]* (телевизор|экран|картинк|изображение) *'] diff --git a/SmallTalk/ctime.py b/SmallTalk/ctime.py index ee66e42..4f9e468 100644 --- a/SmallTalk/ctime.py +++ b/SmallTalk/ctime.py @@ -3,6 +3,7 @@ import datetime, time import requests from bs4 import BeautifulSoup as BS import math +from Command import Response ################################################################################ def method(params): if city := params.get('text'): @@ -85,11 +86,7 @@ def method(params): if city: text = f'Текущее время в {city}: {hours}:{minutes}' else: text = f'Текущее время: {hours}:{minutes}' - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice) keywords = { 10: ['который час', 'сколько времени'], diff --git a/SmallTalk/hello.py b/SmallTalk/hello.py index 597ffc6..4d1db03 100644 --- a/SmallTalk/hello.py +++ b/SmallTalk/hello.py @@ -1,12 +1,9 @@ from .SmallTalk import * +from Command import Response ################################################################################ def method(params): voice = text = 'Привет' - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice) patterns = ['* привет* *',] hello = SmallTalk('Hello', {}, patterns) diff --git a/SmartHome/main_light.py b/SmartHome/main_light.py index 51fa71b..6ae55f6 100644 --- a/SmartHome/main_light.py +++ b/SmartHome/main_light.py @@ -7,11 +7,7 @@ def method(params): 'cmd': 'light_on', }) voice = text = '' - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice) keywords = {} patterns = ['* (включ|выключ)* свет *'] diff --git a/SmartHome/window.py b/SmartHome/window.py index 7cbbece..9bd6669 100644 --- a/SmartHome/window.py +++ b/SmartHome/window.py @@ -8,11 +8,7 @@ def method(params): 'cmd': 'window_open', }) voice = text = 'Поднимаю роллеты' - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice) keywords = {} patterns = ['* (открыть|открой) (окно|окна) *', '* (подними|поднять) (шторы|роллеты) *'] @@ -27,11 +23,7 @@ def method(params): 'cmd': 'window_close', }) voice = text = 'Опускаю роллеты' - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice) keywords = {} patterns = ['* (закрыть|закрой) (окно|окна) *', '* (опусти|опустить) (шторы|роллеты) *'] diff --git a/Zieit/myshedule.py b/Zieit/myshedule.py index 2041c4e..de336b5 100644 --- a/Zieit/myshedule.py +++ b/Zieit/myshedule.py @@ -10,11 +10,7 @@ def nextLessonMethod(params): type = lesson['type'] voice = f'{type} по предмету {subject} в аудитории {auditory}' text = f'{subject}\n{teacher}\n{auditory}\n{type}' - return { - 'type': 'simple', - 'text': text, - 'voice': voice, - } + return Response(text = text, voice = voice) keywords = {} patterns = ['* следующ* (предмет|урок|пара)'] diff --git a/archieapi/apps/api/views.py b/archieapi/apps/api/views.py index 85e3dd8..5cfb337 100644 --- a/archieapi/apps/api/views.py +++ b/archieapi/apps/api/views.py @@ -9,6 +9,7 @@ def index(request): text = request.GET.get("text") if text == None: return HttpResponse("") cmd, params = Command.reg_find(text).values() - responce = cmd.start(params) + try: responce = cmd.start(params) + except: {'text': f'Ошибка в модуле {cmd.getName()}', 'voice': 'Произошла ошибка'} json_string = json.dumps(responce) return HttpResponse(json_string) diff --git a/main.py b/main.py index 2810e36..5e8a62b 100644 --- a/main.py +++ b/main.py @@ -13,4 +13,6 @@ for name, module in modules.items(): os.system(f'lxterminal --command="python3.8 {config.path}/{module}.py"') except: print(f'[error]\t{name} launch failed') -os.system(f'lxterminal --command="python3.8 {config.path}/manage.py runserver"') + +os.system(f'lxterminal --command="python3.8 {config.path}/manage.py runserver 192.168.0.129:8000"') +os.system(f'lxterminal --command="vlc"') diff --git a/modules.py b/modules.py index 5982a09..56051c8 100644 --- a/modules.py +++ b/modules.py @@ -1,6 +1,6 @@ import QA import SmallTalk import Media -import SmartHome +#import SmartHome import Raspi import Zieit diff --git a/voice_assistant.py b/voice_assistant.py index 2b47c39..4f206a3 100644 --- a/voice_assistant.py +++ b/voice_assistant.py @@ -9,11 +9,10 @@ import os listener = SpeechRecognition.SpeechToText() voice = Text2Speech.Engine() threads = [] +reports = [] memory = [] voids = 0 -listener.listen_noise() - if config.double_clap_activation: # check double clap from arduino microphone module def checkClap(channel): @@ -45,65 +44,95 @@ if config.double_clap_activation: GPIO.setup(12, GPIO.IN) GPIO.add_event_detect(12, GPIO.RISING, callback=checkClap) -def reply(responce): - if responce['text']: # print answer - print('Archie: '+responce['text']) - if responce['voice']: # say answer - voice.generate(responce['voice']).speak() - if responce['type'] == 'background': # add background thread to list - threads.append(responce['thread']) - def check_threads(): for thread in threads: - if thread['finish_event'].is_set(): - responce = thread['thread'].join() - reply(responce) - thread['finish_event'].clear() - del thread + if not thread['finish_event'].is_set(): continue + response = thread['thread'].join() + reply(response) + if response.callback: + if response.callback.quiet: + response.callback.start() + else: + for _ in range(3): + print('\nYou: ', end='') + speech = listener.listen() + if speech['status'] == 'ok': + print(speech['text'], '\n') + new_response = response.callback.answer(speech['text']) + reply(new_response) + break + else: + reports.append(response) + thread['finish_event'].clear() + del thread -os.system('clear') -while True: # main loop - check_threads() +def report(): + global reports + for response in reports: + if response.voice: + voice.generate(response.voice).speak() + time.sleep(2) + reports = [] + +def reply(response): + if response.text: # print answer + print('\nArchie: '+response.text) + if response.voice: # say answer + voice.generate(response.voice).speak() + if response.thread: # add background thread to stack + threads.append(response.thread) + +def recognize(callback, params): print('\nYou: ', end='') - # input speech = listener.listen() - text = speech['text'] - if speech['status'] == 'ok': - print(text) - voids = 0 - # repeat last answer - if Command.isRepeat(text): - reply(memory[0]['responce']); - continue - # trying to recognize command with context + if speech['status'] in ['error', 'void']: + return speech + text = speech['text'] + print(text, end='') + while True: + check_threads() + if not callback: break try: - cmd, params = memory[0]['cmd'].checkContext(text).values() - if memory[0].get('params'): params = {**memory[0].get('params'), **params} + if response := callback.answer(text): + reply(response) except: - cmd, params = Command.reg_find(text).values() - # execute command - responce = cmd.start(params) - # say answer - reply(responce) - # waiting answer on question - if responce['type'] == 'question': - print('\nYou: ', end='') - speech = listener.listen() - if speech['status'] == 'ok': - text = speech['text'] - print(text) - if responce := responce['callback'].answer(text): reply(responce) - # remember the command + break memory.insert(0, { 'text': text, 'cmd': cmd, - 'responce': responce, + 'response': response, }) - else: - if speech['status'] == 'error': print('Отсутсвует подключение к интернету'); - elif speech['status'] == 'void': voids += 1; - if voids >= 3: - voids = 0 - if config.double_clap_activation: - print('\nSleep (-_-)zzZZ\n') - sleep() + speech = recognize(response.callback, params) + if callback.once: break + return speech + +listener.listen_noise() +os.system('clear') + +while True: + if voids >= 3: + voids = 0 + if config.double_clap_activation: + print('\nSleep (-_-)zzZZ\n') + sleep() + print('\nYou: ', end='') + speech = listener.listen() + print(speech.get('text') or '', end='') + voids = 0 + while True: + if speech['status'] == 'error': + break + if speech['status'] == 'void': + voids += 1 + break + text = speech['text'] + cmd, params = Command.reg_find(text).values() + try: response = cmd.start(params) + except: break + reply(response) + check_threads() + report() + if response.callback: + speech = recognize(response.callback, {}) + else: + break