Давай-ка я тебе покажу как я бы эту проблему решил. А уж осилишь или нет...
Идеи:
1. Разные команды должны быть независимы друг от друга.
2. Проверка совпадения текста с командой и выполнение самой команды - разные задачи.
3. Список команд не должен быть захардкожен.
4. Код, нужный для реализации и добавления команды, должен быть по возможности сосредоточен в одном месте.
5. Следует избегать необходимости повторять один и тот же код в обработчиках и предикатах. Это позволит упростить их.
Следствия:
1. Каждая команда должна быть отдельной подпрограммой - обработчиком команды.
2. Для каждого обработчика команды должна существовать отдельная подрограмма - предикат команды. Она проверяет, подходит ли произнесённый текст под команду.
3. Нужно хранить коллекцию (например, список - list) пар "обработчик - предикат". При работе бот будет перебирать этот список, ища подходящую пару. Должен быть механизм внесения пары в этот список (регистрация обработчика).
4. Следует предусмотреть возможность регистрации с помощью декораторов. Это удобно и позволяет разместить описание обработчика и его регистрацию рядом, неотрывно друг от друга.
5а. Основной бот должен заниматься вопросами распознавания речи, проверки наличия ключевого слова (имени бота), вызова предикатов для определения обработчика и возврата ответа пользователю (через консоль или синтез речи).
5б. Стоит предусмотреть способ автоматического конструирования простых предикатов вроде "текст начинается с указанного слова/слов".
Отсюда мы получаем следующее...
Код
import typing as t
import speech_recognition as sr
# предикат принимает текстовую строку и возвращает истину или ложь
# истина значит "я понимаю эту строку, мой обработчик с ней справится".
PredicateFunc = t.Callable[[str], bool]
# обработчик принимает объект бота и текстовую строку команды. Он не возвращает ничего.
HandlerFunc = t.Callable[['VoiceBot', str], None]
class VoiceBot:
"""Основной класс бота."""
def __init__(self, name: str):
"""Конструктор вызывается при создании экземпляра бота."""
self._name: str = name.lower() # любая команда должна начинаться с имени бота
# список известных боту обработчиков
self._handlers: t.List[t.Tuple[PredicateFunc, HandlerFunc]] = []
# тут будут другие поля, нужные для работы класса бота
# ты ведь знаешь, как работают классы в питоне?
self._recognizer = sr.Recognizer()
def say(self, text: str) -> None:
"""Обработчики могут вызвать say(), чтобы бот что-нибудь сказал или написал."""
print(text) # пока что бот просто пишет ответ в консоль
def register(self, condition: t.Union[str, t.List[str], PredicateFunc]): # принимает строку, список строк или готовую функцию-предикат
"""Этот метод используется как декоратор для регистрации обработчика.
Он может принимать как готовую функцию-предикат, так и просто строку/строки, с которых должна начинаться команда.
В этом случае он подготовит нужный предикат самостоятельно.
"""
if isinstance(condition, str): # получили строку? переделаем её в предикат
s_lower = s.lower()
def predicate(text: str) -> bool: # объявления функций могут быть вложенными
return text.lower().startswith(s_lower) # если команда начинается с этой строки
elif isinstance(condition, (list, tuple)): # получили список строк
strings_lower = [s.lower() for s in condition] # делаем список строк в нижнем регистре
def predicate(text: str) -> bool: # объявления функций могут быть вложенными
return any(text.lower().startswith(s) for s in strings_lower) # если любая из строк подошла
else: # получили готовую функцию-предикат - используем её как есть
predicate = condition
def decorator(handler: HandlerFunc): # фактический декоратор получит на вход функцию-обработчик
self._handlers.append((predicate, handler)) # запоминаем пару предикта-обработчик
return handler # декоратор должен вернуть функцию
return decorator
def _find_correct_handler(self, command: str) -> t.Union[HandlerFunc, None]:
"""Этот метод определяет, какой из известных обработчиков подойдёт к команде command."""
for predicate, handler in self._handlers:
try:
if predicate(command): # предикат сказал "да"
return handler # значит, этот обработчик и будем использовать
except Exception as err: # что-то пошло не так с предикатом
print(f'Predicate error for handler {handler!r}', err) # сообщаем об ошибке, ищем дальше
# если дошли досюда, мы не нашли подходящий обработчик
return None # вернём None как признак неудачи
def do_one_command(self):
"""Этот метод запишет и обработает одну команду."""
try:
with sr.Microphone(device_index = 1) as source:
print('Настраиваюсь.')
self._recognizer.adjust_for_ambient_noise(source, duration=0.6)
print('Слушаю...')
audio = self._recognizer.listen(source)
print('Услышал.')
query = self._recognizer.recognize_google(audio, language = 'ru-RU')
except Exception as err: # никогда не "глотай" ошибки - хотя бы выведи их в журнал ,если не знаешь, что с ними делать.
self.say('Ошибка при записи/распознавании!') # даём знать пользователю
print(err) # это сообщение об ошибке нужно будет проанализировать, чтобы понять где ошибка
return
text = query.lower()
print(f'Вы сказали: {text}')
if not text.startswith(self._name): # текст начинается не с нашего ключевого слова (имени) - игнорируем
return
text = text[len(self._name):].strip() # убираем имя из текста
handler = self._find_correct_handler(text) # ищем обработчик
if handler is None: # не нашли
self.say("Я не понимаю, что вы хотите.") # так и говорим пользователю
return
# если дошли досюда - значит нашли
try:
handler(self, text) # вызываем обработчик команды
except Exception as err: # он может потерпеть неудачу
self.say("Произошла ошибка") # так и говорим пользователю
print(err) # это сообщение об ошибке нужно будет проанализировать, чтобы понять где ошибка
А вот пример использования:
if __name__ == '__main__':
import webbrowser
bot = VoiceBot('ботва') # "Ботва" - ключевое слово (имя) бота.
# описываем и регистрируем обработчик команды
@bot.register(['открой контакт', 'открой вк']) # должен сработать на "ботва открой контакт", например
def command_vk(bot, text):
webbrowser.open('https://vk.com', new=1)
bot.say('Готово.')
# рабочий цикл бота
while True:
bot.do_one_command()
Как видишь, это очень похоже на код, который получается при использовании готовых библиотек для создания чат-ботов (текстовых). Потому что идеи, лежащие в основе, одни и те же.
Как работают декораторы?
Если коротко, то вот этот код:
@bot.register(['открой контакт', 'открой вк'])
def command_vk(bot, text):
webbrowser.open('https://vk.com', new=1)
bot.say('Готово.')
эквивалентен вот такому коду:
_decorator_func = bot.register(['открой контакт', 'открой вк']) # готовим функцию-декоратор
def command_vk(bot, text): # описываем декорируемую функцию
webbrowser.open('https://vk.com', new=1)
bot.say('Готово.')
_decorated_func = _decorator_func(command_vk) # вызываем декоратор на этой функции
command_vk = _decorated_func # заменяем декорируемую функцию на то, что вернул декоратор
Да, боты - это ни разу не просто.