Пишем голосового ассистента на Python
- среда, 25 ноября 2020 г. в 00:39:53
Описание умения | Работа в offline-режиме | Требуемые зависимости |
Распознавать и синтезировать речь | Поддерживается | pip install PyAudio (использование микрофона) pip install pyttsx3 (синтез речи) Для распознавания речи можно выбрать одну или взять обе:
|
Сообщать о прогнозе погоды в любой точке мира | Не поддерживается | pip install pyowm (OpenWeatherMap) |
Производить поисковый запрос в поисковой системе Google (а также открывать сами результаты данного запроса) | Не поддерживается | pip install google |
Производить поисковый запрос видео в системе YouTube | Не поддерживается | - |
Выполнять поиск определения в Wikipedia c дальнейшим прочтением первых двух предложений | Не поддерживается | pip install wikipedia-api |
Переводить фразы с изучаемого языка на родной язык пользователя и наоборот | Не поддерживается | pip install googletrans (Google Translate) |
Искать человека по имени и фамилии в социальных сетях | Не поддерживается | - |
«Подбрасывать монетку» | Поддерживается | - |
Здороваться и прощаться (после прощания работа приложения завершается) | Поддерживается | - |
Менять настройки языка распознавания и синтеза речи на ходу | Поддерживается | - |
TODO многое другое... |
import speech_recognition
if __name__ == "__main__":
# инициализация инструментов распознавания и ввода речи
recognizer = speech_recognition.Recognizer()
microphone = speech_recognition.Microphone()
while True:
# старт записи речи с последующим выводом распознанной речи
voice_input = record_and_recognize_audio()
print(voice_input)
def record_and_recognize_audio(*args: tuple):
"""
Запись и распознавание аудио
"""
with microphone:
recognized_data = ""
# регулирование уровня окружающего шума
recognizer.adjust_for_ambient_noise(microphone, duration=2)
try:
print("Listening...")
audio = recognizer.listen(microphone, 5, 5)
except speech_recognition.WaitTimeoutError:
print("Can you check if your microphone is on, please?")
return
# использование online-распознавания через Google
try:
print("Started recognition...")
recognized_data = recognizer.recognize_google(audio, language="ru").lower()
except speech_recognition.UnknownValueError:
pass
# в случае проблем с доступом в Интернет происходит выброс ошибки
except speech_recognition.RequestError:
print("Check your Internet Connection, please")
return recognized_data
На самом деле, необязательно внедрять offline-вариант, если он вам не нужен. Мне просто хотелось показать оба способа в рамках статьи, а вы уже выбирайте, исходя из своих требований к системе (например, по количеству доступных языков распознавания бесспорно лидирует Google).Теперь, внедрив offline-решение и добавив в проект нужные языковые модели, при отсутствии доступа к сети у нас автоматически будет выполняться переключение на offline-распознавание.
from vosk import Model, KaldiRecognizer # оффлайн-распознавание от Vosk
import speech_recognition # распознавание пользовательской речи (Speech-To-Text)
import wave # создание и чтение аудиофайлов формата wav
import json # работа с json-файлами и json-строками
import os # работа с файловой системой
def record_and_recognize_audio(*args: tuple):
"""
Запись и распознавание аудио
"""
with microphone:
recognized_data = ""
# регулирование уровня окружающего шума
recognizer.adjust_for_ambient_noise(microphone, duration=2)
try:
print("Listening...")
audio = recognizer.listen(microphone, 5, 5)
with open("microphone-results.wav", "wb") as file:
file.write(audio.get_wav_data())
except speech_recognition.WaitTimeoutError:
print("Can you check if your microphone is on, please?")
return
# использование online-распознавания через Google
try:
print("Started recognition...")
recognized_data = recognizer.recognize_google(audio, language="ru").lower()
except speech_recognition.UnknownValueError:
pass
# в случае проблем с доступом в Интернет происходит попытка
# использовать offline-распознавание через Vosk
except speech_recognition.RequestError:
print("Trying to use offline recognition...")
recognized_data = use_offline_recognition()
return recognized_data
def use_offline_recognition():
"""
Переключение на оффлайн-распознавание речи
:return: распознанная фраза
"""
recognized_data = ""
try:
# проверка наличия модели на нужном языке в каталоге приложения
if not os.path.exists("models/vosk-model-small-ru-0.4"):
print("Please download the model from:\n"
"https://alphacephei.com/vosk/models and unpack as 'model' in the current folder.")
exit(1)
# анализ записанного в микрофон аудио (чтобы избежать повторов фразы)
wave_audio_file = wave.open("microphone-results.wav", "rb")
model = Model("models/vosk-model-small-ru-0.4")
offline_recognizer = KaldiRecognizer(model, wave_audio_file.getframerate())
data = wave_audio_file.readframes(wave_audio_file.getnframes())
if len(data) > 0:
if offline_recognizer.AcceptWaveform(data):
recognized_data = offline_recognizer.Result()
# получение данных распознанного текста из JSON-строки
# (чтобы можно было выдать по ней ответ)
recognized_data = json.loads(recognized_data)
recognized_data = recognized_data["text"]
except:
print("Sorry, speech service is unavailable. Try again later")
return recognized_data
if __name__ == "__main__":
# инициализация инструментов распознавания и ввода речи
recognizer = speech_recognition.Recognizer()
microphone = speech_recognition.Microphone()
while True:
# старт записи речи с последующим выводом распознанной речи
# и удалением записанного в микрофон аудио
voice_input = record_and_recognize_audio()
os.remove("microphone-results.wav")
print(voice_input)
from vosk import Model, KaldiRecognizer # оффлайн-распознавание от Vosk
import speech_recognition # распознавание пользовательской речи (Speech-To-Text)
import pyttsx3 # синтез речи (Text-To-Speech)
import wave # создание и чтение аудиофайлов формата wav
import json # работа с json-файлами и json-строками
import os # работа с файловой системой
class VoiceAssistant:
"""
Настройки голосового ассистента, включающие имя, пол, язык речи
"""
name = ""
sex = ""
speech_language = ""
recognition_language = ""
def setup_assistant_voice():
"""
Установка голоса по умолчанию (индекс может меняться в
зависимости от настроек операционной системы)
"""
voices = ttsEngine.getProperty("voices")
if assistant.speech_language == "en":
assistant.recognition_language = "en-US"
if assistant.sex == "female":
# Microsoft Zira Desktop - English (United States)
ttsEngine.setProperty("voice", voices[1].id)
else:
# Microsoft David Desktop - English (United States)
ttsEngine.setProperty("voice", voices[2].id)
else:
assistant.recognition_language = "ru-RU"
# Microsoft Irina Desktop - Russian
ttsEngine.setProperty("voice", voices[0].id)
def play_voice_assistant_speech(text_to_speech):
"""
Проигрывание речи ответов голосового ассистента (без сохранения аудио)
:param text_to_speech: текст, который нужно преобразовать в речь
"""
ttsEngine.say(str(text_to_speech))
ttsEngine.runAndWait()
def record_and_recognize_audio(*args: tuple):
"""
Запись и распознавание аудио
"""
with microphone:
recognized_data = ""
# регулирование уровня окружающего шума
recognizer.adjust_for_ambient_noise(microphone, duration=2)
try:
print("Listening...")
audio = recognizer.listen(microphone, 5, 5)
with open("microphone-results.wav", "wb") as file:
file.write(audio.get_wav_data())
except speech_recognition.WaitTimeoutError:
print("Can you check if your microphone is on, please?")
return
# использование online-распознавания через Google
# (высокое качество распознавания)
try:
print("Started recognition...")
recognized_data = recognizer.recognize_google(audio, language="ru").lower()
except speech_recognition.UnknownValueError:
pass
# в случае проблем с доступом в Интернет происходит
# попытка использовать offline-распознавание через Vosk
except speech_recognition.RequestError:
print("Trying to use offline recognition...")
recognized_data = use_offline_recognition()
return recognized_data
def use_offline_recognition():
"""
Переключение на оффлайн-распознавание речи
:return: распознанная фраза
"""
recognized_data = ""
try:
# проверка наличия модели на нужном языке в каталоге приложения
if not os.path.exists("models/vosk-model-small-ru-0.4"):
print("Please download the model from:\n"
"https://alphacephei.com/vosk/models and unpack as 'model' in the current folder.")
exit(1)
# анализ записанного в микрофон аудио (чтобы избежать повторов фразы)
wave_audio_file = wave.open("microphone-results.wav", "rb")
model = Model("models/vosk-model-small-ru-0.4")
offline_recognizer = KaldiRecognizer(model, wave_audio_file.getframerate())
data = wave_audio_file.readframes(wave_audio_file.getnframes())
if len(data) > 0:
if offline_recognizer.AcceptWaveform(data):
recognized_data = offline_recognizer.Result()
# получение данных распознанного текста из JSON-строки
# (чтобы можно было выдать по ней ответ)
recognized_data = json.loads(recognized_data)
recognized_data = recognized_data["text"]
except:
print("Sorry, speech service is unavailable. Try again later")
return recognized_data
if __name__ == "__main__":
# инициализация инструментов распознавания и ввода речи
recognizer = speech_recognition.Recognizer()
microphone = speech_recognition.Microphone()
# инициализация инструмента синтеза речи
ttsEngine = pyttsx3.init()
# настройка данных голосового помощника
assistant = VoiceAssistant()
assistant.name = "Alice"
assistant.sex = "female"
assistant.speech_language = "ru"
# установка голоса по умолчанию
setup_assistant_voice()
while True:
# старт записи речи с последующим выводом распознанной речи
# и удалением записанного в микрофон аудио
voice_input = record_and_recognize_audio()
os.remove("microphone-results.wav")
print(voice_input)
# отделение комманд от дополнительной информации (аргументов)
voice_input = voice_input.split(" ")
command = voice_input[0]
if command == "привет":
play_voice_assistant_speech("Здравствуй")
config = {
"intents": {
"greeting": {
"examples": ["привет", "здравствуй", "добрый день",
"hello", "good morning"],
"responses": play_greetings
},
"farewell": {
"examples": ["пока", "до свидания", "увидимся", "до встречи",
"goodbye", "bye", "see you soon"],
"responses": play_farewell_and_quit
},
"google_search": {
"examples": ["найди в гугл",
"search on google", "google", "find on google"],
"responses": search_for_term_on_google
},
},
"failure_phrases": play_failure_phrase
}
commands = {
("hello", "hi", "morning", "привет"): play_greetings,
("bye", "goodbye", "quit", "exit", "stop", "пока"): play_farewell_and_quit,
("search", "google", "find", "найди"): search_for_term_on_google,
("video", "youtube", "watch", "видео"): search_for_video_on_youtube,
("wikipedia", "definition", "about", "определение", "википедия"): search_for_definition_on_wikipedia,
("translate", "interpretation", "translation", "перевод", "перевести", "переведи"): get_translation,
("language", "язык"): change_language,
("weather", "forecast", "погода", "прогноз"): get_weather_forecast,
}
def execute_command_with_name(command_name: str, *args: list):
"""
Выполнение заданной пользователем команды и аргументами
:param command_name: название команды
:param args: аргументы, которые будут переданы в метод
:return:
"""
for key in commands.keys():
if command_name in key:
commands[key](*args)
else:
pass # print("Command not found")
if __name__ == "__main__":
# инициализация инструментов распознавания и ввода речи
recognizer = speech_recognition.Recognizer()
microphone = speech_recognition.Microphone()
while True:
# старт записи речи с последующим выводом распознанной речи
# и удалением записанного в микрофон аудио
voice_input = record_and_recognize_audio()
os.remove("microphone-results.wav")
print(voice_input)
# отделение комманд от дополнительной информации (аргументов)
voice_input = voice_input.split(" ")
command = voice_input[0]
command_options = [str(input_part) for input_part in voice_input[1:len(voice_input)]]
execute_command_with_name(command, command_options)
def search_for_video_on_youtube(*args: tuple):
"""
Поиск видео на YouTube с автоматическим открытием ссылки на список результатов
:param args: фраза поискового запроса
"""
if not args[0]: return
search_term = " ".join(args[0])
url = "https://www.youtube.com/results?search_query=" + search_term
webbrowser.get().open(url)
# для мультиязычных голосовых ассистентов лучше создать
# отдельный класс, который будет брать перевод из JSON-файла
play_voice_assistant_speech("Here is what I found for " + search_term + "on youtube")
{
"Can you check if your microphone is on, please?": {
"ru": "Пожалуйста, проверь, что микрофон включен",
"en": "Can you check if your microphone is on, please?"
},
"What did you say again?": {
"ru": "Пожалуйста, повтори",
"en": "What did you say again?"
},
}
class Translation:
"""
Получение вшитого в приложение перевода строк для
создания мультиязычного ассистента
"""
with open("translations.json", "r", encoding="UTF-8") as file:
translations = json.load(file)
def get(self, text: str):
"""
Получение перевода строки из файла на нужный язык (по его коду)
:param text: текст, который требуется перевести
:return: вшитый в приложение перевод текста
"""
if text in self.translations:
return self.translations[text][assistant.speech_language]
else:
# в случае отсутствия перевода происходит вывод сообщения
# об этом в логах и возврат исходного текста
print(colored("Not translated phrase: {}".format(text), "red"))
return text
play_voice_assistant_speech(translator.get(
"Here is what I found for {} on Wikipedia").format(search_term))
def prepare_corpus():
"""
Подготовка модели для угадывания намерения пользователя
"""
corpus = []
target_vector = []
for intent_name, intent_data in config["intents"].items():
for example in intent_data["examples"]:
corpus.append(example)
target_vector.append(intent_name)
training_vector = vectorizer.fit_transform(corpus)
classifier_probability.fit(training_vector, target_vector)
classifier.fit(training_vector, target_vector)
def get_intent(request):
"""
Получение наиболее вероятного намерения в зависимости от запроса пользователя
:param request: запрос пользователя
:return: наиболее вероятное намерение
"""
best_intent = classifier.predict(vectorizer.transform([request]))[0]
index_of_best_intent = list(classifier_probability.classes_).index(best_intent)
probabilities = classifier_probability.predict_proba(vectorizer.transform([request]))[0]
best_intent_probability = probabilities[index_of_best_intent]
# при добавлении новых намерений стоит уменьшать этот показатель
if best_intent_probability > 0.57:
return best_intent
# подготовка корпуса для распознавания запросов пользователя с некоторой вероятностью
# (поиск похожих)
vectorizer = TfidfVectorizer(analyzer="char", ngram_range=(2, 3))
classifier_probability = LogisticRegression()
classifier = LinearSVC()
prepare_corpus()
while True:
# старт записи речи с последующим выводом распознанной речи
# и удалением записанного в микрофон аудио
voice_input = record_and_recognize_audio()
if os.path.exists("microphone-results.wav"):
os.remove("microphone-results.wav")
print(colored(voice_input, "blue"))
# отделение команд от дополнительной информации (аргументов)
if voice_input:
voice_input_parts = voice_input.split(" ")
# если было сказано одно слово - выполняем команду сразу
# без дополнительных аргументов
if len(voice_input_parts) == 1:
intent = get_intent(voice_input)
if intent:
config["intents"][intent]["responses"]()
else:
config["failure_phrases"]()
# в случае длинной фразы - выполняется поиск ключевой фразы
# и аргументов через каждое слово,
# пока не будет найдено совпадение
if len(voice_input_parts) > 1:
for guess in range(len(voice_input_parts)):
intent = get_intent((" ".join(voice_input_parts[0:guess])).strip())
if intent:
command_options = [voice_input_parts[guess:len(voice_input_parts)]]
config["intents"][intent]["responses"](*command_options)
break
if not intent and guess == len(voice_input_parts)-1:
config["failure_phrases"]()