Ну во-первых, твоя задача меняется с написания голосового помощника под фиксированный набор команд на написание фреймворка для создания голосовых помощников. Это надо иметь ввиду, и проектировать помощника так, чтобы программисту (тебе) было удобнее его дорабатывать.
Например, возьми за образец Flask или многие либы для создания ботов, которые используют декораторы для регистрации команд. Тогда скрипт превращается в набор функций, реализующих саму логику команд, а фреймворк/библиотека занимаются нижележащими базовыми вещами типа общения с сервером.
В твоём случае, этими базовыми вещами будут:
1. Рабочий цикл бота
2. Обнаружение и распознавание речи пользователя
3. Определение, обращается ли пользователь к боту.
4. Опционально, синтез ответа пользователю.
Таким образом, можно спрятать тонкости распознавания речи от отдельных команд - пусть работают с текстом!
Вывод: в итоговом боте, помимо базовых вещей, будут отдельные функции-команды. Каждая команда будет помечена декоратором, и с его помощью будут задаваться текстовые строки, на которые она должна реагировать.
Примерно так:
@command('привет') # на какую команду реагировать?
def hello(message: str) -> typing.Optional[str]:
# параметр - полная текстовая строка, которую мы приняли
return "и тебе привет" # возвращаемое значение - None, или строка, которую нужно произнести
Это позволит легко дополнять функциональность бота, и не строить длинные цепочки if-elif-else. Теперь надо подумать, как это лучше реализовать.
Так как текстовые строки могут иметь вариации, или переменные части (например, погоду на какой день надо узнать?), то стоит использовать не поиск подстроки, а
регулярные выражения. Тогда функция бота будет выглядеть как-то так.
@command(r'погода\s+город\s+(.+)') # строка должна начинаться со слов "погода город", а всё что дальше - мы запоминаем.
def hello(match: re.Match) -> typing.Optional[str]:
# параметр - результат сопоставления принятой строки с регуляркой
city = match.group(1)
weather = get_weather_for(city) # узнаём (как-то) погоду, город выбираем по запомненной строке
# возвращаемое значение - None, или строка, которую нужно произнести
return f"Погода в городе {city}: {weather}"
Это всё хорошо, но как это реализовать? Почитай про декораторы. Если коротко, это просто функции, которые принимают другие функции на вход.
Пример (упрощённый) реализации декоратора command:
import re
# список зарегистрированных через декоратор команд
registered_commands = []
def command(regexp: str):
def decorator(command_func):
global registered_commands
reg = re.compile(regexp, re.I) # компилируем регулярку, чтобы потом она быстрее работала
item = reg, command_func
registered_commands.append(item) # добавляем регулярку и команду в список команд
return command_func # декоратор должен вернуть функцию, которую обработал
return decorator # возвращаем декоратор для использования
По сути, следующие два кода тогда будут эквивалентны:
@command('команда')
def some_func(match):
pass
# это то же самое что и ниже
decorator = command('команда') # получили функцию-декоратор
# объявили декорируемую функцию
def some_func(match):
pass
# применили декоратор к функции
some_func = decorator(some_func)
Таким образом, все функции, которые мы отдекорируем через
@command
, будут автоматически собраны в списке registered_commads. Тогда при обработке очередной команды мы сможем этот список перебрать, и найти нужную. А дальше просто. Делаешь рабочий цикл бота (код абстрактный, но идею должен передать).
while True:
voice = record_user_voice() # детектируем и записываем слова пользователя
text = speech_to_text(voice) # превращаем голос в текст
if text.startswith('имя бота'): # пользователь должен сказать имя бота, чтобы мы напрасно не реагировали
text = text[len('имя бота'):].strip() # убираем имя бота из текста
for regexp, command in registered_commands:
match = regexp.match(text) # пытаемся сопоставить текст с регуляркой
if match is not None: # успешно?
# да
try:
response = command(match) # пытаемся выполнить команду
except Exception as err: # неудачно - сообщаем об ошибке
print(err)
response = "Возникла ошибка"
if response: # если есть что сказать
say(response) # проговариваем ответ
break # так или иначе, мы нашли совпадение с командой. Дальше не ищем.
else: # этот else относится к for ... in registered_commands и сработает если не было break
say("Я не понял команду") # мы не нашли команду
Как-то так.