18 Commits

Author SHA1 Message Date
Dmitrium12 36391ba187 add comment 2024-05-09 16:33:00 +07:00
Dmitrium12 aa1e424e8b windows fix 2024-05-09 16:23:37 +07:00
Dmitrium12 789f4f38d5 add MediaPlayerController 2024-05-09 14:06:49 +07:00
Dmitrium12 e6c7006f1f Merge pull request 'add comment in all function and class< and use .env file' (#5) from update_function_docs_fixes into master
Reviewed-on: #5
2024-05-05 10:30:17 +07:00
Dmitrium12 9d08a7eb85 add comment in all function and class< and use .env file 2024-05-05 10:28:21 +07:00
Dmitrium12 49946322bb Merge pull request 'add_home_assistant_connect' (#4) from add_home_assistant_connect into master
Reviewed-on: #4
2024-05-04 20:14:48 +07:00
Dmitrium12 aa639ffae9 add HomeAssistant get info on entity 2024-05-04 19:28:36 +07:00
Dmitrium12 3d12032942 add HomeAssistant module and use execute command 2024-05-02 12:55:27 +07:00
Dmitrium12 06b70afdce Merge pull request 'modify project structure and add download_models.py' (#3) from census_of_the_project_structure into master
Reviewed-on: #3
2024-05-01 14:15:59 +07:00
Dmitrium12 91cd9b02df modify project structure and add download_models.py 2024-05-01 13:35:00 +07:00
Dmitrium12 ba52d86754 Merge pull request 'add_noisereduce' (#2) from add_noisereduce into master
Reviewed-on: #2
2024-05-01 10:38:58 +07:00
Dmitrium12 4a0155413e micro fix 2024-04-30 08:46:16 +07:00
Dmitrium12 6a6efe8dd6 micro fix 2024-04-30 08:46:06 +07:00
Dmitrium12 08b6f95a67 add testing noisereduce 2024-04-30 08:31:33 +07:00
Dmitrium12 d2b60b53c4 add command and add its in ollama functions 2024-04-29 17:58:53 +07:00
Dmitrium12 a7abfe44b4 add linter and test ollama in python 2024-04-29 15:31:01 +07:00
Dmitrium12 3fb31e60ea .gitignore 2024-01-09 22:10:57 +07:00
Dmitrium12 c03811cb87 starting 2024-01-09 22:10:13 +07:00
30 changed files with 1319 additions and 118 deletions
+10
View File
@@ -0,0 +1,10 @@
VA_ALIAS='("джарвис",)'
VA_TBR='("скажи", "покажи", "ответь", "произнеси", "расскажиv, "сколько", "слушай")'
VOSK_MODEL_NAME='vosk-model-small-ru-0.22' # vosk-model-ru-0.42
MICROPHONE_INDEX=-1
PICOVOICE_TOKEN='token'
# home assistant
HOME_ASSISTANT_URL='http://localhost:8123/api'
HOME_ASSISTANT_TOKEN=''
+2 -1
View File
@@ -6,7 +6,8 @@ __pycache__/
*$py.class
# Custom
model_small/
data/model_small/
data/model_large/
# C extensions
*.so
-5
View File
@@ -1,5 +0,0 @@
VA_ALIAS = ('джарвис',)
VA_TBR = ('скажи', 'покажи', 'ответь', 'произнеси', 'расскажи', 'сколько', 'слушай')
MICROPHONE_INDEX = -1
PICOVOICE_TOKEN = "4xbwaZwZmSHeTiowFl5Rgqsc8CR4FKGV8YueJUlR4Zt2e1kB64IDcA=="
OPENAI_TOKEN = "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"
+6
View File
@@ -72,3 +72,9 @@ weather:
- возможен дождь сегодня?
- прогноз погоды на сегодня
- погода
home_assistant_execute:
- включи телевизор
- выключи телевизор
- начни уборку
home_assistant_get:
- тест
Executable
+15
View File
@@ -0,0 +1,15 @@
import environs
env = environs.Env()
env.read_env()
VA_ALIAS = env.str("VA_ALIAS")
VA_TBR = env.str("VA_TBR")
VOSK_MODEL_NAME = env.str("VOSK_MODEL_NAME")
MICROPHONE_INDEX = env.int("MICROPHONE_INDEX")
PICOVOICE_TOKEN = env.str("PICOVOICE_TOKEN")
# home assistant
HOME_ASSISTANT_URL = env.str("HOME_ASSISTANT_URL")
HOME_ASSISTANT_TOKEN = env.str("HOME_ASSISTANT_TOKEN")
+4
View File
@@ -0,0 +1,4 @@
пылесос:
- entity_id:vacuum.roborock_vacuum_m1s
- state:находится в
- attributes.battery_level:а его уровень зарядки
+1 -1
View File
@@ -1,4 +1,4 @@
from Jarvis import Jarvis
from modules.Jarvis import Jarvis
def main():
+97
View File
@@ -0,0 +1,97 @@
import requests
import yaml
from fuzzywuzzy import process
from requests import Response
from data import config
class HomeAssistant:
"""
Модуль home assistant для работы с его api
"""
def __init__(self):
self.url = "http://192.168.0.112:9999/api"
self.token = config.HOME_ASSISTANT_TOKEN
self.HA_CMD_LIST = yaml.safe_load(open('data/home_assistant_entities.yaml', encoding='utf8'))
def get_info(self, state: str) -> Response:
"""
Функция для получения информации о заданном entity
:param state: str - объект в home assistant информацию о котором надо узнать
:return: Response - ответ от сервера api
"""
response = requests.get(
url=f"{self.url}/states",
headers={
"Authorization": "Bearer " + self.token
}
)
for entity in response.json():
if entity["entity_id"] == state:
return entity
return response
def send_process(self, command: str = "выключи телевизор") -> bool:
"""
Функция для отправки запроса о выполнении команды к api
:param command: str - команда в виде строки
:return: bool - удачная ли отправка запроса к api
"""
response = requests.post(
url=f"{self.url}/services/conversation/process",
json={"text": command},
headers={
"Authorization": "Bearer " + self.token,
"content-type": "application/json"
},
)
if response.status_code == 200:
return True
return False
def voice_to_name(self, voice: str) -> str:
"""
Функция для неточного сравнивания входной строки голоса
и списка устройств дял которых можно узнать информацию
:param voice: str - распознанная фраза без проверки по списку
:return: str - найденный объект для получения информации
"""
words = voice.lower().split()
best_match = None
highest_score = 0
for word in words:
result, score = process.extractOne(word, self.HA_CMD_LIST.keys())
if score > highest_score:
highest_score = score
best_match = result
return best_match
def validate_info(self, name: str) -> str:
"""
Функция для получения готовой строки информации entity по его имени.
Эта строка готова для произношения
:param name: str - имя entity для нахождения информации о нём
:return: str - готовая строка для найденного по имени объекта для её произношения
"""
answer = name
entity_config = self.HA_CMD_LIST.get(name)
if entity_config:
# Создание словаря, разделяя каждый элемент конфигурации на ключ и значение
entity_details = {item.split(':')[0]: item.split(':')[1] for item in entity_config}
entity_id = entity_details.pop("entity_id", "robot")
if entity_id:
responses = self.get_info(entity_id)
for attribute_path, label in entity_details.items():
response = responses
try:
for attribute in attribute_path.split("."):
response = response[attribute]
answer += f" {label} {response}"
except KeyError:
continue
return answer
+37 -9
View File
@@ -3,27 +3,35 @@ import os
import struct
import time
import noisereduce as nr
import pvporcupine
import vosk
import yaml
from fuzzywuzzy import fuzz
from pvrecorder import PvRecorder
import config
from utils import execute_cmd, play
from data import config
from modules import HomeAssistant, MediaPlayerController
from utils import download_models, execute_cmd, play
class Jarvis:
"""
Это основной модуль голосового ассистента
"""
def __init__(self):
download_models.install_vosk_model()
self.recorder = None
self.CDIR = os.getcwd()
self.VA_CMD_LIST = yaml.safe_load(open('commands.yaml', encoding='utf8'))
self.VA_CMD_LIST = yaml.safe_load(open('data/commands.yaml', encoding='utf8'))
self.home_assistant = HomeAssistant.HomeAssistant()
self.media_player_controller = MediaPlayerController.MediaPlayerController()
self.porcupine = pvporcupine.create(
access_key=config.PICOVOICE_TOKEN,
keywords=['jarvis'],
sensitivities=[1]
)
self.kaldi_rec = vosk.KaldiRecognizer(vosk.Model("model_small"), 32000)
self.kaldi_rec = vosk.KaldiRecognizer(vosk.Model("data/model_small"), 16000)
def main(self):
self.recorder = PvRecorder(
@@ -37,7 +45,12 @@ class Jarvis:
while True:
try:
pcm = self.recorder.read()
if self.porcupine.process(pcm) >= 0:
reduced_audio = nr.reduce_noise(
y=pcm,
sr=16000,
prop_decrease=0.6
)
if self.porcupine.process(reduced_audio) >= 0:
self.recorder.stop()
self.play("greet", True)
self.recorder.start()
@@ -53,9 +66,15 @@ class Jarvis:
print(f"Unexpected {err=}, {type(err)=}")
raise
def va_respond(self, voice: str):
def va_respond(self, voice: str) -> bool:
"""
Функция предсказывает команду
:param voice: str - распознанная строка
:return: bool - распознана или нет команда
"""
print(f"Распознано: {voice}")
for x in config.VA_ALIAS + config.VA_TBR:
for x in config.VA_ALIAS:
voice = voice.replace(x, "").strip()
rc = {'cmd': '', 'percent': 0}
for c, v in self.VA_CMD_LIST.items():
@@ -64,6 +83,7 @@ class Jarvis:
if vrt > rc['percent']:
rc['cmd'] = c
rc['percent'] = vrt
rc['recognized_phrase'] = x
if len(rc['cmd'].strip()) <= 0:
return False
elif rc['percent'] < 70 or rc['cmd'] not in self.VA_CMD_LIST.keys():
@@ -71,8 +91,16 @@ class Jarvis:
time.sleep(1)
return False
else:
execute_cmd.execute_cmd(self, rc['cmd'])
execute_cmd.execute_cmd(self, rc['cmd'], rc['recognized_phrase'], voice)
return True
def play(self, phrase, wait_done=True):
def play(self, phrase: str, wait_done: bool = True):
"""
Функция для запуска голосовой команды
:param self: modules.Jarvis - объект основного модуля
:param phrase: str - фраза для запуска голосовой команды
:param wait_done: bool - нужно-ли ждать окончания фразы
:return:
"""
play.play(self, phrase, wait_done)
+90
View File
@@ -0,0 +1,90 @@
import platform
import subprocess
class MediaPlayerController:
"""
Модуль для манипуляции музыкой
"""
def __init__(self):
self.os_type = platform.system()
def play_pause(self) -> None:
"""
Запуск/остановка музыки
:return:
"""
if self.os_type == 'Windows':
self._windows_play_pause()
elif self.os_type == 'Linux':
self._linux_control("play-pause")
def next_track(self) -> None:
"""
Включает следующею композицию
:return:
"""
if self.os_type == 'Windows':
self._windows_control("next")
elif self.os_type == 'Linux':
self._linux_control("next")
def previous_track(self) -> None:
"""
Включает предыдущею композицию
:return:
"""
if self.os_type == 'Windows':
self._windows_control("previous")
elif self.os_type == 'Linux':
self._linux_control("previous")
def _windows_play_pause(self) -> None:
"""
Запуск/остановка музыки в windows
:return:
"""
import win32con
self.key_press(win32con.VK_MEDIA_PLAY_PAUSE)
def _windows_control(self, action: str) -> None:
"""
Включает предыдущею или следующею композицию в windows
:return:
"""
import win32con
if action == "next":
self.key_press(win32con.VK_MEDIA_NEXT_TRACK)
elif action == "previous":
self.key_press(win32con.VK_MEDIA_PREV_TRACK)
@staticmethod
def key_press(key_code: str) -> None:
"""
Симуляция нажатия и отпускания клавиши
:param key_code: str - какую кнопку нажать
:return:
"""
import win32api
import win32con
win32api.keybd_event(key_code, 0, 0, 0)
win32api.keybd_event(key_code, 0, win32con.KEYEVENTF_KEYUP, 0)
@staticmethod
def _linux_control(command: str) -> None:
"""
Запускает команду для linux систем
:param command: str - команда для запуска
:return:
"""
try:
subprocess.run(["playerctl", command], check=True)
except subprocess.CalledProcessError as e:
print(f"Failed to {command}: {e}")
Generated
+917 -1
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -26,6 +26,8 @@ torch = "^2.1.1+cpu"
torchaudio = "^2.1.1+cpu"
ollama = "^0.1.6"
ruff = "^0.4.2"
noisereduce = "^3.0.2"
environs = "^11.0.0"
[[tool.poetry.source]]
-67
View File
@@ -1,67 +0,0 @@
import asyncio
import re
import ollama
import yaml
def load_commands(file_path):
with open(file_path) as file:
data = yaml.safe_load(file)
answers = {}
command_mapping = {}
idx = 1
for category, items in data.items():
answers[idx] = items
command_mapping[idx] = category
idx += 1
return answers, command_mapping
async def chat(answers: dict[int, str], request: str = 'музычку пожалуйста') -> str:
answers_str = ''.join([f'{key}. {", ".join(value)}\n' for key, value in answers.items()])
messages = [
{
'role': 'system',
'content': 'Пожалуйста, просмотрите список доступных команд и '
'выберите подходящую команду, указав ее номер. '
'Вы можете выбрать одну команду или несколько команд одновременно. '
'В ответе укажите только номер или номера команд, '
'например: "1" или "1, 3, 5". Вот список доступных команд:\n' + answers_str
},
{
'role': 'user',
'content': request
}
]
async_client = ollama.AsyncClient()
response = await async_client.chat(
model='llama3:8b',
messages=messages,
options={
'temperature': 0.5,
'mirostat_tau': 100.0,
'repeat_last_n': 2,
'num_predict': 20
}
)
return response.get("message").get("content")
async def main():
answers, command_mapping = load_commands('commands.yaml')
number = None
while not number:
response_content = await chat(answers, "останови музыку и скажи погоду")
number = [
int(i)
for i in re.findall(r'\d+', response_content)
if int(i) in answers.keys()
]
command_names = [command_mapping[n] for n in number]
print(f"Выбранный номер: {command_names}")
if __name__ == '__main__':
ollama.pull("llama3:8b")
asyncio.run(main())
+37
View File
@@ -0,0 +1,37 @@
import os
import sys
from data import config
def install_vosk_model() -> None:
"""
Функция устанавливает заданную в конфигурационном файле модели
:return:
"""
try:
open('data/model_small/README')
except Exception as e:
print(e)
if sys.platform == "linux" or sys.platform == "linux2":
os.system(
f"wget https://alphacephei.com/vosk/models/{config.VOSK_MODEL_NAME}.zip"
)
os.system(f"unzip {config.VOSK_MODEL_NAME}.zip")
os.system(f"mv {config.VOSK_MODEL_NAME} data/model_small")
os.system(f"rm -rf {config.VOSK_MODEL_NAME}.zip")
elif sys.platform == "darwin":
os.system(
f"curl https://alphacephei.com/vosk/models/{config.VOSK_MODEL_NAME}.zip"
)
os.system(f"unzip {config.VOSK_MODEL_NAME}.zip")
os.system(f"mv {config.VOSK_MODEL_NAME} data/model_small")
os.system(f"rm -rf {config.VOSK_MODEL_NAME}.zip")
elif sys.platform == "win32":
os.system(
f"curl https://alphacephei.com/vosk/models/{config.VOSK_MODEL_NAME}.zip --output 1.zip"
)
os.system('powershell -command "Expand-Archive 1.zip ./"')
os.system(f"rename {config.VOSK_MODEL_NAME} data/model_small")
os.system("del /s /q 1.zip")
+24 -1
View File
@@ -1,4 +1,13 @@
def execute_cmd(self, cmd: str):
def execute_cmd(self, cmd: str, recognized_phrase: str, voice: str) -> None:
"""
Функция выполняет полученные команды
:param self: modules.Jarvis - объект основного модуля
:param cmd: str - команда которую функция должна выполнить
:param recognized_phrase: str - распознанная фраза из списка фраз
:param voice: str - распознанная фраза без проверки по списку
:return:
"""
if cmd == 'thanks':
self.play("thanks")
elif cmd == 'stupid':
@@ -7,3 +16,17 @@ def execute_cmd(self, cmd: str):
self.play("off", True)
self.porcupine.delete()
exit(0)
elif cmd == 'music_on':
self.media_player_controller.play_pause()
elif cmd == 'music_off':
self.media_player_controller.play_pause()
elif cmd == 'music_next':
self.media_player_controller.next_track()
elif cmd == 'music_previous':
self.media_player_controller.previous_track()
elif cmd == 'home_assistant_execute':
self.home_assistant.send_process(recognized_phrase)
elif cmd == 'home_assistant_get':
entity_name = self.home_assistant.voice_to_name(voice)
entity_info = self.home_assistant.validate_info(entity_name)
print(entity_info)
+10 -2
View File
@@ -3,7 +3,15 @@ import random
import simpleaudio as sa
def play(self, phrase, wait_done=True):
def play(self, phrase: str, wait_done: bool = True) -> None:
"""
Функция для запуска голосовой команды
:param self: modules.Jarvis - объект основного модуля
:param phrase: str - фраза для запуска голосовой команды
:param wait_done: bool - нужно-ли ждать окончания фразы
:return:
"""
filename = None
file_array = ["not_found", "thanks", "run", "stupid", "ready", "off"]
if phrase == "greet":
@@ -15,7 +23,7 @@ def play(self, phrase, wait_done=True):
if wait_done:
self.recorder.stop()
if filename:
wave_obj = sa.WaveObject.from_wave_file(f"{self.CDIR}/sound/{filename}")
wave_obj = sa.WaveObject.from_wave_file(f"{self.CDIR}/data/sound/{filename}")
play_obj = wave_obj.play()
if wait_done:
play_obj.wait_done()
-31
View File
@@ -1,31 +0,0 @@
import time
import sounddevice as sd
import torch
language = 'ru'
model_id = 'ru_v3'
sample_rate = 48000 # 48000
speaker = 'aidar' # aidar, baya, kseniya, xenia, random
put_accent = True
put_yo = True
device = torch.device('cpu') # cpu или gpu
text = "Хауди Хо, друзья!!!"
model, _ = torch.hub.load(repo_or_dir='snakers4/silero-models',
model='silero_tts',
language=language,
speaker=model_id)
model.to(device)
def va_speak(what: str):
audio = model.apply_tts(text=what + "..",
speaker=speaker,
sample_rate=sample_rate,
put_accent=put_accent,
put_yo=put_yo)
sd.play(audio, sample_rate * 1.05)
time.sleep((len(audio) / sample_rate) + 0.5)
sd.stop()
+67
View File
@@ -0,0 +1,67 @@
import asyncio
import re
import ollama
import yaml
def load_commands(file_path):
with open(file_path) as file:
data = yaml.safe_load(file)
answers = {}
command_mapping = {}
idx = 1
for category, items in data.items():
answers[idx] = items
command_mapping[idx] = category
idx += 1
return answers, command_mapping
async def chat(answers: dict[int, str], request: str = 'музычку пожалуйста') -> str:
answers_str = ''.join([f'{key}. {", ".join(value)}\n' for key, value in answers.items()])
messages = [
{
'role': 'system',
'content': 'Пожалуйста, просмотрите список доступных команд и '
'выберите подходящую команду, указав ее номер. '
'Вы можете выбрать одну команду или несколько команд одновременно. '
'В ответе укажите только номер или номера команд, '
'например: "1" или "1, 3, 5". Вот список доступных команд:\n' + answers_str
},
{
'role': 'user',
'content': request
}
]
async_client = ollama.AsyncClient()
response = await async_client.chat(
model='llama3:8b',
messages=messages,
options={
'temperature': 0.5,
'mirostat_tau': 100.0,
'repeat_last_n': 2,
'num_predict': 20
}
)
return response.get("message").get("content")
async def main():
answers, command_mapping = load_commands('commands.yaml')
number = None
while not number:
response_content = await chat(answers, "останови музыку и скажи погоду")
number = [
int(i)
for i in re.findall(r'\d+', response_content)
if int(i) in answers.keys()
]
return [command_mapping[n] for n in number]
if __name__ == '__main__':
ollama.pull("llama3:8b")
command_names = asyncio.run(main())
print(f"Выбранный номер: {command_names}")