1
0
mirror of https://github.com/janvarev/Irene-Voice-Assistant.git synced 2025-11-26 22:50:58 +02:00
- поддержки работы с контекстом (см. справку)
- демо-игра работы с контекстом Больше-меньше и Больше-меньше альтернативная (два стиля)
- базовые реализации используют работы с контекстом
- runva_webapi.py - теперь вызовы core._update_timers делаются через периодичпеские вызовы HTTP, и через таймер. Webapi стало значительно устойчивей
- справка содержит информацию о контексте
This commit is contained in:
janvarev
2022-03-18 12:04:28 +03:00
parent b21710ea3f
commit b2d53d875a
9 changed files with 383 additions and 82 deletions

View File

@@ -26,6 +26,8 @@
**plugin_yandex_rasp.py** - расписание ближайших электричек через Яндекс.Расписания. Пример: "электричка, электрички".
Требует установки в конфиге бесплатного API-ключа для личных нужд (до 500 запросов в сутки) с https://yandex.ru/dev/rasp/raspapi/ , а также станций отправления и назначения
**plugin_gamemoreless.py** - игра Больше-меньше. Команда "игра больше меньше". Является примером работы с контекстом в движке Ирины.
---
**plugin_tts_pyttsx.py** - (оффлайн) позволяет делать TTS (Text-To-Speech, озвучку текста) через pyttsx движок. Используется по умолчанию.
@@ -61,6 +63,9 @@
**plugin_youtubesearch.py** - поиск на Ютубе. Пример: "ютуб остров сокровищ". Открывает страницу в браузере с поиском. (Inspired by @EnjiRouz)
**plugin_gamemoreless_alt.py** - альтернативная реализация игры Больше-меньше. Команда "игра меньше больше".
Является примером работы с контекстом альтернативным способом (**рекомендуется**) в движке Ирины.
**plugin_tts_silero.py** - (оффлайн) TTS через Silero. При первом запуске требует онлайна, чтобы скачать файл с нейросетью.
Требует pytorch 1.9+. На мой взгляд, работает немного медленно + не озвучивает числа, требуя их перевода в числительные + шипит. Тем не менее, очень крут!
Голос задается в конфиге.

View File

@@ -44,6 +44,64 @@
Полная информация: [PLUGINS.md](/PLUGINS.md)
### Поддержка контекста (с версии 4.0)
Обычно запуск любой команды требует префикса имени ассистента
(напр, "Ирина, погода"). Но иногда это избыточно - например, если мы пишем игру, и команды идут непрерывно.
С версии 4.0 поддерживается установление **контекста**. В контексте
движок:
* принимает фразы без префикса Ирина (имени ассистента)
* сразу передает их в функцию обработки контекста
**Как работает**
При вызове функции обработки можно вызвать функцию
```
core.context_set(new_context)
```
Контекст будет держаться 10 секунд, потом будет сброшен
(нужно, чтобы Ирина не зависала в состоянии, если пользователь ушел)
Пока контекст установлен, любой непустой вход (фраза) будет сразу
подана на вход функции контекста, после чего **контекст будет сброшен!**
Таким образом, если вы хотите продолжать взаимодействовать с контекстом,
нужно после каждого вызова функции его устанавливать повторно.
**Пример**
Для примера доступна игра "Больше-меньше" (плагин plugins/plugin_gamemoreless.py).
Запустите её с помощью команды "ирина игра больше меньше"
Также доступен второй тип задания контекста в альтернативной реализации игры
plugins_inactive/plugin_gamemoreless_alt.py.
**Альтернативный вариант рекомендуется больше, чем основной**
**Типы контекста**
Контекст может задаваться двумя способами:
1. Как функция, принимающая текстовый ввод (пример в игре больше меньше plugins/plugin_gamemoreless.py)
2. Как словарь команд внутри контекста - такой же, как при задании команд Ирины в манифесте плагина. В этом
случае движок будет пытаться распознать команду внутри контекста,
и в случае нераспознавания контекст будет переустановлен.
Пример диалога во втором варианте:
```
В: ирина игра больше меньше
О: скажи правила или начать
В: тест
О: не поняла (контекст сохранился, по-прежнему можно сказать правила)
```
Пример реализации игры Больше-меньше в альтернативном варианте есть в файле
plugins_inactive/plugin_gamemoreless_alt.py (нужно перенести в plugins_active,
а затем запустить командой "игра меньше больше")
### Сторонние плагины
Если вы хотите узнать:

View File

@@ -21,6 +21,7 @@ def start(core:VACore):
"logPolicy": "cmd", # all | cmd | none
"replyNoCommandFound": "Извини, я не поняла",
"replyNoCommandFoundInContext": "Не поняла...",
"replyOnlineRequired": "Для этой команды необходим онлайн",
"tempDir": "temp",

