22 Commits

Author SHA1 Message Date
Dmitrium12 1f2f9955d6 fix weather validate_city 2024-05-10 18:58:55 +07:00
Dmitrium12 92a8d35e15 micro fix 2024-05-09 21:13:29 +07:00
Dmitrium12 daf7211394 add strapping and only standard city get data 2024-05-09 20:46:44 +07:00
Dmitrium12 ee5acf4316 Merge pull request 'add_music_functions' (#6) from add_music_functions into master
Reviewed-on: #6
2024-05-09 16:36:26 +07:00
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
32 changed files with 2765 additions and 118 deletions
+13
View File
@@ -0,0 +1,13 @@
VA_ALIAS='("джарвис",)'
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=''
# weather
WEATHER_DEFAULT_CITY='krasnoyarsk'
WEATHER_URL='https://yandex.ru/pogoda'
+2 -1
View File
@@ -6,7 +6,8 @@ __pycache__/
*$py.class *$py.class
# Custom # Custom
model_small/ data/model_small/
data/model_large/
# C extensions # C extensions
*.so *.so
-5
View File
@@ -1,5 +0,0 @@
VA_ALIAS = ('джарвис',)
VA_TBR = ('скажи', 'покажи', 'ответь', 'произнеси', 'расскажи', 'сколько', 'слушай')
MICROPHONE_INDEX = -1
PICOVOICE_TOKEN = "4xbwaZwZmSHeTiowFl5Rgqsc8CR4FKGV8YueJUlR4Zt2e1kB64IDcA=="
OPENAI_TOKEN = "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"
+8
View File
@@ -46,6 +46,7 @@ music_on:
- хочу послушать музыку - хочу послушать музыку
- запусти плейлист - запусти плейлист
music_off: music_off:
- пауза
- выключи музыку - выключи музыку
- остановить музыку - остановить музыку
- пауза музыки - пауза музыки
@@ -72,3 +73,10 @@ weather:
- возможен дождь сегодня? - возможен дождь сегодня?
- прогноз погоды на сегодня - прогноз погоды на сегодня
- погода - погода
- скажи погоду
home_assistant_execute:
- включи телевизор
- выключи телевизор
- начни уборку
home_assistant_get:
- тест
Executable
+18
View File
@@ -0,0 +1,18 @@
import environs
import ast
env = environs.Env()
env.read_env()
VA_ALIAS = ast.literal_eval(env.str("VA_ALIAS"))
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")
# weather
WEATHER_DEFAULT_CITY = env.str("WEATHER_DEFAULT_CITY")
WEATHER_URL = env.str("WEATHER_URL")
+4
View File
@@ -0,0 +1,4 @@
пылесос:
- entity_id:vacuum.roborock_vacuum_m1s
- state:находится в
- attributes.battery_level:а его уровень зарядки
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
from Jarvis import Jarvis from modules.Jarvis import Jarvis
def main(): 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
+38 -9
View File
@@ -3,27 +3,36 @@ import os
import struct import struct
import time import time
import noisereduce as nr
import pvporcupine import pvporcupine
import vosk import vosk
import yaml import yaml
from fuzzywuzzy import fuzz from fuzzywuzzy import fuzz
from pvrecorder import PvRecorder from pvrecorder import PvRecorder
import config from data import config
from utils import execute_cmd, play from modules import HomeAssistant, MediaPlayerController, Weather
from utils import download_models, execute_cmd, play
class Jarvis: class Jarvis:
"""
Это основной модуль голосового ассистента
"""
def __init__(self): def __init__(self):
download_models.install_vosk_model()
self.recorder = None self.recorder = None
self.CDIR = os.getcwd() 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.weather = Weather.Weather()
self.porcupine = pvporcupine.create( self.porcupine = pvporcupine.create(
access_key=config.PICOVOICE_TOKEN, access_key=config.PICOVOICE_TOKEN,
keywords=['jarvis'], keywords=['jarvis'],
sensitivities=[1] 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): def main(self):
self.recorder = PvRecorder( self.recorder = PvRecorder(
@@ -37,7 +46,12 @@ class Jarvis:
while True: while True:
try: try:
pcm = self.recorder.read() 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.recorder.stop()
self.play("greet", True) self.play("greet", True)
self.recorder.start() self.recorder.start()
@@ -53,9 +67,15 @@ class Jarvis:
print(f"Unexpected {err=}, {type(err)=}") print(f"Unexpected {err=}, {type(err)=}")
raise raise
def va_respond(self, voice: str): def va_respond(self, voice: str) -> bool:
"""
Функция предсказывает команду
:param voice: str - распознанная строка
:return: bool - распознана или нет команда
"""
print(f"Распознано: {voice}") print(f"Распознано: {voice}")
for x in config.VA_ALIAS + config.VA_TBR: for x in config.VA_ALIAS:
voice = voice.replace(x, "").strip() voice = voice.replace(x, "").strip()
rc = {'cmd': '', 'percent': 0} rc = {'cmd': '', 'percent': 0}
for c, v in self.VA_CMD_LIST.items(): for c, v in self.VA_CMD_LIST.items():
@@ -64,6 +84,7 @@ class Jarvis:
if vrt > rc['percent']: if vrt > rc['percent']:
rc['cmd'] = c rc['cmd'] = c
rc['percent'] = vrt rc['percent'] = vrt
rc['recognized_phrase'] = x
if len(rc['cmd'].strip()) <= 0: if len(rc['cmd'].strip()) <= 0:
return False return False
elif rc['percent'] < 70 or rc['cmd'] not in self.VA_CMD_LIST.keys(): elif rc['percent'] < 70 or rc['cmd'] not in self.VA_CMD_LIST.keys():
@@ -71,8 +92,16 @@ class Jarvis:
time.sleep(1) time.sleep(1)
return False return False
else: else:
execute_cmd.execute_cmd(self, rc['cmd']) execute_cmd.execute_cmd(self, rc['cmd'], rc['recognized_phrase'], voice)
return True 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) 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}")
+50
View File
@@ -0,0 +1,50 @@
import json
from bs4 import BeautifulSoup
from curl_cffi import requests
from fuzzywuzzy import fuzz
from data.config import WEATHER_DEFAULT_CITY, WEATHER_URL
class Weather:
def __init__(self):
self.default_city = WEATHER_DEFAULT_CITY
self.url = WEATHER_URL
def get_info(self, city: str) -> str:
try:
response = requests.get(f"{self.url}/{city}", impersonate="chrome110")
soup = BeautifulSoup(response.text, "html.parser")
card = soup.find(
"div",
class_=["fact", "fact_prec_rain-low", "card", "card_size_big"]
)
info = card.find(
"div",
class_=["fact__temp-wrap"]
)
temp = info.find(
"span",
class_=["temp__value", "temp__value_with-unit"]
).text
weather = info.find(
"div",
class_=["link__condition", "day-anchor i-bem"]
).text.lower()
return f"За окном {temp}, {weather}"
except AttributeError:
return self.get_info(self.default_city)
def validate_city(self, voice: str) -> str:
validate_voice = voice.split(" ")[-1]
rc = {'cmd': '', 'percent': 0}
data = json.load(open("data/weather_city.json"))
for ru, en in data.items():
vrt = fuzz.ratio(validate_voice.lower(), ru.lower())
if vrt > rc['percent']:
rc['cmd'] = en.lower()
rc['percent'] = vrt
if rc['percent'] > 80:
return rc['cmd']
return self.default_city
Generated
+1096 -1
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -26,6 +26,12 @@ torch = "^2.1.1+cpu"
torchaudio = "^2.1.1+cpu" torchaudio = "^2.1.1+cpu"
ollama = "^0.1.6" ollama = "^0.1.6"
ruff = "^0.4.2" ruff = "^0.4.2"
noisereduce = "^3.0.2"
environs = "^11.0.0"
bs4 = "^0.0.2"
fake-useragent = "^1.5.1"
curl-cffi = "^0.6.3"
pandas = "^2.2.2"
[[tool.poetry.source]] [[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")
+36 -1
View File
@@ -1,4 +1,21 @@
def execute_cmd(self, cmd: str): from typing import TYPE_CHECKING
if TYPE_CHECKING:
from modules.Jarvis import Jarvis
else:
Jarvis = None
def execute_cmd(self: Jarvis, 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': if cmd == 'thanks':
self.play("thanks") self.play("thanks")
elif cmd == 'stupid': elif cmd == 'stupid':
@@ -7,3 +24,21 @@ def execute_cmd(self, cmd: str):
self.play("off", True) self.play("off", True)
self.porcupine.delete() self.porcupine.delete()
exit(0) 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)
elif cmd == 'weather':
city = self.weather.validate_city(voice)
city_info = self.weather.get_info(city)
print(city_info)
+10 -2
View File
@@ -3,7 +3,15 @@ import random
import simpleaudio as sa 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 filename = None
file_array = ["not_found", "thanks", "run", "stupid", "ready", "off"] file_array = ["not_found", "thanks", "run", "stupid", "ready", "off"]
if phrase == "greet": if phrase == "greet":
@@ -15,7 +23,7 @@ def play(self, phrase, wait_done=True):
if wait_done: if wait_done:
self.recorder.stop() self.recorder.stop()
if filename: 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() play_obj = wave_obj.play()
if wait_done: if wait_done:
play_obj.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}")