├── .gitignore ├── requirements.txt ├── LICENSE ├── translations.json ├── README.md ├── app.py └── app_nlu_version.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | __pycache__/ 3 | 4 | models/* 5 | whl/* 6 | 7 | **/*.mpeg 8 | **/*.mp3 9 | **/*.whl 10 | **/*.google-cookie 11 | **/*.env 12 | **/*.wav -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyAudio~=0.2.11 2 | google~=3.0.0 3 | SpeechRecognition~=3.8.1 4 | wikipedia-api~=0.5.4 5 | googletrans~=3.0.0 6 | pyttsx3~=2.90 7 | vosk~=0.3.7 8 | pyowm~=3.1.1 9 | termcolor~=1.1.0 10 | python-dotenv~=0.14.0 11 | scikit-learn -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 EnjiRouz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /translations.json: -------------------------------------------------------------------------------- 1 | { 2 | "Can you check if your microphone is on, please?": { 3 | "ru": "Пожалуйста, проверь, что микрофон включен", 4 | "en": "Can you check if your microphone is on, please?" 5 | }, 6 | "What did you say again?": { 7 | "ru": "Пожалуйста, повтори", 8 | "en": "What did you say again?" 9 | }, 10 | "Seems like we have a trouble. See logs for more information": { 11 | "ru": "Что-то пошло не так. Посмотри логи", 12 | "en": "Seems like we have a trouble. See logs for more information" 13 | }, 14 | "Hello, {}! How can I help you today?": { 15 | "ru": "Привет, {}! Чем могу помочь?", 16 | "en": "Hello, {}! How can I help you today?" 17 | }, 18 | "Good day to you {}! How can I help you today?": { 19 | "ru": "Здравствуй, {}! Какой план на сегодня?", 20 | "en": "Good day to you {}! How can I help you today?" 21 | }, 22 | "Goodbye, {}! Have a nice day!": { 23 | "ru": "До связи, {}! Хорошего тебе дня!", 24 | "en": "Goodbye, {}! Have a nice day!" 25 | }, 26 | "See you soon, {}!": { 27 | "ru": "До скорого, {}!", 28 | "en": "See you soon, {}!" 29 | }, 30 | "Here is what I found for {} on google": { 31 | "ru": "Вот, что умне удалось найти по запросу {} в Гугл", 32 | "en": "Here is what I found for {} on google" 33 | }, 34 | "Here is what I found for {} on youtube": { 35 | "ru": "Вот, что умне удалось найти по запросу {} на Ютьюб", 36 | "en": "Here is what I found for {} on youtube" 37 | }, 38 | "Here is what I found for {} on Wikipedia": { 39 | "ru": "Вот, что умне удалось найти по запросу {} в Википедии", 40 | "en": "Here is what I found for {} on Wikipedia" 41 | }, 42 | "Can't find {} on Wikipedia. But here is what I found on google": { 43 | "ru": "Не могу найти в Википедии ничего по запросу {}. Но вот, что мне удалось найти в Гугл", 44 | "en": "Can't find {} on Wikipedia. But here is what I found on google" 45 | }, 46 | "It is {0} in {1}": { 47 | "ru": "Сейчас {0} в городе {1}", 48 | "en": "It is {0} in {1}" 49 | }, 50 | "The temperature is {} degrees Celsius": { 51 | "ru": "Температура {} градусов Цельсия", 52 | "en": "The temperature is {} degrees Celsius" 53 | }, 54 | "The wind speed is {} meters per second": { 55 | "ru": "Скорость ветра {} метров в секунду", 56 | "en": "The wind speed is {} meters per second" 57 | }, 58 | "The pressure is {} mm Hg": { 59 | "ru": "Давление {} миллиметров ртутного столба", 60 | "en": "The pressure is {} mm Hg" 61 | }, 62 | "Here is what I found for {} on social nets": { 63 | "ru": "Вот, что мне удалось найти по запросу {} в социальных сетях", 64 | "en": "Here is what I found for {} on social nets" 65 | }, 66 | "won": { 67 | "ru": "победили", 68 | "en": "won" 69 | }, 70 | "Tails": { 71 | "ru": "Решки", 72 | "en": "Tails" 73 | }, 74 | "Heads": { 75 | "ru": "Орлы", 76 | "en": "Heads" 77 | }, 78 | "Can you repeat, please?": { 79 | "ru": "Можешь повторить?", 80 | "en": "Can you repeat, please?" 81 | }, 82 | "": { 83 | "ru": "", 84 | "en": "" 85 | }, 86 | "": { 87 | "ru": "", 88 | "en": " " 89 | }, 90 | "": { 91 | "ru": "", 92 | "en": " " 93 | }, 94 | "": { 95 | "ru": "", 96 | "en": " " 97 | }, 98 | "": { 99 | "ru": "", 100 | "en": " " 101 | }, 102 | "": { 103 | "ru": "", 104 | "en": " " 105 | }, 106 | "": { 107 | "ru": "", 108 | "en": " " 109 | }, 110 | "": { 111 | "ru": "", 112 | "en": " " 113 | }, 114 | "": { 115 | "ru": "", 116 | "en": " " 117 | }, 118 | "": { 119 | "ru": "", 120 | "en": " " 121 | }, 122 | "": { 123 | "ru": "", 124 | "en": " " 125 | }, 126 | "": { 127 | "ru": "", 128 | "en": " " 129 | }, 130 | "": { 131 | "ru": "", 132 | "en": " " 133 | } 134 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Voice Assistant Python App for Windows, Linux & MacOS 2 | 3 | ### Возможности приложения 4 | Данный проект голосового ассистента на Python 3 для Windows и Linux умеет: 5 | * распознавать и синтезировать речь в offline-режиме (без доступа к Интернету); 6 | * сообщать о прогнозе погоды в любой точке мира; 7 | * производить поисковый запрос в поисковой системе Google 8 | (а также открывать список результатов и сами результаты данного запроса); 9 | * производить поисковый запрос видео в системе YouTube и открывать список результатов данного запроса; 10 | * выполнять поиск определения в Wikipedia c дальнейшим прочтением первых двух предложений; 11 | * переводить с изучаемого языка на родной язык пользователя (с учетом особенностей воспроизведения речи); 12 | * искать человека по имени и фамилии в соцсетях ВКонтакте и Facebook; 13 | * "подбрасывать монетку"; 14 | * воспроизводить случайное приветствие; 15 | * воспроизводить случайное прощание с последующим завершением работы программы; 16 | * менять настройки языка распознавания и синтеза речи; 17 | * TODO многое другое... 18 | 19 | Для **быстрой установки** всех требуемых зависимостей можно воспользоваться командой: 20 | 21 | `pip install requirements.txt` 22 | 23 | ### Настройка синтеза и анализа речи с возможностью offline-работы 24 | Голосовой ассистент использует для синтеза речи встроенные в операционные системы возможности 25 | (т.е. **голоса зависят от настроек операционной системы**). Для этого используется библиотека `pyttsx3`. [Подробнее здесь](https://github.com/nateshmbhat/pyttsx3) 26 | 27 | Для корректной работы системы распознавания речи в сочетании с библиотекой SpeechRecognition 28 | используется библиотека PyAudio для получения звука с микрофона. 29 | 30 | В целом, решение работает на Windows, Linux и MacOS с незначительными различиями при установке библиотек PyAudio и Google. 31 | 32 | Для установки PyAudio на Windows можно найти и скачать нужный в зависимости от архитектуры и версии Python whl-файл [здесь](https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyaudio) в папку с проектом. После чего его можно установить при помощи подобной команды: 33 | 34 | `pip install PyAudio-0.2.11-cp38-cp38m-win_amd64.whl` 35 | 36 | В случае проблем с установкой PyAudio на MacOS может помочь [данное решение](https://stackoverflow.com/questions/33851379/pyaudio-installation-on-mac-python-3). 37 | 38 | Для использования SpeechRecognition в offline-режиме (без доступа к Интернету), 39 | потребуется дополнительно установить Vosk (качество моделей близко к Google) 40 | 41 | В проекте преимущественно используется Google при наличии доступа в Интернет и 42 | предусмотрено переключение на Vosk в случае отсутствия доступа к сети. 43 | 44 | Для избежания проблем с установкой Vosk на Windows, я предлагаю скачать whl-файл в зависимости от требуемой архитектуры и версии Python. Его можно найти [здесь](https://github.com/alphacep/vosk-api/releases/). Загрузив файл в папку с проектом, установку можно будет запустить с помощью подобной команды: 45 | 46 | `pip install vosk-0.3.7-cp38-cp38-win_amd64.whl` 47 | 48 | Модели для распознавания речи с помощью Vosk можно найти [здесь](https://alphacephei.com/vosk/models). Я использовала в проекте [ru](https://alphacephei.com/vosk/models/vosk-model-small-ru-0.4.zip) и [en](http://alphacephei.com/vosk/models/vosk-model-small-en-us-0.4.zip) модели 49 | 50 | ### Настройка получения прогноза погоды от OpenWeatherMap 51 | Для получения данных прогноза погоды мною был использован сервис `OpenWeatherMap`, который **требует API-ключ**. 52 | Получить API-ключ и ознакомиться с документацией можно после регистрации (есть `Free`-тариф) [здесь](https://openweathermap.org/). 53 | Примеры использования можно найти [здесь](https://pyowm.readthedocs.io/en/latest/v3/code-recipes.html) 54 | 55 | 56 | ### Прочие зависимости 57 | Команды для установки прочих сторонних библиотек: 58 | Команда установки | Назначение библиотеки 59 | ----------------|---------------------- 60 | `pip install google` | Поисковые запросы в Google 61 | `pip install SpeechRecognition` | Распознавание речи (Speech-To-Text) 62 | `pip install vosk` | Offline распознавание речи (Speech-To-Text) 63 | `pip install pyttsx3` | Offline синтез речи на Windows (Text-To-Speech) 64 | `pip install wikipedia-api`| Wikipedia API 65 | `pip install googletrans`| Google Translate 66 | `pip install pyowm`| Получение данных погоды с помощью OpenWeatherMap 67 | `pip install python-dotenv`| Работа с `.env`-файлами для хранения API-ключей 68 | `pip install scikit-learn`| Машинного обучение для угадывания намерений 69 | 70 | Дополнительную информацию по установке и использованию библиотек можно найти [здесь](https://pypi.org/) 71 | 72 | У меня на Windows не возникало проблем с установкой библиотек, перечисленных в таблице выше, 73 | потому прилагаю только команды для установки. 74 | 75 | В случае возникновения проблем с установкой на Windows, вы можете воспользоваться тем же способом, 76 | который я предлагала для установки PyAudio выше. 77 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Проект голосового ассистента на Python 3 от восхитительной EnjiRouz :Р 3 | 4 | Помощник умеет: 5 | * распознавать и синтезировать речь в offline-моде (без доступа к Интернету); 6 | * сообщать о прогнозе погоды в любой точке мира; 7 | * производить поисковый запрос в поисковой системе Google 8 | (а также открывать список результатов и сами результаты данного запроса); 9 | * производить поисковый запрос видео в системе YouTube и открывать список результатов данного запроса; 10 | * выполнять поиск определения в Wikipedia c дальнейшим прочтением первых двух предложений; 11 | * искать человека по имени и фамилии в соцсетях ВКонтакте и Facebook; 12 | * "подбрасывать монетку"; 13 | * переводить с изучаемого языка на родной язык пользователя (с учетом особенностей воспроизведения речи); 14 | * воспроизводить случайное приветствие; 15 | * воспроизводить случайное прощание с последующим завершением работы программы; 16 | * менять настройки языка распознавания и синтеза речи; 17 | * TODO........ 18 | 19 | Голосовой ассистент использует для синтеза речи встроенные в операционную систему Windows 10 возможности 20 | (т.е. голоса зависят от операционной системы). Для этого используется библиотека pyttsx3 21 | 22 | Для корректной работы системы распознавания речи в сочетании с библиотекой SpeechRecognition 23 | используется библиотека PyAudio для получения звука с микрофона. 24 | 25 | Для установки PyAudio можно найти и скачать нужный в зависимости от архитектуры и версии Python whl-файл здесь: 26 | https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyaudio 27 | 28 | Загрузив файл в папку с проектом, установку можно будет запустить с помощью подобной команды: 29 | pip install PyAudio-0.2.11-cp38-cp38m-win_amd64.whl 30 | 31 | Для использования SpeechRecognition в offline-режиме (без доступа к Интернету), потребуется дополнительно установить 32 | vosk, whl-файл для которого можно найти здесь в зависимости от требуемой архитектуры и версии Python: 33 | https://github.com/alphacep/vosk-api/releases/ 34 | 35 | Загрузив файл в папку с проектом, установку можно будет запустить с помощью подобной команды: 36 | pip install vosk-0.3.7-cp38-cp38-win_amd64.whl 37 | 38 | Для получения данных прогноза погоды мною был использован сервис OpenWeatherMap, который требует API-ключ. 39 | Получить API-ключ и ознакомиться с документацией можно после регистрации (есть Free-тариф) здесь: 40 | https://openweathermap.org/ 41 | 42 | Команды для установки прочих сторонних библиотек: 43 | pip install google 44 | pip install SpeechRecognition 45 | pip install pyttsx3 46 | pip install wikipedia-api 47 | pip install googletrans 48 | pip install python-dotenv 49 | pip install pyowm 50 | 51 | Для быстрой установки всех требуемых зависимостей можно воспользоваться командой: 52 | pip install requirements.txt 53 | 54 | Дополнительную информацию по установке и использованию библиотек можно найти здесь: 55 | https://pypi.org/ 56 | """ 57 | 58 | from vosk import Model, KaldiRecognizer # оффлайн-распознавание от Vosk 59 | from googlesearch import search # поиск в Google 60 | from pyowm import OWM # использование OpenWeatherMap для получения данных о погоде 61 | from termcolor import colored # вывод цветных логов (для выделения распознанной речи) 62 | from dotenv import load_dotenv # загрузка информации из .env-файла 63 | import speech_recognition # распознавание пользовательской речи (Speech-To-Text) 64 | import googletrans # использование системы Google Translate 65 | import pyttsx3 # синтез речи (Text-To-Speech) 66 | import wikipediaapi # поиск определений в Wikipedia 67 | import random # генератор случайных чисел 68 | import webbrowser # работа с использованием браузера по умолчанию (открывание вкладок с web-страницей) 69 | import traceback # вывод traceback без остановки работы программы при отлове исключений 70 | import json # работа с json-файлами и json-строками 71 | import wave # создание и чтение аудиофайлов формата wav 72 | import os # работа с файловой системой 73 | 74 | 75 | class Translation: 76 | """ 77 | Получение вшитого в приложение перевода строк для создания мультиязычного ассистента 78 | """ 79 | with open("translations.json", "r", encoding="UTF-8") as file: 80 | translations = json.load(file) 81 | 82 | def get(self, text: str): 83 | """ 84 | Получение перевода строки из файла на нужный язык (по его коду) 85 | :param text: текст, который требуется перевести 86 | :return: вшитый в приложение перевод текста 87 | """ 88 | if text in self.translations: 89 | return self.translations[text][assistant.speech_language] 90 | else: 91 | # в случае отсутствия перевода происходит вывод сообщения об этом в логах и возврат исходного текста 92 | print(colored("Not translated phrase: {}".format(text), "red")) 93 | return text 94 | 95 | 96 | class OwnerPerson: 97 | """ 98 | Информация о владельце, включающие имя, город проживания, родной язык речи, изучаемый язык (для переводов текста) 99 | """ 100 | name = "" 101 | home_city = "" 102 | native_language = "" 103 | target_language = "" 104 | 105 | 106 | class VoiceAssistant: 107 | """ 108 | Настройки голосового ассистента, включающие имя, пол, язык речи 109 | Примечание: для мультиязычных голосовых ассистентов лучше создать отдельный класс, 110 | который будет брать перевод из JSON-файла с нужным языком 111 | """ 112 | name = "" 113 | sex = "" 114 | speech_language = "" 115 | recognition_language = "" 116 | 117 | 118 | def setup_assistant_voice(): 119 | """ 120 | Установка голоса по умолчанию (индекс может меняться в зависимости от настроек операционной системы) 121 | """ 122 | voices = ttsEngine.getProperty("voices") 123 | 124 | if assistant.speech_language == "en": 125 | assistant.recognition_language = "en-US" 126 | if assistant.sex == "female": 127 | # Microsoft Zira Desktop - English (United States) 128 | ttsEngine.setProperty("voice", voices[1].id) 129 | else: 130 | # Microsoft David Desktop - English (United States) 131 | ttsEngine.setProperty("voice", voices[2].id) 132 | else: 133 | assistant.recognition_language = "ru-RU" 134 | # Microsoft Irina Desktop - Russian 135 | ttsEngine.setProperty("voice", voices[0].id) 136 | 137 | 138 | def record_and_recognize_audio(*args: tuple): 139 | """ 140 | Запись и распознавание аудио 141 | """ 142 | with microphone: 143 | recognized_data = "" 144 | 145 | # запоминание шумов окружения для последующей очистки звука от них 146 | recognizer.adjust_for_ambient_noise(microphone, duration=2) 147 | 148 | try: 149 | print("Listening...") 150 | audio = recognizer.listen(microphone, 5, 5) 151 | 152 | with open("microphone-results.wav", "wb") as file: 153 | file.write(audio.get_wav_data()) 154 | 155 | except speech_recognition.WaitTimeoutError: 156 | play_voice_assistant_speech(translator.get("Can you check if your microphone is on, please?")) 157 | traceback.print_exc() 158 | return 159 | 160 | # использование online-распознавания через Google (высокое качество распознавания) 161 | try: 162 | print("Started recognition...") 163 | recognized_data = recognizer.recognize_google(audio, language=assistant.recognition_language).lower() 164 | 165 | except speech_recognition.UnknownValueError: 166 | pass # play_voice_assistant_speech("What did you say again?") 167 | 168 | # в случае проблем с доступом в Интернет происходит попытка использовать offline-распознавание через Vosk 169 | except speech_recognition.RequestError: 170 | print(colored("Trying to use offline recognition...", "cyan")) 171 | recognized_data = use_offline_recognition() 172 | 173 | return recognized_data 174 | 175 | 176 | def use_offline_recognition(): 177 | """ 178 | Переключение на оффлайн-распознавание речи 179 | :return: распознанная фраза 180 | """ 181 | recognized_data = "" 182 | try: 183 | # проверка наличия модели на нужном языке в каталоге приложения 184 | if not os.path.exists("models/vosk-model-small-" + assistant.speech_language + "-0.4"): 185 | print(colored("Please download the model from:\n" 186 | "https://alphacephei.com/vosk/models and unpack as 'model' in the current folder.", 187 | "red")) 188 | exit(1) 189 | 190 | # анализ записанного в микрофон аудио (чтобы избежать повторов фразы) 191 | wave_audio_file = wave.open("microphone-results.wav", "rb") 192 | model = Model("models/vosk-model-small-" + assistant.speech_language + "-0.4") 193 | offline_recognizer = KaldiRecognizer(model, wave_audio_file.getframerate()) 194 | 195 | data = wave_audio_file.readframes(wave_audio_file.getnframes()) 196 | if len(data) > 0: 197 | if offline_recognizer.AcceptWaveform(data): 198 | recognized_data = offline_recognizer.Result() 199 | 200 | # получение данных распознанного текста из JSON-строки (чтобы можно было выдать по ней ответ) 201 | recognized_data = json.loads(recognized_data) 202 | recognized_data = recognized_data["text"] 203 | except: 204 | traceback.print_exc() 205 | print(colored("Sorry, speech service is unavailable. Try again later", "red")) 206 | 207 | return recognized_data 208 | 209 | 210 | def play_voice_assistant_speech(text_to_speech): 211 | """ 212 | Проигрывание речи ответов голосового ассистента (без сохранения аудио) 213 | :param text_to_speech: текст, который нужно преобразовать в речь 214 | """ 215 | ttsEngine.say(str(text_to_speech)) 216 | ttsEngine.runAndWait() 217 | 218 | 219 | def play_greetings(*args: tuple): 220 | """ 221 | Проигрывание случайной приветственной речи 222 | """ 223 | greetings = [ 224 | translator.get("Hello, {}! How can I help you today?").format(person.name), 225 | translator.get("Good day to you {}! How can I help you today?").format(person.name) 226 | ] 227 | play_voice_assistant_speech(greetings[random.randint(0, len(greetings) - 1)]) 228 | 229 | 230 | def play_farewell_and_quit(*args: tuple): 231 | """ 232 | Проигрывание прощательной речи и выход 233 | """ 234 | farewells = [ 235 | translator.get("Goodbye, {}! Have a nice day!").format(person.name), 236 | translator.get("See you soon, {}!").format(person.name) 237 | ] 238 | play_voice_assistant_speech(farewells[random.randint(0, len(farewells) - 1)]) 239 | ttsEngine.stop() 240 | quit() 241 | 242 | 243 | def search_for_term_on_google(*args: tuple): 244 | """ 245 | Поиск в Google с автоматическим открытием ссылок (на список результатов и на сами результаты, если возможно) 246 | :param args: фраза поискового запроса 247 | """ 248 | if not args[0]: return 249 | search_term = " ".join(args[0]) 250 | 251 | # открытие ссылки на поисковик в браузере 252 | url = "https://google.com/search?q=" + search_term 253 | webbrowser.get().open(url) 254 | 255 | # альтернативный поиск с автоматическим открытием ссылок на результаты (в некоторых случаях может быть небезопасно) 256 | search_results = [] 257 | try: 258 | for _ in search(search_term, # что искать 259 | tld="com", # верхнеуровневый домен 260 | lang=assistant.speech_language, # используется язык, на котором говорит ассистент 261 | num=1, # количество результатов на странице 262 | start=0, # индекс первого извлекаемого результата 263 | stop=1, # индекс последнего извлекаемого результата (я хочу, чтобы открывался первый результат) 264 | pause=1.0, # задержка между HTTP-запросами 265 | ): 266 | search_results.append(_) 267 | webbrowser.get().open(_) 268 | 269 | # поскольку все ошибки предсказать сложно, то будет произведен отлов с последующим выводом без остановки программы 270 | except: 271 | play_voice_assistant_speech(translator.get("Seems like we have a trouble. See logs for more information")) 272 | traceback.print_exc() 273 | return 274 | 275 | print(search_results) 276 | play_voice_assistant_speech(translator.get("Here is what I found for {} on google").format(search_term)) 277 | 278 | 279 | def search_for_video_on_youtube(*args: tuple): 280 | """ 281 | Поиск видео на YouTube с автоматическим открытием ссылки на список результатов 282 | :param args: фраза поискового запроса 283 | """ 284 | if not args[0]: return 285 | search_term = " ".join(args[0]) 286 | url = "https://www.youtube.com/results?search_query=" + search_term 287 | webbrowser.get().open(url) 288 | play_voice_assistant_speech(translator.get("Here is what I found for {} on youtube").format(search_term)) 289 | 290 | 291 | def search_for_definition_on_wikipedia(*args: tuple): 292 | """ 293 | Поиск в Wikipedia определения с последующим озвучиванием результатов и открытием ссылок 294 | :param args: фраза поискового запроса 295 | """ 296 | if not args[0]: return 297 | 298 | search_term = " ".join(args[0]) 299 | 300 | # установка языка (в данном случае используется язык, на котором говорит ассистент) 301 | wiki = wikipediaapi.Wikipedia(assistant.speech_language) 302 | 303 | # поиск страницы по запросу, чтение summary, открытие ссылки на страницу для получения подробной информации 304 | wiki_page = wiki.page(search_term) 305 | try: 306 | if wiki_page.exists(): 307 | play_voice_assistant_speech(translator.get("Here is what I found for {} on Wikipedia").format(search_term)) 308 | webbrowser.get().open(wiki_page.fullurl) 309 | 310 | # чтение ассистентом первых двух предложений summary со страницы Wikipedia 311 | # (могут быть проблемы с мультиязычностью) 312 | play_voice_assistant_speech(wiki_page.summary.split(".")[:2]) 313 | else: 314 | # открытие ссылки на поисковик в браузере в случае, если на Wikipedia не удалось найти ничего по запросу 315 | play_voice_assistant_speech(translator.get( 316 | "Can't find {} on Wikipedia. But here is what I found on google").format(search_term)) 317 | url = "https://google.com/search?q=" + search_term 318 | webbrowser.get().open(url) 319 | 320 | # поскольку все ошибки предсказать сложно, то будет произведен отлов с последующим выводом без остановки программы 321 | except: 322 | play_voice_assistant_speech(translator.get("Seems like we have a trouble. See logs for more information")) 323 | traceback.print_exc() 324 | return 325 | 326 | 327 | def get_translation(*args: tuple): 328 | """ 329 | Получение перевода текста с одного языка на другой (в данном случае с изучаемого на родной язык или обратно) 330 | :param args: фраза, которую требуется перевести 331 | """ 332 | if not args[0]: return 333 | 334 | search_term = " ".join(args[0]) 335 | google_translator = googletrans.Translator() 336 | translation_result = "" 337 | 338 | old_assistant_language = assistant.speech_language 339 | try: 340 | # если язык речи ассистента и родной язык пользователя различаются, то перевод выполяется на родной язык 341 | if assistant.speech_language != person.native_language: 342 | translation_result = google_translator.translate(search_term, # что перевести 343 | src=person.target_language, # с какого языка 344 | dest=person.native_language) # на какой язык 345 | 346 | play_voice_assistant_speech("The translation for {} in Russian is".format(search_term)) 347 | 348 | # смена голоса ассистента на родной язык пользователя (чтобы можно было произнести перевод) 349 | assistant.speech_language = person.native_language 350 | setup_assistant_voice() 351 | 352 | # если язык речи ассистента и родной язык пользователя одинаковы, то перевод выполяется на изучаемый язык 353 | else: 354 | translation_result = google_translator.translate(search_term, # что перевести 355 | src=person.native_language, # с какого языка 356 | dest=person.target_language) # на какой язык 357 | play_voice_assistant_speech("По-английски {} будет как".format(search_term)) 358 | 359 | # смена голоса ассистента на изучаемый язык пользователя (чтобы можно было произнести перевод) 360 | assistant.speech_language = person.target_language 361 | setup_assistant_voice() 362 | 363 | # произнесение перевода 364 | play_voice_assistant_speech(translation_result.text) 365 | 366 | # поскольку все ошибки предсказать сложно, то будет произведен отлов с последующим выводом без остановки программы 367 | except: 368 | play_voice_assistant_speech(translator.get("Seems like we have a trouble. See logs for more information")) 369 | traceback.print_exc() 370 | 371 | finally: 372 | # возвращение преждних настроек голоса помощника 373 | assistant.speech_language = old_assistant_language 374 | setup_assistant_voice() 375 | 376 | 377 | def get_weather_forecast(*args: tuple): 378 | """ 379 | Получение и озвучивание прогнза погоды 380 | :param args: город, по которому должен выполняться запос 381 | """ 382 | # в случае наличия дополнительного аргумента - запрос погоды происходит по нему, 383 | # иначе - используется город, заданный в настройках 384 | if args[0]: 385 | city_name = args[0][0] 386 | else: 387 | city_name = person.home_city 388 | 389 | try: 390 | # использование API-ключа, помещённого в .env-файл по примеру WEATHER_API_KEY = "01234abcd....." 391 | weather_api_key = os.getenv("WEATHER_API_KEY") 392 | open_weather_map = OWM(weather_api_key) 393 | 394 | # запрос данных о текущем состоянии погоды 395 | weather_manager = open_weather_map.weather_manager() 396 | observation = weather_manager.weather_at_place(city_name) 397 | weather = observation.weather 398 | 399 | # поскольку все ошибки предсказать сложно, то будет произведен отлов с последующим выводом без остановки программы 400 | except: 401 | play_voice_assistant_speech(translator.get("Seems like we have a trouble. See logs for more information")) 402 | traceback.print_exc() 403 | return 404 | 405 | # разбивание данных на части для удобства работы с ними 406 | status = weather.detailed_status 407 | temperature = weather.temperature('celsius')["temp"] 408 | wind_speed = weather.wind()["speed"] 409 | pressure = int(weather.pressure["press"] / 1.333) # переведено из гПА в мм рт.ст. 410 | 411 | # вывод логов 412 | print(colored("Weather in " + city_name + 413 | ":\n * Status: " + status + 414 | "\n * Wind speed (m/sec): " + str(wind_speed) + 415 | "\n * Temperature (Celsius): " + str(temperature) + 416 | "\n * Pressure (mm Hg): " + str(pressure), "yellow")) 417 | 418 | # озвучивание текущего состояния погоды ассистентом (здесь для мультиязычности требуется дополнительная работа) 419 | play_voice_assistant_speech(translator.get("It is {0} in {1}").format(status, city_name)) 420 | play_voice_assistant_speech(translator.get("The temperature is {} degrees Celsius").format(str(temperature))) 421 | play_voice_assistant_speech(translator.get("The wind speed is {} meters per second").format(str(wind_speed))) 422 | play_voice_assistant_speech(translator.get("The pressure is {} mm Hg").format(str(pressure))) 423 | 424 | 425 | def change_language(*args: tuple): 426 | """ 427 | Изменение языка голосового ассистента (языка распознавания речи) 428 | """ 429 | assistant.speech_language = "ru" if assistant.speech_language == "en" else "en" 430 | setup_assistant_voice() 431 | print(colored("Language switched to " + assistant.speech_language, "cyan")) 432 | 433 | 434 | def run_person_through_social_nets_databases(*args: tuple): 435 | """ 436 | Поиск человека по базе данных социальных сетей ВКонтакте и Facebook 437 | :param args: имя, фамилия TODO город 438 | """ 439 | if not args[0]: return 440 | 441 | google_search_term = " ".join(args[0]) 442 | vk_search_term = "_".join(args[0]) 443 | fb_search_term = "-".join(args[0]) 444 | 445 | # открытие ссылки на поисковик в браузере 446 | url = "https://google.com/search?q=" + google_search_term + " site: vk.com" 447 | webbrowser.get().open(url) 448 | 449 | url = "https://google.com/search?q=" + google_search_term + " site: facebook.com" 450 | webbrowser.get().open(url) 451 | 452 | # открытие ссылкок на поисковики социальных сетей в браузере 453 | vk_url = "https://vk.com/people/" + vk_search_term 454 | webbrowser.get().open(vk_url) 455 | 456 | fb_url = "https://www.facebook.com/public/" + fb_search_term 457 | webbrowser.get().open(fb_url) 458 | 459 | play_voice_assistant_speech(translator.get("Here is what I found for {} on social nets").format(google_search_term)) 460 | 461 | 462 | def toss_coin(*args: tuple): 463 | """ 464 | "Подбрасывание" монетки для выбора из 2 опций 465 | """ 466 | flips_count, heads, tails = 3, 0, 0 467 | 468 | for flip in range(flips_count): 469 | if random.randint(0, 1) == 0: 470 | heads += 1 471 | 472 | tails = flips_count - heads 473 | winner = "Tails" if tails > heads else "Heads" 474 | play_voice_assistant_speech(translator.get(winner) + " " + translator.get("won")) 475 | 476 | 477 | def execute_command_with_name(command_name: str, *args: list): 478 | """ 479 | Выполнение заданной пользователем команды и аргументами 480 | :param command_name: название команды 481 | :param args: аргументы, которые будут переданы в метод 482 | :return: 483 | """ 484 | for key in commands.keys(): 485 | if command_name in key: 486 | commands[key](*args) 487 | else: 488 | pass # print("Command not found") 489 | 490 | 491 | # перечень команд для использования (качестве ключей словаря используется hashable-тип tuple) 492 | # в качестве альтернативы можно использовать JSON-объект с намерениями и сценариями 493 | # (подобно тем, что применяют для чат-ботов) 494 | commands = { 495 | ("hello", "hi", "morning", "привет"): play_greetings, 496 | ("bye", "goodbye", "quit", "exit", "stop", "пока"): play_farewell_and_quit, 497 | ("search", "google", "find", "найди"): search_for_term_on_google, 498 | ("video", "youtube", "watch", "видео"): search_for_video_on_youtube, 499 | ("wikipedia", "definition", "about", "определение", "википедия"): search_for_definition_on_wikipedia, 500 | ("translate", "interpretation", "translation", "перевод", "перевести", "переведи"): get_translation, 501 | ("language", "язык"): change_language, 502 | ("weather", "forecast", "погода", "прогноз"): get_weather_forecast, 503 | ("facebook", "person", "run", "пробей", "контакт"): run_person_through_social_nets_databases, 504 | ("toss", "coin", "монета", "подбрось"): toss_coin, 505 | } 506 | 507 | if __name__ == "__main__": 508 | 509 | # инициализация инструментов распознавания и ввода речи 510 | recognizer = speech_recognition.Recognizer() 511 | microphone = speech_recognition.Microphone() 512 | 513 | # инициализация инструмента синтеза речи 514 | ttsEngine = pyttsx3.init() 515 | 516 | # настройка данных пользователя 517 | person = OwnerPerson() 518 | person.name = "Tanya" 519 | person.home_city = "Yekaterinburg" 520 | person.native_language = "ru" 521 | person.target_language = "en" 522 | 523 | # настройка данных голосового помощника 524 | assistant = VoiceAssistant() 525 | assistant.name = "Alice" 526 | assistant.sex = "female" 527 | assistant.speech_language = "en" 528 | 529 | # установка голоса по умолчанию 530 | setup_assistant_voice() 531 | 532 | # добавление возможностей перевода фраз (из заготовленного файла) 533 | translator = Translation() 534 | 535 | # загрузка информации из .env-файла (там лежит API-ключ для OpenWeatherMap) 536 | load_dotenv() 537 | 538 | while True: 539 | # старт записи речи с последующим выводом распознанной речи и удалением записанного в микрофон аудио 540 | voice_input = record_and_recognize_audio() 541 | os.remove("microphone-results.wav") 542 | print(colored(voice_input, "blue")) 543 | 544 | # отделение комманд от дополнительной информации (аргументов) 545 | voice_input = voice_input.split(" ") 546 | command = voice_input[0] 547 | command_options = [str(input_part) for input_part in voice_input[1:len(voice_input)]] 548 | execute_command_with_name(command, command_options) 549 | 550 | # TODO food order 551 | # TODO recommend film by rating/genre (use recommendation system project) 552 | # как насчёт "название фильма"? Вот его описание:..... 553 | -------------------------------------------------------------------------------- /app_nlu_version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Проект голосового ассистента на Python 3 от восхитительной EnjiRouz :Р 3 | 4 | Помощник умеет: 5 | * распознавать и синтезировать речь в offline-моде (без доступа к Интернету); 6 | * сообщать о прогнозе погоды в любой точке мира; 7 | * производить поисковый запрос в поисковой системе Google 8 | (а также открывать список результатов и сами результаты данного запроса); 9 | * производить поисковый запрос видео в системе YouTube и открывать список результатов данного запроса; 10 | * выполнять поиск определения в Wikipedia c дальнейшим прочтением первых двух предложений; 11 | * искать человека по имени и фамилии в соцсетях ВКонтакте и Facebook; 12 | * "подбрасывать монетку"; 13 | * переводить с изучаемого языка на родной язык пользователя (с учетом особенностей воспроизведения речи); 14 | * воспроизводить случайное приветствие; 15 | * воспроизводить случайное прощание с последующим завершением работы программы; 16 | * менять настройки языка распознавания и синтеза речи; 17 | * TODO........ 18 | 19 | Голосовой ассистент использует для синтеза речи встроенные в операционную систему Windows 10 возможности 20 | (т.е. голоса зависят от операционной системы). Для этого используется библиотека pyttsx3 21 | 22 | Для корректной работы системы распознавания речи в сочетании с библиотекой SpeechRecognition 23 | используется библиотека PyAudio для получения звука с микрофона. 24 | 25 | Для установки PyAudio можно найти и скачать нужный в зависимости от архитектуры и версии Python whl-файл здесь: 26 | https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyaudio 27 | 28 | Загрузив файл в папку с проектом, установку можно будет запустить с помощью подобной команды: 29 | pip install PyAudio-0.2.11-cp38-cp38m-win_amd64.whl 30 | 31 | Для использования SpeechRecognition в offline-режиме (без доступа к Интернету), потребуется дополнительно установить 32 | vosk, whl-файл для которого можно найти здесь в зависимости от требуемой архитектуры и версии Python: 33 | https://github.com/alphacep/vosk-api/releases/ 34 | 35 | Загрузив файл в папку с проектом, установку можно будет запустить с помощью подобной команды: 36 | pip install vosk-0.3.7-cp38-cp38-win_amd64.whl 37 | 38 | Для получения данных прогноза погоды мною был использован сервис OpenWeatherMap, который требует API-ключ. 39 | Получить API-ключ и ознакомиться с документацией можно после регистрации (есть Free-тариф) здесь: 40 | https://openweathermap.org/ 41 | 42 | Команды для установки прочих сторонних библиотек: 43 | pip install google 44 | pip install SpeechRecognition 45 | pip install pyttsx3 46 | pip install wikipedia-api 47 | pip install googletrans 48 | pip install python-dotenv 49 | pip install pyowm 50 | pip install scikit-learn 51 | 52 | Для быстрой установки всех требуемых зависимостей можно воспользоваться командой: 53 | pip install requirements.txt 54 | 55 | Дополнительную информацию по установке и использованию библиотек можно найти здесь: 56 | https://pypi.org/ 57 | """ 58 | 59 | # машинное обучения для реализации возможности угадывания намерений 60 | from sklearn.feature_extraction.text import TfidfVectorizer 61 | from sklearn.linear_model import LogisticRegression 62 | from sklearn.svm import LinearSVC 63 | 64 | from vosk import Model, KaldiRecognizer # оффлайн-распознавание от Vosk 65 | from googlesearch import search # поиск в Google 66 | from pyowm import OWM # использование OpenWeatherMap для получения данных о погоде 67 | from termcolor import colored # вывод цветных логов (для выделения распознанной речи) 68 | from dotenv import load_dotenv # загрузка информации из .env-файла 69 | import speech_recognition # распознавание пользовательской речи (Speech-To-Text) 70 | import googletrans # использование системы Google Translate 71 | import pyttsx3 # синтез речи (Text-To-Speech) 72 | import wikipediaapi # поиск определений в Wikipedia 73 | import random # генератор случайных чисел 74 | import webbrowser # работа с использованием браузера по умолчанию (открывание вкладок с web-страницей) 75 | import traceback # вывод traceback без остановки работы программы при отлове исключений 76 | import json # работа с json-файлами и json-строками 77 | import wave # создание и чтение аудиофайлов формата wav 78 | import os # работа с файловой системой 79 | 80 | 81 | class Translation: 82 | """ 83 | Получение вшитого в приложение перевода строк для создания мультиязычного ассистента 84 | """ 85 | with open("translations.json", "r", encoding="UTF-8") as file: 86 | translations = json.load(file) 87 | 88 | def get(self, text: str): 89 | """ 90 | Получение перевода строки из файла на нужный язык (по его коду) 91 | :param text: текст, который требуется перевести 92 | :return: вшитый в приложение перевод текста 93 | """ 94 | if text in self.translations: 95 | return self.translations[text][assistant.speech_language] 96 | else: 97 | # в случае отсутствия перевода происходит вывод сообщения об этом в логах и возврат исходного текста 98 | print(colored("Not translated phrase: {}".format(text), "red")) 99 | return text 100 | 101 | 102 | class OwnerPerson: 103 | """ 104 | Информация о владельце, включающие имя, город проживания, родной язык речи, изучаемый язык (для переводов текста) 105 | """ 106 | name = "" 107 | home_city = "" 108 | native_language = "" 109 | target_language = "" 110 | 111 | 112 | class VoiceAssistant: 113 | """ 114 | Настройки голосового ассистента, включающие имя, пол, язык речи 115 | Примечание: для мультиязычных голосовых ассистентов лучше создать отдельный класс, 116 | который будет брать перевод из JSON-файла с нужным языком 117 | """ 118 | name = "" 119 | sex = "" 120 | speech_language = "" 121 | recognition_language = "" 122 | 123 | 124 | def setup_assistant_voice(): 125 | """ 126 | Установка голоса по умолчанию (индекс может меняться в зависимости от настроек операционной системы) 127 | """ 128 | voices = ttsEngine.getProperty("voices") 129 | 130 | if assistant.speech_language == "en": 131 | assistant.recognition_language = "en-US" 132 | if assistant.sex == "female": 133 | # Microsoft Zira Desktop - English (United States) 134 | ttsEngine.setProperty("voice", voices[1].id) 135 | else: 136 | # Microsoft David Desktop - English (United States) 137 | ttsEngine.setProperty("voice", voices[2].id) 138 | else: 139 | assistant.recognition_language = "ru-RU" 140 | # Microsoft Irina Desktop - Russian 141 | ttsEngine.setProperty("voice", voices[0].id) 142 | 143 | 144 | def record_and_recognize_audio(*args: tuple): 145 | """ 146 | Запись и распознавание аудио 147 | """ 148 | with microphone: 149 | recognized_data = "" 150 | 151 | # запоминание шумов окружения для последующей очистки звука от них 152 | recognizer.adjust_for_ambient_noise(microphone, duration=2) 153 | 154 | try: 155 | print("Listening...") 156 | audio = recognizer.listen(microphone, 5, 5) 157 | 158 | with open("microphone-results.wav", "wb") as file: 159 | file.write(audio.get_wav_data()) 160 | 161 | except speech_recognition.WaitTimeoutError: 162 | play_voice_assistant_speech(translator.get("Can you check if your microphone is on, please?")) 163 | traceback.print_exc() 164 | return 165 | 166 | # использование online-распознавания через Google (высокое качество распознавания) 167 | try: 168 | print("Started recognition...") 169 | recognized_data = recognizer.recognize_google(audio, language=assistant.recognition_language).lower() 170 | 171 | except speech_recognition.UnknownValueError: 172 | pass # play_voice_assistant_speech("What did you say again?") 173 | 174 | # в случае проблем с доступом в Интернет происходит попытка использовать offline-распознавание через Vosk 175 | except speech_recognition.RequestError: 176 | print(colored("Trying to use offline recognition...", "cyan")) 177 | recognized_data = use_offline_recognition() 178 | 179 | return recognized_data 180 | 181 | 182 | def use_offline_recognition(): 183 | """ 184 | Переключение на оффлайн-распознавание речи 185 | :return: распознанная фраза 186 | """ 187 | recognized_data = "" 188 | try: 189 | # проверка наличия модели на нужном языке в каталоге приложения 190 | if not os.path.exists("models/vosk-model-small-" + assistant.speech_language + "-0.4"): 191 | print(colored("Please download the model from:\n" 192 | "https://alphacephei.com/vosk/models and unpack as 'model' in the current folder.", 193 | "red")) 194 | exit(1) 195 | 196 | # анализ записанного в микрофон аудио (чтобы избежать повторов фразы) 197 | wave_audio_file = wave.open("microphone-results.wav", "rb") 198 | model = Model("models/vosk-model-small-" + assistant.speech_language + "-0.4") 199 | offline_recognizer = KaldiRecognizer(model, wave_audio_file.getframerate()) 200 | 201 | data = wave_audio_file.readframes(wave_audio_file.getnframes()) 202 | if len(data) > 0: 203 | if offline_recognizer.AcceptWaveform(data): 204 | recognized_data = offline_recognizer.Result() 205 | 206 | # получение данных распознанного текста из JSON-строки (чтобы можно было выдать по ней ответ) 207 | recognized_data = json.loads(recognized_data) 208 | recognized_data = recognized_data["text"] 209 | except: 210 | traceback.print_exc() 211 | print(colored("Sorry, speech service is unavailable. Try again later", "red")) 212 | 213 | return recognized_data 214 | 215 | 216 | def play_voice_assistant_speech(text_to_speech): 217 | """ 218 | Проигрывание речи ответов голосового ассистента (без сохранения аудио) 219 | :param text_to_speech: текст, который нужно преобразовать в речь 220 | """ 221 | ttsEngine.say(str(text_to_speech)) 222 | ttsEngine.runAndWait() 223 | 224 | 225 | def play_failure_phrase(*args: tuple): 226 | """ 227 | Проигрывание случайной фразы при неудачном распознавании 228 | """ 229 | failure_phrases = [ 230 | translator.get("Can you repeat, please?"), 231 | translator.get("What did you say again?") 232 | ] 233 | play_voice_assistant_speech(failure_phrases[random.randint(0, len(failure_phrases) - 1)]) 234 | 235 | 236 | def play_greetings(*args: tuple): 237 | """ 238 | Проигрывание случайной приветственной речи 239 | """ 240 | greetings = [ 241 | translator.get("Hello, {}! How can I help you today?").format(person.name), 242 | translator.get("Good day to you {}! How can I help you today?").format(person.name) 243 | ] 244 | play_voice_assistant_speech(greetings[random.randint(0, len(greetings) - 1)]) 245 | 246 | 247 | def play_farewell_and_quit(*args: tuple): 248 | """ 249 | Проигрывание прощательной речи и выход 250 | """ 251 | farewells = [ 252 | translator.get("Goodbye, {}! Have a nice day!").format(person.name), 253 | translator.get("See you soon, {}!").format(person.name) 254 | ] 255 | play_voice_assistant_speech(farewells[random.randint(0, len(farewells) - 1)]) 256 | ttsEngine.stop() 257 | quit() 258 | 259 | 260 | def search_for_term_on_google(*args: tuple): 261 | """ 262 | Поиск в Google с автоматическим открытием ссылок (на список результатов и на сами результаты, если возможно) 263 | :param args: фраза поискового запроса 264 | """ 265 | if not args[0]: return 266 | search_term = " ".join(args[0]) 267 | 268 | # открытие ссылки на поисковик в браузере 269 | url = "https://google.com/search?q=" + search_term 270 | webbrowser.get().open(url) 271 | 272 | # альтернативный поиск с автоматическим открытием ссылок на результаты (в некоторых случаях может быть небезопасно) 273 | search_results = [] 274 | try: 275 | for _ in search(search_term, # что искать 276 | tld="com", # верхнеуровневый домен 277 | lang=assistant.speech_language, # используется язык, на котором говорит ассистент 278 | num=1, # количество результатов на странице 279 | start=0, # индекс первого извлекаемого результата 280 | stop=1, # индекс последнего извлекаемого результата (я хочу, чтобы открывался первый результат) 281 | pause=1.0, # задержка между HTTP-запросами 282 | ): 283 | search_results.append(_) 284 | webbrowser.get().open(_) 285 | 286 | # поскольку все ошибки предсказать сложно, то будет произведен отлов с последующим выводом без остановки программы 287 | except: 288 | play_voice_assistant_speech(translator.get("Seems like we have a trouble. See logs for more information")) 289 | traceback.print_exc() 290 | return 291 | 292 | print(search_results) 293 | play_voice_assistant_speech(translator.get("Here is what I found for {} on google").format(search_term)) 294 | 295 | 296 | def search_for_video_on_youtube(*args: tuple): 297 | """ 298 | Поиск видео на YouTube с автоматическим открытием ссылки на список результатов 299 | :param args: фраза поискового запроса 300 | """ 301 | if not args[0]: return 302 | search_term = " ".join(args[0]) 303 | url = "https://www.youtube.com/results?search_query=" + search_term 304 | webbrowser.get().open(url) 305 | play_voice_assistant_speech(translator.get("Here is what I found for {} on youtube").format(search_term)) 306 | 307 | 308 | def search_for_definition_on_wikipedia(*args: tuple): 309 | """ 310 | Поиск в Wikipedia определения с последующим озвучиванием результатов и открытием ссылок 311 | :param args: фраза поискового запроса 312 | """ 313 | if not args[0]: return 314 | 315 | search_term = " ".join(args[0]) 316 | 317 | # установка языка (в данном случае используется язык, на котором говорит ассистент) 318 | wiki = wikipediaapi.Wikipedia(assistant.speech_language) 319 | 320 | # поиск страницы по запросу, чтение summary, открытие ссылки на страницу для получения подробной информации 321 | wiki_page = wiki.page(search_term) 322 | try: 323 | if wiki_page.exists(): 324 | play_voice_assistant_speech(translator.get("Here is what I found for {} on Wikipedia").format(search_term)) 325 | webbrowser.get().open(wiki_page.fullurl) 326 | 327 | # чтение ассистентом первых двух предложений summary со страницы Wikipedia 328 | # (могут быть проблемы с мультиязычностью) 329 | play_voice_assistant_speech(wiki_page.summary.split(".")[:2]) 330 | else: 331 | # открытие ссылки на поисковик в браузере в случае, если на Wikipedia не удалось найти ничего по запросу 332 | play_voice_assistant_speech(translator.get( 333 | "Can't find {} on Wikipedia. But here is what I found on google").format(search_term)) 334 | url = "https://google.com/search?q=" + search_term 335 | webbrowser.get().open(url) 336 | 337 | # поскольку все ошибки предсказать сложно, то будет произведен отлов с последующим выводом без остановки программы 338 | except: 339 | play_voice_assistant_speech(translator.get("Seems like we have a trouble. See logs for more information")) 340 | traceback.print_exc() 341 | return 342 | 343 | 344 | def get_translation(*args: tuple): 345 | """ 346 | Получение перевода текста с одного языка на другой (в данном случае с изучаемого на родной язык или обратно) 347 | :param args: фраза, которую требуется перевести 348 | """ 349 | if not args[0]: return 350 | 351 | search_term = " ".join(args[0]) 352 | google_translator = googletrans.Translator() 353 | translation_result = "" 354 | 355 | old_assistant_language = assistant.speech_language 356 | try: 357 | # если язык речи ассистента и родной язык пользователя различаются, то перевод выполяется на родной язык 358 | if assistant.speech_language != person.native_language: 359 | translation_result = google_translator.translate(search_term, # что перевести 360 | src=person.target_language, # с какого языка 361 | dest=person.native_language) # на какой язык 362 | 363 | play_voice_assistant_speech("The translation for {} in Russian is".format(search_term)) 364 | 365 | # смена голоса ассистента на родной язык пользователя (чтобы можно было произнести перевод) 366 | assistant.speech_language = person.native_language 367 | setup_assistant_voice() 368 | 369 | # если язык речи ассистента и родной язык пользователя одинаковы, то перевод выполяется на изучаемый язык 370 | else: 371 | translation_result = google_translator.translate(search_term, # что перевести 372 | src=person.native_language, # с какого языка 373 | dest=person.target_language) # на какой язык 374 | play_voice_assistant_speech("По-английски {} будет как".format(search_term)) 375 | 376 | # смена голоса ассистента на изучаемый язык пользователя (чтобы можно было произнести перевод) 377 | assistant.speech_language = person.target_language 378 | setup_assistant_voice() 379 | 380 | # произнесение перевода 381 | play_voice_assistant_speech(translation_result.text) 382 | 383 | # поскольку все ошибки предсказать сложно, то будет произведен отлов с последующим выводом без остановки программы 384 | except: 385 | play_voice_assistant_speech(translator.get("Seems like we have a trouble. See logs for more information")) 386 | traceback.print_exc() 387 | 388 | finally: 389 | # возвращение преждних настроек голоса помощника 390 | assistant.speech_language = old_assistant_language 391 | setup_assistant_voice() 392 | 393 | 394 | def get_weather_forecast(*args: tuple): 395 | """ 396 | Получение и озвучивание прогнза погоды 397 | :param args: город, по которому должен выполняться запос 398 | """ 399 | # в случае наличия дополнительного аргумента - запрос погоды происходит по нему, 400 | # иначе - используется город, заданный в настройках 401 | city_name = person.home_city 402 | 403 | if args: 404 | if args[0]: 405 | city_name = args[0][0] 406 | 407 | try: 408 | # использование API-ключа, помещённого в .env-файл по примеру WEATHER_API_KEY = "01234abcd....." 409 | weather_api_key = os.getenv("WEATHER_API_KEY") 410 | open_weather_map = OWM(weather_api_key) 411 | 412 | # запрос данных о текущем состоянии погоды 413 | weather_manager = open_weather_map.weather_manager() 414 | observation = weather_manager.weather_at_place(city_name) 415 | weather = observation.weather 416 | 417 | # поскольку все ошибки предсказать сложно, то будет произведен отлов с последующим выводом без остановки программы 418 | except: 419 | play_voice_assistant_speech(translator.get("Seems like we have a trouble. See logs for more information")) 420 | traceback.print_exc() 421 | return 422 | 423 | # разбивание данных на части для удобства работы с ними 424 | status = weather.detailed_status 425 | temperature = weather.temperature('celsius')["temp"] 426 | wind_speed = weather.wind()["speed"] 427 | pressure = int(weather.pressure["press"] / 1.333) # переведено из гПА в мм рт.ст. 428 | 429 | # вывод логов 430 | print(colored("Weather in " + city_name + 431 | ":\n * Status: " + status + 432 | "\n * Wind speed (m/sec): " + str(wind_speed) + 433 | "\n * Temperature (Celsius): " + str(temperature) + 434 | "\n * Pressure (mm Hg): " + str(pressure), "yellow")) 435 | 436 | # озвучивание текущего состояния погоды ассистентом (здесь для мультиязычности требуется дополнительная работа) 437 | play_voice_assistant_speech(translator.get("It is {0} in {1}").format(status, city_name)) 438 | play_voice_assistant_speech(translator.get("The temperature is {} degrees Celsius").format(str(temperature))) 439 | play_voice_assistant_speech(translator.get("The wind speed is {} meters per second").format(str(wind_speed))) 440 | play_voice_assistant_speech(translator.get("The pressure is {} mm Hg").format(str(pressure))) 441 | 442 | 443 | def change_language(*args: tuple): 444 | """ 445 | Изменение языка голосового ассистента (языка распознавания речи) 446 | """ 447 | assistant.speech_language = "ru" if assistant.speech_language == "en" else "en" 448 | setup_assistant_voice() 449 | print(colored("Language switched to " + assistant.speech_language, "cyan")) 450 | 451 | 452 | def run_person_through_social_nets_databases(*args: tuple): 453 | """ 454 | Поиск человека по базе данных социальных сетей ВКонтакте и Facebook 455 | :param args: имя, фамилия TODO город 456 | """ 457 | if not args[0]: return 458 | 459 | google_search_term = " ".join(args[0]) 460 | vk_search_term = "_".join(args[0]) 461 | fb_search_term = "-".join(args[0]) 462 | 463 | # открытие ссылки на поисковик в браузере 464 | url = "https://google.com/search?q=" + google_search_term + " site: vk.com" 465 | webbrowser.get().open(url) 466 | 467 | url = "https://google.com/search?q=" + google_search_term + " site: facebook.com" 468 | webbrowser.get().open(url) 469 | 470 | # открытие ссылкок на поисковики социальных сетей в браузере 471 | vk_url = "https://vk.com/people/" + vk_search_term 472 | webbrowser.get().open(vk_url) 473 | 474 | fb_url = "https://www.facebook.com/public/" + fb_search_term 475 | webbrowser.get().open(fb_url) 476 | 477 | play_voice_assistant_speech(translator.get("Here is what I found for {} on social nets").format(google_search_term)) 478 | 479 | 480 | def toss_coin(*args: tuple): 481 | """ 482 | "Подбрасывание" монетки для выбора из 2 опций 483 | """ 484 | flips_count, heads, tails = 3, 0, 0 485 | 486 | for flip in range(flips_count): 487 | if random.randint(0, 1) == 0: 488 | heads += 1 489 | 490 | tails = flips_count - heads 491 | winner = "Tails" if tails > heads else "Heads" 492 | play_voice_assistant_speech(translator.get(winner) + " " + translator.get("won")) 493 | 494 | 495 | # перечень команд для использования в виде JSON-объекта 496 | config = { 497 | "intents": { 498 | "greeting": { 499 | "examples": ["привет", "здравствуй", "добрый день", 500 | "hello", "good morning"], 501 | "responses": play_greetings 502 | }, 503 | "farewell": { 504 | "examples": ["пока", "до свидания", "увидимся", "до встречи", 505 | "goodbye", "bye", "see you soon"], 506 | "responses": play_farewell_and_quit 507 | }, 508 | "google_search": { 509 | "examples": ["найди в гугл", 510 | "search on google", "google", "find on google"], 511 | "responses": search_for_term_on_google 512 | }, 513 | "youtube_search": { 514 | "examples": ["найди видео", "покажи видео", 515 | "find video", "find on youtube", "search on youtube"], 516 | "responses": search_for_video_on_youtube 517 | }, 518 | "wikipedia_search": { 519 | "examples": ["найди определение", "найди на википедии", 520 | "find on wikipedia", "find definition", "tell about"], 521 | "responses": search_for_definition_on_wikipedia 522 | }, 523 | "person_search": { 524 | "examples": ["пробей имя", "найди человека", 525 | "find on facebook", " find person", "run person", "search for person"], 526 | "responses": run_person_through_social_nets_databases 527 | }, 528 | "weather_forecast": { 529 | "examples": ["прогноз погоды", "какая погода", 530 | "weather forecast", "report weather"], 531 | "responses": get_weather_forecast 532 | }, 533 | "translation": { 534 | "examples": ["выполни перевод", "переведи", "найди перевод", 535 | "translate", "find translation"], 536 | "responses": get_translation 537 | }, 538 | "language": { 539 | "examples": ["смени язык", "поменяй язык", 540 | "change speech language", "language"], 541 | "responses": change_language 542 | }, 543 | "toss_coin": { 544 | "examples": ["подбрось монетку", "подкинь монетку", 545 | "toss coin", "coin", "flip a coin"], 546 | "responses": toss_coin 547 | } 548 | }, 549 | 550 | "failure_phrases": play_failure_phrase 551 | } 552 | 553 | 554 | def prepare_corpus(): 555 | """ 556 | Подготовка модели для угадывания намерения пользователя 557 | """ 558 | corpus = [] 559 | target_vector = [] 560 | for intent_name, intent_data in config["intents"].items(): 561 | for example in intent_data["examples"]: 562 | corpus.append(example) 563 | target_vector.append(intent_name) 564 | 565 | training_vector = vectorizer.fit_transform(corpus) 566 | classifier_probability.fit(training_vector, target_vector) 567 | classifier.fit(training_vector, target_vector) 568 | 569 | 570 | def get_intent(request): 571 | """ 572 | Получение наиболее вероятного намерения в зависимости от запроса пользователя 573 | :param request: запрос пользователя 574 | :return: наиболее вероятное намерение 575 | """ 576 | best_intent = classifier.predict(vectorizer.transform([request]))[0] 577 | 578 | index_of_best_intent = list(classifier_probability.classes_).index(best_intent) 579 | probabilities = classifier_probability.predict_proba(vectorizer.transform([request]))[0] 580 | 581 | best_intent_probability = probabilities[index_of_best_intent] 582 | 583 | # при добавлении новых намерений стоит уменьшать этот показатель 584 | print(best_intent_probability) 585 | if best_intent_probability > 0.157: 586 | return best_intent 587 | 588 | 589 | def make_preparations(): 590 | """ 591 | Подготовка глобальных переменных к запуску приложения 592 | """ 593 | global recognizer, microphone, ttsEngine, person, assistant, translator, vectorizer, classifier_probability, classifier 594 | 595 | # инициализация инструментов распознавания и ввода речи 596 | recognizer = speech_recognition.Recognizer() 597 | microphone = speech_recognition.Microphone() 598 | 599 | # инициализация инструмента синтеза речи 600 | ttsEngine = pyttsx3.init() 601 | 602 | # настройка данных пользователя 603 | person = OwnerPerson() 604 | person.name = "Tanya" 605 | person.home_city = "Yekaterinburg" 606 | person.native_language = "ru" 607 | person.target_language = "en" 608 | 609 | # настройка данных голосового помощника 610 | assistant = VoiceAssistant() 611 | assistant.name = "Alice" 612 | assistant.sex = "female" 613 | assistant.speech_language = "en" 614 | 615 | # установка голоса по умолчанию 616 | setup_assistant_voice() 617 | 618 | # добавление возможностей перевода фраз (из заготовленного файла) 619 | translator = Translation() 620 | 621 | # загрузка информации из .env-файла (там лежит API-ключ для OpenWeatherMap) 622 | load_dotenv() 623 | 624 | # подготовка корпуса для распознавания запросов пользователя с некоторой вероятностью (поиск похожих) 625 | vectorizer = TfidfVectorizer(analyzer="char", ngram_range=(2, 3)) 626 | classifier_probability = LogisticRegression() 627 | classifier = LinearSVC() 628 | prepare_corpus() 629 | 630 | 631 | if __name__ == "__main__": 632 | make_preparations() 633 | 634 | while True: 635 | # старт записи речи с последующим выводом распознанной речи и удалением записанного в микрофон аудио 636 | voice_input = record_and_recognize_audio() 637 | 638 | if os.path.exists("microphone-results.wav"): 639 | os.remove("microphone-results.wav") 640 | 641 | print(colored(voice_input, "blue")) 642 | 643 | # отделение комманд от дополнительной информации (аргументов) 644 | if voice_input: 645 | voice_input_parts = voice_input.split(" ") 646 | 647 | # если было сказано одно слово - выполняем команду сразу без дополнительных аргументов 648 | if len(voice_input_parts) == 1: 649 | intent = get_intent(voice_input) 650 | if intent: 651 | config["intents"][intent]["responses"]() 652 | else: 653 | config["failure_phrases"]() 654 | 655 | # в случае длинной фразы - выполняется поиск ключевой фразы и аргументов через каждое слово, 656 | # пока не будет найдено совпадение 657 | if len(voice_input_parts) > 1: 658 | for guess in range(len(voice_input_parts)): 659 | intent = get_intent((" ".join(voice_input_parts[0:guess])).strip()) 660 | print(intent) 661 | if intent: 662 | command_options = [voice_input_parts[guess:len(voice_input_parts)]] 663 | print(command_options) 664 | config["intents"][intent]["responses"](*command_options) 665 | break 666 | if not intent and guess == len(voice_input_parts)-1: 667 | config["failure_phrases"]() 668 | 669 | # TODO food order 670 | # TODO recommend film by rating/genre (use recommendation system project) 671 | # как насчёт "название фильма"? Вот его описание:..... 672 | --------------------------------------------------------------------------------