View File

@@ -0,0 +1,84 @@
# Игра больше меньше
# author: Vladislav Janvarev
from datetime import datetime
from vacore import VACore
import random
# функция на старте
def start(core:VACore):
manifest = { # возвращаем настройки плагина - словарь
"name": "Игра больше меньше", # имя
"version": "1.0", # версия
"require_online": False, # требует ли онлайн?
"commands": { # набор скиллов. Фразы скилла разделены | . Если найдены - вызывается функция
"игра больше меньше": play_game_start,
}
}
return manifest
questNumber = -1
tries = 0
def play_game_start(core:VACore, phrase: str): # в phrase находится остаток фразы после названия скилла,
# если юзер сказал больше
# в этом плагине не используется
core.play_voice_assistant_speech("Скажи правила или начать")
core.context_set(play_1)
def play_1(core:VACore, phrase: str):
if phrase == "правила":
core.play_voice_assistant_speech("Правила игры. Я загадываю число от одного до тридцати. "
"Ты называешь число, а я говорю, загаданное число больше названного, или меньше. "
"Твоя задача - отгадать число за пять попыток. Скажи начать для начала игры.")
core.context_set(play_1)
return
if phrase == "начать" or phrase == "скачать" or phrase == "повторить":
global questNumber, tries
questNumber = random.randint(1,30)
#print(questNumber)
tries = 0
core.play_voice_assistant_speech("Число от одного до тридцати загадано. Начинаем!")
#play_game_start(core,"")
core.context_set(play_2)
return
if phrase == "отмена":
core.say("Поняла, играть не будем")
return
core.play_voice_assistant_speech("Не поняла...")
core.context_set(play_1)
def play_2(core:VACore, phrase: str):
from utils.num_to_text_ru import num2text
for i in range(1,31):
if phrase == num2text(i):
global tries
tries += 1
if i == questNumber:
core.say("Да, ты угадал. Поздравляю с победой! Скажи повторить, если хочешь сыграть еще раз.")
core.context_set(play_1)
return
else:
txtsay = ""
if i < questNumber:
txtsay += "Больше. "
else:
txtsay += "Меньше. "
if tries >= 5:
txtsay += "Пять попыток прошло, к сожалению, ты проиграл. А я загадала число "+num2text(questNumber)
txtsay += ". Скажи повторить, если хочешь сыграть еще раз."
core.say(txtsay)
core.context_set(play_1)
return
else:
core.say(txtsay)
core.context_set(play_2)
return
core.play_voice_assistant_speech("Не поняла число, скажи еще раз!")
core.context_set(play_2)

View File

@@ -0,0 +1,91 @@
# Игра больше меньше (альтернативная реализация на меню)
# author: Vladislav Janvarev
from datetime import datetime
from vacore import VACore
import random
# функция на старте
def start(core:VACore):
manifest = { # возвращаем настройки плагина - словарь
"name": "Игра больше меньше (альтернативная реализация)", # имя
"version": "1.0", # версия
"require_online": False, # требует ли онлайн?
"commands": { # набор скиллов. Фразы скилла разделены | . Если найдены - вызывается функция
"игра меньше больше": play_game_start,
}
}
return manifest
questNumber = -1
tries = 0
def play_game_start(core:VACore, phrase: str): # в phrase находится остаток фразы после названия скилла,
# если юзер сказал больше
# в этом плагине не используется
core.play_voice_assistant_speech("Скажи правила или начать")
core.context_set(menu_main) # меню - набор фраз и правил, в конце файла
def play_cancel(core:VACore, phrase: str):
core.say("Поняла, играть не будем")
return
def play_rules(core:VACore, phrase: str):
core.play_voice_assistant_speech("Правила игры. Я загадываю число от одного до тридцати. "
"Ты называешь число, а я говорю, загаданное число больше названного, или меньше. "
"Твоя задача - отгадать число за пять попыток. Скажи начать для начала игры.")
core.context_set(menu_main)
def play_start(core:VACore, phrase: str):
global questNumber, tries
questNumber = random.randint(1,30)
#print(questNumber)
tries = 0
core.play_voice_assistant_speech("Число от одного до тридцати загадано. Начинаем!")
core.context_set(menu_in_game)
return
def play_game(core:VACore, phrase: str, i: int):
if phrase != "":
# что-то не распозналось или было добавлено. Просим повторить еще раз
core.say("Извини, не поняла число")
core.context_set(menu_in_game)
return
global tries
tries += 1
if i == questNumber:
core.say("Да, ты угадал. Поздравляю с победой! Скажи повторить, если хочешь сыграть еще раз.")
core.context_set(menu_main)
return
else:
txtsay = ""
if i < questNumber:
txtsay += "Больше. "
else:
txtsay += "Меньше. "
if tries >= 5:
txtsay += "Пять попыток прошло, к сожалению, ты проиграл. А я загадала число "+num2text(questNumber)
txtsay += ". Скажи повторить, если хочешь сыграть еще раз."
core.say(txtsay)
core.context_set(menu_main)
return
else:
core.say(txtsay)
core.context_set(menu_in_game)
return
# game menus
menu_main = {"правила":play_rules,"начать|скачать|повторить":play_start,"отмена":play_cancel}
menu_in_game = {}
from utils.num_to_text_ru import num2text
for i in range(1,31):
menu_in_game[num2text(i)] = (play_game, i)

