""" Smart Speaker - Main Application Голосовой ассистент с wake word detection, STT, AI и TTS. Flow: 1. Wait for wake word ("Alexandr") 2. Listen to user speech (STT) 3. Send query to AI (Perplexity) 4. Clean response from markdown 5. Speak response (TTS) 6. Loop back to step 1 """ # Главный файл приложения (`main.py`). # Здесь находится основной бесконечный цикл, который связывает все компоненты воедино. import signal import sys from collections import deque # Импорт наших модулей from .audio.wakeword import ( wait_for_wakeword, cleanup as cleanup_wakeword, check_wakeword_once, stop_monitoring as stop_wakeword_monitoring, ) from .audio.stt import listen, cleanup as cleanup_stt, get_recognizer from .core.ai import ask_ai, translate_text from .core.cleaner import clean_response from .audio.tts import speak, initialize as init_tts from .audio.sound_level import set_volume, parse_volume_text from .features.alarm import get_alarm_clock # Список стоп-слов, чтобы прервать диалог или остановить ассистента STOP_WORDS = { "стоп", "хватит", "перестань", "замолчи", "прекрати", "тихо", "stop", } def signal_handler(sig, frame): """ Обработчик сигнала Ctrl+C. Позволяет корректно завершить работу программы, освободив ресурсы (микрофон, модели). """ print("\n\n👋 Завершение работы...") cleanup_wakeword() # Остановка Porcupine cleanup_stt() # Остановка Deepgram sys.exit(0) def parse_translation_request(text: str): """ Определяет, является ли фраза запросом на перевод. Пример: "Переведи на английский привет мир" Возвращает словарь: {'source_lang': 'ru', 'target_lang': 'en', 'text': 'привет мир'} Или None, если это не запрос перевода. """ text_lower = text.lower().strip() # Список префиксов команд перевода и соответствующих направлений языков commands = [ ("переведи на английский", "ru", "en"), ("переведи на русский", "en", "ru"), ("переведи с английского", "en", "ru"), ("переведи с русского", "ru", "en"), ("как по-английски", "ru", "en"), ("как по английски", "ru", "en"), ("как по-русски", "en", "ru"), ("как по русски", "en", "ru"), ("translate to english", "ru", "en"), ("translate into english", "ru", "en"), ("translate to russian", "en", "ru"), ("translate into russian", "en", "ru"), ("translate from english", "en", "ru"), ("translate from russian", "ru", "en"), ] for prefix, source_lang, target_lang in commands: if text_lower.startswith(prefix): # Отрезаем команду (префикс), оставляем только текст для перевода rest = text[len(prefix) :].strip() return { "source_lang": source_lang, "target_lang": target_lang, "text": rest, } return None def is_stop_command(text: str) -> bool: """ Проверяет, содержится ли в тексте команда остановки. Удаляет знаки препинания и ищет слова из списка STOP_WORDS. """ text_lower = text.lower() for ch in ",.!?:;": text_lower = text_lower.replace(ch, " ") words = text_lower.split() for word in words: if word in STOP_WORDS: return True return False def main(): """ Основная функция (точка входа). """ print("=" * 50) print("🔊 УМНАЯ КОЛОНКА") print("=" * 50) print("Скажите 'Alexandr' для активации") print("Нажмите Ctrl+C для выхода") print("=" * 50) print() # Устанавливаем перехватчик Ctrl+C signal.signal(signal.SIGINT, signal_handler) # Предварительная инициализация моделей (занимает пару секунд при старте) print("⏳ Инициализация моделей...") get_recognizer().initialize() # Подключение к Deepgram init_tts() # Загрузка нейросети для синтеза речи (Silero) alarm_clock = get_alarm_clock() # Загрузка будильников print() # История чата (храним последние 10 обменов репликами для контекста) chat_history = deque(maxlen=20) # Переменная для хранения последнего ответа ассистента last_response = None # Переменная, указывающая, нужно ли пропускать ожидание wake word # (True = режим диалога, слушаем сразу. False = ждем "Alexandr") skip_wakeword = False # БЕСКОНЕЧНЫЙ ЦИКЛ РАБОТЫ while True: try: # Гарантируем, что микрофон детектора wake word освобожден stop_wakeword_monitoring() # --- Проверка будильников --- # Проверяем каждую итерацию. Если будильник сработал, он заблокирует выполнение, пока его не выключат. if alarm_clock.check_alarms(): # Если будильник прозвенел и был выключен пользователем, сбрасываем режим диалога skip_wakeword = False continue # --- Шаг 1: Активация --- if not skip_wakeword: # Ожидание фразы "Alexandr". Используем таймаут 1 сек, чтобы часто проверять будильники. detected = wait_for_wakeword(timeout=1.0) # Если время вышло, а фразы не было — начинаем цикл заново (проверяем будильники) if not detected: continue # Фраза услышана! Слушаем команду пользователя (7 секунд тишины макс) user_text = listen(timeout_seconds=7.0) else: # Режим диалога (Follow-up): ждем продолжения речи без "Alexandr" print("👂 Слушаю продолжение диалога (5 сек)...") # Ждем начала речи 5 сек. Если начали говорить, слушаем до 10 сек. user_text = listen(timeout_seconds=10.0, detection_timeout=5.0) if not user_text: # Пользователь промолчал — выходим из режима диалога, засыпаем. skip_wakeword = False continue # --- Шаг 2: Анализ распознанного текста --- if not user_text: # Была активация, но речь не распознана speak("Извините, я вас не расслышал. Попробуйте ещё раз.") skip_wakeword = False # Возвращаемся в режим ожидания имени continue # Проверка на команду "Стоп" if is_stop_command(user_text): print("_" * 50) print("💤 Жду 'Alexandr' для активации...") skip_wakeword = False continue # Проверка на команду "Повтори" / "Еще раз" user_text_lower = user_text.lower().strip() repeat_phrases = [ "еще раз", "повтори", "скажи еще раз", "что ты сказал", "повтори пожалуйста", "александр еще раз", "еще раз александр", "александр повтори", "повтори александр", ] # Проверяем точное совпадение или если фраза начинается с "повтори" (но не "повтори за мной") if user_text_lower in repeat_phrases or ( user_text_lower.startswith("повтори") and "за мной" not in user_text_lower ): if last_response: print(f"🔁 Повторяю: {last_response}") speak(last_response) else: speak("Я еще ничего не говорил.") # После повтора остаемся в диалоге skip_wakeword = True continue # Проверка команд будильника ("поставь будильник на 7") alarm_response = alarm_clock.parse_command(user_text) if alarm_response: speak(alarm_response) last_response = alarm_response continue # Проверка команды громкости ("громкость 5") if user_text.lower().startswith("громкость"): try: # Убираем слово "громкость" и ищем число vol_str = user_text.lower().replace("громкость", "", 1).strip() level = parse_volume_text(vol_str) if level is not None: if set_volume(level): msg = f"Громкость установлена на {level}" speak(msg) last_response = msg else: speak("Не удалось установить громкость.") else: speak( "Я не понял число громкости. Скажите число от одного до десяти." ) continue except Exception as e: print(f"❌ Ошибка громкости: {e}") speak("Не удалось изменить громкость.") continue # Проверка запроса на перевод translation_request = parse_translation_request(user_text) if translation_request: source_lang = translation_request["source_lang"] target_lang = translation_request["target_lang"] text_to_translate = translation_request["text"] # Если сказано только "переведи на английский", спрашиваем "что перевести?" if not text_to_translate: prompt = ( "Скажи фразу на английском." if source_lang == "en" else "Скажи фразу на русском." ) speak(prompt) # Слушаем саму фразу на нужном языке text_to_translate = listen( timeout_seconds=7.0, detection_timeout=5.0, lang=source_lang ) if not text_to_translate: speak("Я не расслышал текст для перевода.") skip_wakeword = False continue # Выполняем перевод через AI translated_text = translate_text( text_to_translate, source_lang, target_lang ) # Очищаем результат (убираем лишние символы) clean_text = clean_response(translated_text, language=target_lang) # Сохраняем для повтора last_response = clean_text # Озвучиваем перевод на целевом языке completed = speak( clean_text, check_interrupt=check_wakeword_once, language=target_lang, ) stop_wakeword_monitoring() skip_wakeword = True # Остаемся в диалоге if not completed: print("⏹️ Перевод прерван - слушаю следующий вопрос") continue # --- Шаг 3: Запрос к AI (обычный чат) --- # Добавляем сообщение пользователя в историю chat_history.append({"role": "user", "content": user_text}) # Отправляем историю диалога в Perplexity ai_response = ask_ai(list(chat_history)) # Добавляем ответ AI в историю chat_history.append({"role": "assistant", "content": ai_response}) # --- Шаг 4: Очистка ответа --- # Убираем Markdown (**жирный**, *курсив*) и готовим числа для озвучки clean_text = clean_response(ai_response, language="ru") # Сохраняем последний ответ для функции "еще раз" last_response = clean_text # --- Шаг 5: Озвучка ответа --- # check_interrupt=check_wakeword_once позволяет прервать речь, сказав "Alexandr" completed = speak( clean_text, check_interrupt=check_wakeword_once, language="ru" ) # После озвучки обязательно закрываем поток микрофона, который открывался для проверки прерывания stop_wakeword_monitoring() # Включаем режим диалога (следующий запрос можно говорить без имени) skip_wakeword = True if not completed: print("⏹️ Ответ прерван - слушаю следующий вопрос") # Если перебили, значит есть новый вопрос, сразу слушаем его (цикл перезапустится) pass print() print("-" * 30) print() # --- Шаг 6: Конец итерации, возврат в начало цикла --- except KeyboardInterrupt: signal_handler(None, None) except Exception as e: print(f"❌ Ошибка: {e}") speak("Произошла ошибка. Попробуйте ещё раз.") skip_wakeword = False if __name__ == "__main__": main()