View File

@@ -62,23 +62,7 @@ if __name__ == "__main__":
voice_input_str = record_and_recognize_audio()
if voice_input_str != "":
#print("Input: ",voice_input)
if core.logPolicy == "all":
print("Input: ",voice_input_str)
core.run_input_str(voice_input_str)
try:
voice_input = voice_input_str.split(" ")
#callname = voice_input[0]
for ind in range(len(voice_input)):
callname = voice_input[ind]
if callname in core.voiceAssNames: # найдено имя ассистента
if core.logPolicy == "cmd":
print("Input (cmd): ",voice_input_str)
command_options = " ".join([str(input_part) for input_part in voice_input[(ind+1):len(voice_input)]])
core.execute_next(command_options, None)
break
except Exception as err:
print(traceback.format_exc())
core._update_timers()

View File

@@ -11,6 +11,11 @@ from vacore import VACore
mic_blocked = False
def block_mic():
global mic_blocked
#print("Blocking microphone...")
mic_blocked = True
# ------------------- vosk ------------------
if __name__ == "__main__":
q = queue.Queue()
@@ -109,28 +114,11 @@ if __name__ == "__main__":
if voice_input_str != "":
#print("Input: ",voice_input)
if core.logPolicy == "all":
print("Input: ",voice_input_str)
try:
voice_input = voice_input_str.split(" ")
#callname = voice_input[0]
for ind in range(len(voice_input)):
callname = voice_input[ind]
if callname in core.voiceAssNames: # найдено имя ассистента
if core.logPolicy == "cmd":
print("Input (cmd): ",voice_input_str)
mic_blocked = True
command_options = " ".join([str(input_part) for input_part in voice_input[(ind+1):len(voice_input)]])
core.execute_next(command_options, None)
break
except Exception as err:
print(traceback.format_exc())
core.run_input_str(voice_input_str,block_mic)
mic_blocked = False
#print("UNBlocking microphone...")
else:
#print("2",rec.PartialResult())
pass
@@ -144,4 +132,6 @@ if __name__ == "__main__":
print('\nDone')
parser.exit(0)
except Exception as e:
parser.exit(type(e).__name__ + ': ' + str(e))
parser.exit(type(e).__name__ + ': ' + str(e))

View File

@@ -35,7 +35,7 @@ def runCmd(cmd:str,returnFormat:str):
core.remoteTTS = returnFormat
core.remoteTTSResult = ""
core.lastSay = ""
core.execute_next(cmd,None)
core.execute_next(cmd,core.context)
core.remoteTTS = tmpformat
app = FastAPI()
@@ -63,45 +63,40 @@ async def sendSimpleTxtCmd(cmd:str,returnFormat:str = "none"):
# Пример: ирина погода, раз два
@app.get("/sendRawTxt")
async def sendRawTxt(rawtxt:str,returnFormat:str = "none"):
voice_input = rawtxt.split(" ")
isFound = False
for ind in range(len(voice_input)):
callname = voice_input[ind]
if callname in core.voiceAssNames: # найдено имя ассистента
isFound = True
if core.logPolicy == "cmd":
print("Input (cmd): ",rawtxt)
command_options = " ".join([str(input_part) for input_part in voice_input[(ind+1):len(voice_input)]])
runCmd(command_options, returnFormat)
break
tmpformat = core.remoteTTS
core.remoteTTS = returnFormat
core.remoteTTSResult = ""
core.lastSay = ""
isFound = core.run_input_str(rawtxt)
core.remoteTTS = tmpformat
if isFound:
return core.remoteTTSResult
else:
return "NO_VA_NAME"
# Запускает внутреннюю процедуру проверки таймеров. Должна запускаться периодически
@app.get("/updTimers")
async def updTimers():
#core.say("аа")
#print("upd timers")
core._update_timers()
return ""
# simple threading for timer
from threading import Thread, Event
class MyThread(Thread):
def __init__(self, event):
Thread.__init__(self)
self.stopped = event
def run(self):
while not self.stopped.wait(0.5):
core._update_timers()
if __name__ != "__main__": # must run only in web
stopFlag = Event()
thread = MyThread(stopFlag)
thread.start()
# this will stop the timer
#stopFlag.set()
def core_update_timers_http(runReq=True):
from threading import Timer
if runReq:
try:
import requests
reqstr = "http://{0}:{1}/updTimers".format(webapi_options["host"],webapi_options["port"])
#print(reqstr)
r = requests.get(reqstr)
except Exception:
pass
t = Timer(2, core_update_timers_http)
t.start()
if __name__ == "__main__":
core_update_timers_http(False)
uvicorn.run("runva_webapi:app", host=webapi_options["host"], port=webapi_options["port"],
log_level=webapi_options["log_level"])

119
vacore.py
View File

@@ -2,13 +2,16 @@ import os
import traceback
import time
from threading import Timer
import sounddevice as sound_device
import soundfile as sound_file
from jaa import JaaCore
version = "3.3"
version = "4.0"
# main VACore class
class VACore(JaaCore):
def __init__(self):
@@ -45,6 +48,10 @@ class VACore(JaaCore):
self.remoteTTS = "none"
self.remoteTTSResult = None
self.context = None
self.contextTimer = None
self.contextTimerLastDuration = 0
import mpcapi.core
self.mpchc = mpcapi.core.MpcAPI()
@@ -150,10 +157,22 @@ class VACore(JaaCore):
else:
# it is function to call!
#context(self,command)
self.context_clear()
self.call_ext_func_phrase(command,context)
return
try:
# первый проход - ищем полное совпадение
for keyall in context.keys():
keys = keyall.split("|")
for key in keys:
if command == key:
rest_phrase = ""
next_context = context[keyall]
self.execute_next(rest_phrase,next_context)
return
# второй проход - ищем частичное совпадение
for keyall in context.keys():
keys = keyall.split("|")
for key in keys:
@@ -161,21 +180,19 @@ class VACore(JaaCore):
rest_phrase = command[(len(key)+1):]
next_context = context[keyall]
self.execute_next(rest_phrase,next_context)
#print(next_context)
#print(rest_phrase)
#if isinstance(next_context,dict):
#commands[key](*args)
#print
return
else:
#print("Command not found", command_name)
pass
# if not founded
self.play_voice_assistant_speech(self.plugin_options("core")["replyNoCommandFound"])
if self.context == None:
# no context
self.say(self.plugin_options("core")["replyNoCommandFound"])
else:
# in context
self.say(self.plugin_options("core")["replyNoCommandFoundInContext"])
# restart timer for context
if self.contextTimer != None:
self.context_set(self.context,self.contextTimerLastDuration)
except Exception as err:
print(traceback.format_exc())
@@ -238,3 +255,79 @@ class VACore(JaaCore):
# Wait until file is done playing
status = sound_device.wait()
# -------- raw txt running -----------------
def run_input_str(self,voice_input_str,func_before_run_cmd = None): # voice_input_str - строка распознавания голоса, разделенная пробелами
# пример: "ирина таймер пять"
if self.logPolicy == "all":
if self.context == None:
print("Input: ",voice_input_str)
else:
print("Input (in context): ",voice_input_str)
try:
voice_input = voice_input_str.split(" ")
#callname = voice_input[0]
haveRun = False
if self.context == None:
for ind in range(len(voice_input)):
callname = voice_input[ind]
if callname in self.voiceAssNames: # найдено имя ассистента
if self.logPolicy == "cmd":
print("Input (cmd): ",voice_input_str)
command_options = " ".join([str(input_part) for input_part in voice_input[(ind+1):len(voice_input)]])
# running some cmd before run cmd
if func_before_run_cmd != None:
func_before_run_cmd()
#context = self.context
#self.context_clear()
self.execute_next(command_options, None)
haveRun = True
break
else:
if self.logPolicy == "cmd":
print("Input (cmd in context): ",voice_input_str)
# running some cmd before run cmd
if func_before_run_cmd != None:
func_before_run_cmd()
self.execute_next(voice_input_str, self.context)
haveRun = True
except Exception as err:
print(traceback.format_exc())
return haveRun
# ------------ context handling functions ----------------
def context_set(self,context,duration = None):
if duration == None:
duration = 10
self.context_clear()
self.context = context
self.contextTimerLastDuration = duration
self.contextTimer = Timer(duration,self._context_clear_timer)
self.contextTimer.start()
#def _timer_context
def _context_clear_timer(self):
print("Context cleared after timeout")
self.contextTimer = None
self.context_clear()
def context_clear(self):
self.context = None
if self.contextTimer != None:
self.contextTimer.cancel()
self.contextTimer = None