diff --git a/.qwen/settings.json b/.qwen/settings.json new file mode 100644 index 0000000..8521d2b --- /dev/null +++ b/.qwen/settings.json @@ -0,0 +1,11 @@ +{ + "$version": 3, + "model": { + "name": "coder-model" + }, + "general": { + "checkpointing": { + "enabled": true + } + } +} diff --git a/11.py b/11.py new file mode 100644 index 0000000..a58b13d --- /dev/null +++ b/11.py @@ -0,0 +1,13 @@ +def f(x, p1, p2): + if x == 3: + return 1 + elif x == 38: + p1 += 1 + elif x == 18: + p2 += 1 + else: + if p2 >= 1 or p1 >= 1: + return f(x - 3, p1, p2) + f(x // 3, p1, p2) + f(x - 5, p1, p2) + + +print(f(80, 0, 0)) diff --git a/app/core/ai.py b/app/core/ai.py index 2305769..9751515 100644 --- a/app/core/ai.py +++ b/app/core/ai.py @@ -1,7 +1,4 @@ -"""AI module with pluggable providers.""" - -# Модуль общения с искусственным интеллектом. -# Обрабатывает запросы пользователя и переводы через выбранный API-провайдер. +"""AI module.""" import json import re @@ -31,8 +28,7 @@ from .config import ( _HTTP = requests.Session() -# Системный промпт (инструкция) для AI. -# Задает личность ассистента: имя "Александр", стиль общения, краткость. +# Системный промпт SYSTEM_PROMPT = """Ты — Александр, умный голосовой ассистент с человеческим поведением. Веди себя как живой человек: будь дружелюбным, естественным и немного эмоциональным, где это уместно. Твоя главная цель — помогать пользователю и поддерживать интересный диалог. @@ -45,8 +41,7 @@ SYSTEM_PROMPT += ( '"language":"ru","style":["дружелюбный","естественный","краткий"],"format":"plain"}' ) -# Системный промпт для режима переводчика. -# Требует возвращать ТОЛЬКО перевод, без лишних слов ("Конечно, вот перевод..."). +# Промпт для перевода TRANSLATION_SYSTEM_PROMPT = """You are a translation engine. Translate from {source} to {target}. Return 2-3 short translation variants only. @@ -69,9 +64,7 @@ _PROVIDER_ALIASES = { "zai": "zai", } -# В реальном .env у пользователя должен быть активен только один AI-ключ. -# Поэтому настройки храним в одном словаре, а ниже отдельно проверяем конфликт -# конфигурации, чтобы ассистент не делал "лучшее предположение" молча. +# В .env нужен только один AI-ключ _PROVIDER_SETTINGS = { "perplexity": { "provider": "perplexity", @@ -140,8 +133,8 @@ def _normalize_provider_name(provider_name: str) -> str: def _get_provider_settings(): - # Сначала ищем реально активные ключи. Это главный источник истины: - # если ключ один, используем именно его, даже если AI_PROVIDER указан иначе. + """Определяет какой AI провайдер использовать.""" + # Ищем активные ключи configured = [ cfg for cfg in _PROVIDER_SETTINGS.values() @@ -150,7 +143,11 @@ def _get_provider_settings(): if len(configured) == 1: cfg = configured[0] requested = _normalize_provider_name(AI_PROVIDER) - if requested and requested in _PROVIDER_SETTINGS and requested != cfg["provider"]: + if ( + requested + and requested in _PROVIDER_SETTINGS + and requested != cfg["provider"] + ): print( f"⚠️ AI_PROVIDER={AI_PROVIDER!r} не совпадает с единственным " f"активным ключом {cfg['name']}. Используем {cfg['name']}." @@ -223,6 +220,7 @@ def _build_headers(cfg): def _split_system_messages(messages): + """Извлекает system prompt из списка сообщений.""" system_parts = [] chat_messages = [] @@ -238,7 +236,6 @@ def _split_system_messages(messages): role = "user" chat_messages.append({"role": role, "content": content}) - # Anthropic хранит системную инструкцию отдельно от обычной истории чата. return "\n\n".join(system_parts), chat_messages diff --git a/app/core/cleaner.py b/app/core/cleaner.py index 659be73..449c870 100644 --- a/app/core/cleaner.py +++ b/app/core/cleaner.py @@ -1,24 +1,13 @@ -""" -Response cleaner module. -Removes markdown formatting and special characters from AI responses. -Handles complex number-to-text conversion for Russian language. -""" - -# Модуль очистки текста перед озвучкой. -# 1. Убирает Markdown (жирный шрифт, ссылки), который генерирует AI, чтобы робот не читал спецсимволы. -# 2. Преобразует числа в слова ("5 мая" -> "пятого мая", "5 рублей" -> "пять рублей"). -# Это критически важно для качественного русского TTS. +"""Text cleaner for TTS.""" import re import pymorphy3 from num2words import num2words from .roman import roman_to_int -# Инициализация морфологического анализатора (для определения падежей) morph = pymorphy3.MorphAnalyzer() -# Карта предлогов и падежей. -# Помогает понять, в какой падеж ставить число после предлога. +# Предлоги и падежи PREPOSITION_CASES = { "в": "loct", # В ком/чем? (Предложный) или Винительный. Часто loct для годов. "во": "loct", @@ -55,7 +44,7 @@ PREPOSITION_CASES = { "про": "accs", } -# Соответствие падежей pymorphy и библиотеки num2words +# Соответствие падежей PYMORPHY_TO_NUM2WORDS = { "nomn": "nominative", "gent": "genitive", @@ -69,14 +58,14 @@ PYMORPHY_TO_NUM2WORDS = { "loc2": "prepositional", } -# Соответствие родов pymorphy и num2words +# Роды PYMORPHY_TO_GENDER = { "masc": "m", "femn": "f", "neut": "n", } -# Названия месяцев в родительном падеже (для поиска дат в тексте) +# Месяца MONTHS_GENITIVE = [ "января", "февраля", @@ -92,10 +81,10 @@ MONTHS_GENITIVE = [ "декабря", ] -# Леммы единиц времени (для корректного падежа числительных) +# Время TIME_UNIT_LEMMAS = {"час", "минута", "секунда"} -# Суффиксы порядковых числительных для формата "1968-й", "1968-го", "1968-му", "1968-м" и т.п. +# Суффиксы порядковых _ORDINAL_SUFFIX_MAP = { # Masculine "ого": ("genitive", "m"), @@ -126,20 +115,15 @@ _ORDINAL_SUFFIX_MAP = { def get_case_from_preposition(prep_token): - """Определяет падеж по предлогу.""" + """Падеж по предлогу.""" if not prep_token: return None return PREPOSITION_CASES.get(prep_token.lower()) def convert_number(number_str, context_type="cardinal", case="nominative", gender="m"): - """ - Обертка над num2words для конвертации числа в строку. - cardinal - количественное (один, два) - ordinal - порядковое (первый, второй) - """ + """Число в слова.""" try: - # Обработка дробей (замена запятой на точку) if "." in number_str or "," in number_str: num_val = float(number_str.replace(",", ".")) else: @@ -152,20 +136,18 @@ def convert_number(number_str, context_type="cardinal", case="nominative", gende def numbers_to_words(text: str) -> str: - """ - Интеллектуальная замена цифр на слова с учетом контекста (даты, года, падежи). - """ + """Замена цифр на слова.""" if not text: return "" preps_list = "|".join(map(re.escape, PREPOSITION_CASES.keys())) - # 0. Обработка короткой записи годов с суффиксом: "1968-й", "1968-го", "1968-му", "1968-м", "в 1968-м году" + # Года с суффиксом def replace_year_suffix_match(match): - prep = match.group(1) # Предлог (в, во, о...) - year_str = match.group(2) # Само число - suffix = match.group(3) # Суффикс порядкового числительного - year_word = match.group(4) # Слово "год", "году" и т.д. (опционально) + prep = match.group(1) + year_str = match.group(2) + suffix = match.group(3) + year_word = match.group(4) case = None gender = None @@ -191,7 +173,9 @@ def numbers_to_words(text: str) -> str: if not gender: gender = "m" - words = convert_number(year_str, context_type="ordinal", case=case, gender=gender) + words = convert_number( + year_str, context_type="ordinal", case=case, gender=gender + ) prefix = f"{prep} " if prep else "" if year_word: @@ -206,25 +190,23 @@ def numbers_to_words(text: str) -> str: text, ) - # 1. Обработка годов: "в 1999 году", "2024 год" + # Года def replace_year_match(match): - prep = match.group(1) # Предлог (в, с, к...) - year_str = match.group(2) # Само число - year_word = match.group(3) # Слово "год", "году" и т.д. + prep = match.group(1) + year_str = match.group(2) + year_word = match.group(3) - # Определяем падеж слова "год" через pymorphy + # Падеж parsed = morph.parse(year_word)[0] case_tag = parsed.tag.case nw_case = PYMORPHY_TO_NUM2WORDS.get(case_tag, "nominative") - # FIX: Pymorphy часто определяет "год" как accs (винительный), что для num2words - # превращается в родительный (для одушевленных?), давая "2024 года". - # Если предлога нет, принудительно ставим именительный. + # Без предлога - именительный if not prep and year_word.lower().startswith("год"): nw_case = "nominative" - # Конвертируем число в порядковое числительное (тысяча девятьсот девяносто девятом) + # Конвертируем words = convert_number( year_str, context_type="ordinal", case=nw_case, gender="m" ) @@ -239,7 +221,7 @@ def numbers_to_words(text: str) -> str: text, ) - # 2. Обработка дат: "25 июня", "с 1 мая" + # Даты month_regex = "|".join(MONTHS_GENITIVE) def replace_date_match(match): @@ -247,7 +229,7 @@ def numbers_to_words(text: str) -> str: day_str = match.group(2) month_word = match.group(3) - # По умолчанию родительный падеж ("двадцать пятого июня") + # По умолчанию родительный case = "genitive" if prep: @@ -266,7 +248,7 @@ def numbers_to_words(text: str) -> str: if morph_case: case = PYMORPHY_TO_NUM2WORDS.get(morph_case, "genitive") - # Используем средний род ('n') для дат (число - средний род: пятое, пятого) + # Средний род для дат words = convert_number(day_str, context_type="ordinal", case=case, gender="n") prefix = f"{prep} " if prep else "" @@ -279,7 +261,7 @@ def numbers_to_words(text: str) -> str: text, ) - # 3. Обработка всех остальных чисел (Количественные: пять столов, десять минут) + # Остальные числа def replace_cardinal_match(match): prep = match.group(1) num_str = match.group(2) @@ -294,7 +276,7 @@ def numbers_to_words(text: str) -> str: if morph_case: case = PYMORPHY_TO_NUM2WORDS.get(morph_case, "nominative") - # Если есть следующее слово, проверяем его род (для "2 минуты" -> "две") + # Проверяем род if next_word: word_clean = next_word.strip() parsed = morph.parse(word_clean)[0] @@ -302,21 +284,19 @@ def numbers_to_words(text: str) -> str: morph_gender = parsed.tag.gender gender = PYMORPHY_TO_GENDER.get(morph_gender, "m") - # Спец-случай: "на 1 час" -> "на один час" (не "одного") - # Для неодушевленных муж./ср. рода в винительном падеже - # числительные должны совпадать с именительным. - if ( - prep_clean == "на" - and parsed.normal_form in TIME_UNIT_LEMMAS - and parsed.tag.gender in ("masc", "neut") - ): - case = "nominative" + # Спец-случай: "на 1 час" + if ( + prep_clean == "на" + and parsed.normal_form in TIME_UNIT_LEMMAS + and parsed.tag.gender in ("masc", "neut") + ): + case = "nominative" words = convert_number( num_str, context_type="cardinal", case=case, gender=gender ) - # Если конвертация вернула пустую строку (сбой?), возвращаем цифры + # Если конвертация не удалась - возвращаем цифры if not words: words = num_str @@ -336,11 +316,7 @@ def numbers_to_words(text: str) -> str: def roman_numerals_to_words(text: str) -> str: - """ - Преобразует римские цифры в порядковые числительные с учетом - морфологии предыдущего слова. - Пример: "Ивана III" -> "Ивана третьего". - """ + """Римские в слова.""" if not text: return "" @@ -380,63 +356,53 @@ def roman_numerals_to_words(text: str) -> str: def clean_response(text: str, language: str = "ru") -> str: - """ - Основная функция очистки. - Убирает Markdown, ссылки, мусор и преобразует числа. - - Args: - text: Сырой текст от AI. - language: Язык (для конвертации чисел, работает только для ru). - """ + """Очистка текста для TTS.""" if not text: return "" - # Удаление ссылок на источники [1], [citation needed] + # Удаление ссылок text = re.sub(r"\x5B\d+\x5D", "", text) text = re.sub(r"\x5Bcitation\s*needed\x5D", "", text, flags=re.IGNORECASE) text = re.sub(r"\x5Bsource\x5D", "", text, flags=re.IGNORECASE) - # Удаление жирного шрифта **text** и __text__ + # Удаление жирного text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) text = re.sub(r"__(.+?)__", r"\1", text) - # Удаление курсива *text* и _text_ + # Удаление курсива text = re.sub(r"\*(.+?)\*", r"\1", text) text = re.sub(r"(? удаляем полностью + # Картинки text = re.sub(r"!\x5B([^\x5D]*)\x5D\([^)]+\)", "", text) - # Удаление ссылок [text](url) -> оставляем только text - # \x5B = [, \x5D = ] + # Ссылки text = re.sub(r"\x5B([^\x5D]+)\x5D\([^)]+\)", r"\1", text) - # Удаление inline кода `code` + # Код text = re.sub(r"`([^`]+)`", r"\1", text) - - # Удаление блоков кода ```code``` text = re.sub(r"```[\s\S]*?```", "", text) - # Удаление маркеров списков (-, *, 1.) + # Списки text = re.sub(r"^\s*[-*+]\s+", "", text, flags=re.MULTILINE) text = re.sub(r"^\s*\d+\.\s+", "", text, flags=re.MULTILINE) - # Удаление цитат > + # Цитаты text = re.sub(r"^\s*>\s*", "", text, flags=re.MULTILINE) - # Удаление горизонтальных линий --- + # Линии text = re.sub(r"^[-*_]{3,}\s*$", "", text, flags=re.MULTILINE) - # Удаление HTML тегов + # HTML теги text = re.sub(r"<[^>]+>", "", text) - # Удаление фразы "— это, скорее всего" в корректировках произношения + # Корректировки text = re.sub( r"([—-])\s*это,\s*скорее\s*всего\b\s*,?\s*", r"\1 ", @@ -445,7 +411,7 @@ def clean_response(text: str, language: str = "ru") -> str: ) text = re.sub(r"[—-]\s*([.!?])", r"\1", text) - # Remove informal slang greetings at the beginning of sentences/responses + # Удаление сленга text = re.sub( r"^(Эй|Хэй|Слушай|Так|Ну|Короче|В\s+общем)[,!?:]?\s*", "", @@ -453,13 +419,13 @@ def clean_response(text: str, language: str = "ru") -> str: flags=re.IGNORECASE | re.MULTILINE, ) - # Convert Roman numerals and Arabic digits to words for Russian. + # Числа в слова if language == "ru": text = roman_numerals_to_words(text) if re.search(r"\d", text): text = numbers_to_words(text) - # Remove extra whitespace + # Чистка пробелов text = re.sub(r"\n{3,}", "\n\n", text) text = re.sub(r" +", " ", text) diff --git a/app/core/smalltalk.py b/app/core/smalltalk.py index 7edd27d..526902c 100644 --- a/app/core/smalltalk.py +++ b/app/core/smalltalk.py @@ -1,6 +1,4 @@ -""" -Short, human-like responses for small talk. -""" +"""Small talk responses.""" from __future__ import annotations diff --git a/app/features/timer.py b/app/features/timer.py index 3d5c23f..32ad485 100644 --- a/app/features/timer.py +++ b/app/features/timer.py @@ -1,8 +1,5 @@ """Timer module.""" -# Модуль таймера. -# Отвечает за установку таймеров (в оперативной памяти), их проверку и воспроизведение звука. - import subprocess import re import json @@ -25,7 +22,7 @@ ALARM_SOUND = BASE_DIR / "assets" / "sounds" / "Apex-1.mp3" TIMER_FILE = BASE_DIR / "data" / "timers.json" ASK_TIMER_TIME_PROMPT = "На какое время мне поставить таймер?" -# --- Number words parsing helpers (ru) --- +# Числа словами _NUMBER_UNITS = { "ноль": 0, "один": 1, @@ -103,13 +100,19 @@ _UNIT_LEMMAS = { "мин": "minutes", "сек": "seconds", } +_UNIT_LEMMAS = { + "час": "hours", + "минута": "minutes", + "секунда": "seconds", + "мин": "minutes", + "сек": "seconds", +} _UNIT_FORMS = { "hours": ("час", "часа", "часов"), "minutes": ("минуту", "минуты", "минут"), "seconds": ("секунду", "секунды", "секунд"), } -# Optional ordinal formatting for list numbering. try: from num2words import num2words except Exception: @@ -251,12 +254,11 @@ def _format_ordinal_index(index: int) -> str: class TimerManager: def __init__(self): - # Список активных таймеров: {"end_time": datetime, "label": str} self.timers = [] self.load_timers() def load_timers(self): - """Загрузка списка таймеров из JSON файла.""" + """Загрузка из файла.""" if TIMER_FILE.exists(): try: with open(TIMER_FILE, "r", encoding="utf-8") as f: @@ -277,7 +279,7 @@ class TimerManager: self.timers = sorted(timers, key=lambda x: x["end_time"]) def save_timers(self): - """Сохранение списка таймеров в JSON файл.""" + """Сохранение в файл.""" payload = [ {"end_time": t["end_time"].isoformat(), "label": t.get("label", "")} for t in self.timers @@ -289,7 +291,7 @@ class TimerManager: print(f"❌ Ошибка сохранения таймеров: {e}") def describe_timers(self) -> str: - """Возвращает текстовое описание активных таймеров.""" + """Описание активных таймеров.""" if not self.timers: return "Активных таймеров нет." @@ -312,38 +314,29 @@ class TimerManager: return "Активные таймеры: " + "; ".join(items) + "." def add_timer(self, seconds: int, label: str): - """Добавление нового таймера.""" + """Добавить таймер.""" end_time = datetime.now() + timedelta(seconds=seconds) self.timers.append({"end_time": end_time, "label": label}) - # Сортируем, чтобы ближайший был первым self.timers.sort(key=lambda x: x["end_time"]) self.save_timers() - print(f"⏳ Таймер установлен на {label} (до {end_time.strftime('%H:%M:%S')})") + print(f"⏳ Таймер: {label} (до {end_time.strftime('%H:%M:%S')})") def cancel_all_timers(self): - """Отмена всех таймеров.""" + """Отменить все таймеры.""" count = len(self.timers) self.timers = [] self.save_timers() - print(f"🔕 Все таймеры ({count}) отменены.") + print(f"🔕 Таймеры отменены: {count}") def check_timers(self): - """ - Проверка: не истек ли какой-то таймер? - Вызывается в главном цикле. - Возвращает True, если таймер сработал (и был обработан). - """ + """Проверка таймеров. Возвращает True если сработал.""" if not self.timers: return False now = datetime.now() - # Смотрим первый (самый ранний) таймер - # Используем индекс 0, так как список отсортирован first_timer = self.timers[0] if now >= first_timer["end_time"]: - # Таймер сработал! - # Удаляем его из списка label = first_timer["label"] self.timers.pop(0) self.save_timers() @@ -355,36 +348,30 @@ class TimerManager: return False def trigger_timer(self, label: str): - """ - Логика срабатывания таймера. - Запускает воспроизведение MP3 и слушает команду "Стоп". - """ - print(f"🔔 ТАЙМЕР НА {label} СРАБОТАЛ! (Скажите 'Стоп')") + """Срабатывание таймера.""" + print(f"🔔 ТАЙМЕР {label}!") - # Запуск плеера mpg123 в бесконечном цикле cmd = ["mpg123", "-q", "--loop", "-1", str(ALARM_SOUND)] try: process = subprocess.Popen(cmd) except FileNotFoundError: - print( - "❌ Ошибка: mpg123 не найден. Установите его: sudo apt install mpg123" - ) + print("❌ mpg123 не найден. Установите: sudo apt install mpg123") return try: - # Цикл ожидания стоп-команды while True: - text = listen(timeout_seconds=3.0, detection_timeout=3.0, fast_stop=True) + text = listen( + timeout_seconds=3.0, detection_timeout=3.0, fast_stop=True + ) if text: if is_stop_command(text, mode="lenient"): - print(f"🛑 Таймер остановлен по команде: '{text}'") + print(f"🛑 Остановлен: '{text}'") break except Exception as e: - print(f"❌ Ошибка во время таймера: {e}") + print(f"❌ Ошибка: {e}") finally: - # Обязательно убиваем процесс плеера process.terminate() try: process.wait(timeout=1) @@ -393,12 +380,9 @@ class TimerManager: print("🔕 Таймер выключен.") def parse_command(self, text: str) -> str | None: - """ - Парсинг команды установки таймера. - Примеры: "таймер на 5 минут", "засеки 10 секунд". - """ + """Парсинг команды таймера.""" text = _normalize_timer_text(text.lower()) - + # Ключевые слова для таймера if not any(word in text for word in ["таймер", "засеки", "поставь таймер"]): return None @@ -413,9 +397,6 @@ class TimerManager: return "Хорошо, все таймеры отменены." # Поиск времени - # Ищем комбинации: число + (час/мин/сек) - # Пример: "1 час 30 минут", "5 минут", "30 секунд" - total_seconds = 0 parts = [] hours = None @@ -438,7 +419,7 @@ class TimerManager: if match_seconds: seconds = int(match_seconds.group(1)) - # Дополняем числительные словами (например, "одну минуту") + # Числа словами word_values = _extract_word_time_values(text) if hours is None and word_values["hours"] is not None: hours = word_values["hours"] @@ -456,9 +437,7 @@ class TimerManager: found_time = any(value is not None for value in [hours, minutes, seconds]) if found_time: - total_seconds = ( - (hours or 0) * 3600 + (minutes or 0) * 60 + (seconds or 0) - ) + total_seconds = (hours or 0) * 3600 + (minutes or 0) * 60 + (seconds or 0) if has_fractional: total_seconds = int(round(total_seconds)) h = total_seconds // 3600 @@ -480,16 +459,17 @@ class TimerManager: label = " ".join(parts) self.add_timer(total_seconds, label) return f"Поставил таймер на {label}." - - # Если попросили поставить таймер, но не назвали время — задаем уточняющий вопрос. - if re.search(r"(постав|установ|запусти|включи|засеки)", text) or text.strip() in { + + # Если время не названо — спрашиваем + if re.search( + r"(постав|установ|запусти|включи|засеки)", text + ) or text.strip() in { "таймер", "поставь таймер", }: return ASK_TIMER_TIME_PROMPT - # Если сказали "таймер", но не нашли время. - return "Я не понял, на сколько поставить таймер. Скажите, например, 'таймер на 5 минут'." + return "Я не понял, на сколько поставить таймер." # Глобальный экземпляр diff --git a/app/main.py b/app/main.py index 271d808..8217c61 100644 --- a/app/main.py +++ b/app/main.py @@ -1,18 +1,8 @@ """ 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 os import os import queue @@ -32,7 +22,7 @@ except Exception as exc: else: _MIXER_IMPORT_ERROR = None -# Импорт наших модулей +# Наши модули from .audio.sound_level import parse_volume_text, set_volume from .audio.stt import cleanup as cleanup_stt from .audio.stt import get_recognizer, listen @@ -174,11 +164,10 @@ _CITY_PATTERNS = [ ), ] + def signal_handler(sig, frame): - """ - Обработчик сигнала Ctrl+C. - Позволяет корректно завершить работу программы, освободив ресурсы (микрофон, модели). - """ + """Обработчик Ctrl+C.""" + print("\n\n👋 Завершение работы...") print("\n\n👋 Завершение работы...") try: cleanup_wakeword() # Остановка Porcupine @@ -192,13 +181,8 @@ def signal_handler(sig, frame): def parse_translation_request(text: str): - """ - Определяет, является ли фраза запросом на перевод. - - Пример: "Переведи на английский привет мир" - Возвращает словарь: {'source_lang': 'ru', 'target_lang': 'en', 'text': 'привет мир'} - Или None, если это не запрос перевода. - """ + """Проверяет, является ли фраза запросом на перевод.""" + text_lower = text.lower().strip() text_lower = text.lower().strip() # Список префиксов команд перевода и соответствующих направлений языков. # Важно: более длинные префиксы должны проверяться первыми (например, @@ -217,9 +201,8 @@ def parse_translation_request(text: str): def main(): - """ - Основная функция (точка входа). - """ + """Точка входа.""" + print("=" * 50) print("=" * 50) print("🔊 УМНАЯ КОЛОНКА") print("=" * 50) @@ -231,7 +214,6 @@ def main(): # Устанавливаем перехватчик Ctrl+C signal.signal(signal.SIGINT, signal_handler) - # Предварительная инициализация моделей print("⏳ Инициализация моделей...") # Инициализация звуковой системы для эффектов (опционально) @@ -262,70 +244,61 @@ def main(): cities_game = get_cities_game() # Игра "Города" print() - # История чата (храним последние 10 обменов репликами для контекста) + # История чата chat_history = deque(maxlen=20) - # Переменная для хранения последнего ответа ассистента + # Последний ответ ассистента last_response = None - # Переменная, указывающая, нужно ли пропускать ожидание wake word - # (True = режим диалога, слушаем сразу. False = ждем "Alexandr") + # Режим диалога (без wake word) skip_wakeword = False - # После ответа ассистент ждет продолжение фразы 4 секунды. - # Если речи нет, выходим из диалога и снова ждем wake word. followup_idle_timeout_seconds = 4.0 - # Контекст уточнения "на какое время поставить ...". - # Может быть: "timer", "alarm". + # Контекст уточнения времени для таймера/будильника pending_time_target = None - # Переменная для отслеживания последней проверки здоровья STT + # Проверка здоровья STT last_stt_check = time.time() - # БЕСКОНЕЧНЫЙ ЦИКЛ РАБОТЫ + # ГЛАВНЫЙ ЦИКЛ while True: - # Периодическая проверка здоровья STT каждые 10 минут - if time.time() - last_stt_check > 600: # 10 минут = 600 секунд + # Периодическая проверка STT + if time.time() - last_stt_check > 600: try: recognizer = get_recognizer() - if hasattr(recognizer, 'check_connection_health'): + if hasattr(recognizer, "check_connection_health"): recognizer.check_connection_health() last_stt_check = time.time() except Exception as e: - print(f"Ошибка при проверке здоровья STT: {e}") + print(f"Ошибка при проверке STT: {e}") try: - # Гарантируем, что микрофон детектора wake word освобожден + # Освобождаем микрофон wake word stop_wakeword_monitoring() - # --- Проверка таймеров --- - # Проверяем каждую итерацию. Если таймер сработал, он заблокирует выполнение, пока его не выключат. + # Проверяем таймеры if timer_manager.check_timers(): skip_wakeword = False continue - # --- Проверка будильников --- - # Проверяем каждую итерацию. Если будильник сработал, он заблокирует выполнение, пока его не выключат. + # Проверяем будильники if alarm_clock.check_alarms(): - # Если будильник прозвенел и был выключен пользователем, сбрасываем режим диалога skip_wakeword = False continue - # --- Шаг 1: Активация --- + # Ждем wake word if not skip_wakeword: - # Ожидание фразы "Alexandr". Используем таймаут 0.5 сек, чтобы чаще проверять будильники. detected = wait_for_wakeword(timeout=0.5) - # Если время вышло, а фразы не было — начинаем цикл заново (проверяем будильники) + # Если время вышло — проверяем будильники if not detected: continue - # Воспроизводим звук активации + # Звук активации if ding_sound: ding_sound.play() - # Фраза активации услышана: - # до 5с ждём начало речи, после начала завершаем STT по 3с тишины. + # Слушаем команду try: user_text = listen(timeout_seconds=5.0, fast_stop=True) except Exception as e: @@ -338,12 +311,8 @@ def main(): print(f"Ошибка переинициализации STT: {init_error}") continue # Продолжаем цикл else: - # Режим диалога (Follow-up): ждем продолжения речи без "Alexandr" - print( - "👂 Слушаю продолжение диалога " - f"({followup_idle_timeout_seconds:.0f} сек)..." - ) - # Ждем начала речи 4 сек. Если начали говорить, слушаем до 7 сек. + # Follow-up режим — без wake word + print(f"👂 Слушаю ({followup_idle_timeout_seconds:.0f} сек)...") try: user_text = listen( timeout_seconds=7.0, @@ -362,13 +331,12 @@ def main(): continue if not user_text: - # Пользователь промолчал — выходим из режима диалога, засыпаем. + # Молчание — возвращаемся к ожиданию skip_wakeword = False continue - # --- Шаг 2: Анализ распознанного текста --- + # Анализ текста if not user_text: - # Пустой ввод: без лишних ответов возвращаемся к ожиданию wake word. skip_wakeword = False continue @@ -384,13 +352,12 @@ def main(): skip_wakeword = False continue print("_" * 50) - print("💤 Жду 'Alexandr' для активации...") + print("💤 Жду 'Alexandr'...") skip_wakeword = False continue - # Проверка на команду "Повтори" / "Еще раз" + # Проверка на "Повтори" user_text_lower = user_text.lower().strip() - # Проверяем точное совпадение или если фраза начинается с "повтори" (но не "повтори за мной") if user_text_lower in _REPEAT_PHRASES or ( user_text_lower.startswith("повтори") and "за мной" not in user_text_lower @@ -400,11 +367,10 @@ def main(): speak(last_response) else: speak("Я еще ничего не говорил.") - # После повтора остаемся в диалоге skip_wakeword = True continue - # Короткие ответы на small-talk ("как дела" и т.п.) + # Small-talk smalltalk_response = get_smalltalk_response(user_text) if smalltalk_response: clean_smalltalk = clean_response(smalltalk_response, language="ru") @@ -424,7 +390,7 @@ def main(): ): command_text = f"будильник {command_text}" - # Проверка команд таймера ("поставь таймер на 6 минут") + # Таймеры stopwatch_response = stopwatch_manager.parse_command(command_text) if stopwatch_response: clean_stopwatch_response = clean_response( @@ -435,7 +401,7 @@ def main(): skip_wakeword = True continue - # Проверка команд таймера ("поставь таймер на 6 минут") + # Таймер timer_response = timer_manager.parse_command(command_text) if timer_response: clean_timer_response = clean_response(timer_response, language="ru") @@ -449,7 +415,7 @@ def main(): skip_wakeword = not completed continue - # Проверка команд будильника ("поставь будильник на 7") + # Будильник alarm_response = alarm_clock.parse_command(command_text) if alarm_response: clean_alarm_response = clean_response(alarm_response, language="ru") @@ -461,10 +427,9 @@ def main(): skip_wakeword = alarm_response == ASK_ALARM_TIME_PROMPT continue - # Проверка команды громкости ("громкость 5") + # Громкость if user_text.lower().startswith("громкость"): try: - # Убираем слово "громкость" и ищем число vol_str = user_text.lower().replace("громкость", "", 1).strip() level = parse_volume_text(vol_str) @@ -489,32 +454,31 @@ def main(): skip_wakeword = True continue - # Проверка команды "Погода" - # Проверяем, содержит ли запрос информацию о конкретном городе + # Погода requested_city = None user_text_lower = user_text.lower() - # Проверяем наличие упоминания города в запросе (например, "погода в Нью-Йорке", "какая погода в Москве") for pattern in _CITY_PATTERNS: match = pattern.search(user_text_lower) if match: potential_city = match.group(1).strip() - # Проверяем, что это не местоимение или другое слово, а реально название города if ( potential_city and len(potential_city) > 1 - and not any(word in potential_city for word in _CITY_INVALID_WORDS) + and not any( + word in potential_city for word in _CITY_INVALID_WORDS + ) ): - requested_city = potential_city.title() # Приводим к формату "Нью-Йорк", "Москва" + requested_city = potential_city.title() break - # Проверяем, содержит ли запрос одну из погодных команд has_weather_trigger = any( trigger in user_text_lower for trigger in _WEATHER_TRIGGERS ) if has_weather_trigger: from .features.weather import get_weather_report + weather_report = get_weather_report(requested_city) clean_report = clean_response(weather_report, language="ru") speak(clean_report) @@ -522,7 +486,7 @@ def main(): skip_wakeword = True continue - # Проверка музыкальных команд ("включи музыку", "пауза", и т.д.) + # Музыка music_controller = get_music_controller() music_response = music_controller.parse_command(user_text) if music_response: @@ -532,14 +496,14 @@ def main(): skip_wakeword = True 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 = ( "Скажи фразу на английском." @@ -547,7 +511,6 @@ def main(): else "Скажи фразу на русском." ) speak(prompt) - # Слушаем саму фразу на нужном языке try: text_to_translate = listen( timeout_seconds=7.0, detection_timeout=5.0, lang=source_lang @@ -569,31 +532,30 @@ def main(): skip_wakeword = False continue - # Выполняем перевод через AI + # Перевод через 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 # Остаемся в диалоге + skip_wakeword = True if not completed: - print("⏹️ Перевод прерван - слушаю следующий вопрос") + print("⏹️ Перевод прерван") continue # Игра "Города" cities_response = cities_game.handle(user_text) + cities_response = cities_game.handle(user_text) if cities_response: clean_cities_response = clean_response(cities_response, language="ru") speak(clean_cities_response) @@ -601,41 +563,36 @@ def main(): skip_wakeword = True continue - # --- Шаг 3: Запрос к AI (Streaming) --- - # Добавляем сообщение пользователя в историю + # AI запрос chat_history.append({"role": "user", "content": user_text}) - # Очередь для предложений, которые нужно озвучить + # Очередь для TTS tts_q = queue.Queue() - # Флаг прерывания для worker-а interrupt_event = threading.Event() def tts_worker(): - """Фоновый поток, читающий предложения из очереди и озвучивающий их.""" + """Фоновый поток для озвучки.""" while True: item = tts_q.get() - if item is None: # Poison pill (сигнал остановки) + if item is None: tts_q.task_done() break text, lang = item - # Если уже было прерывание, просто пропускаем (чистим очередь) if interrupt_event.is_set(): tts_q.task_done() continue - # Озвучиваем completed = speak( text, check_interrupt=check_wakeword_once, language=lang ) if not completed: - interrupt_event.set() # Сообщаем всем, что нас перебили + interrupt_event.set() tts_q.task_done() - # Запускаем поток озвучки worker_thread = threading.Thread(target=tts_worker, daemon=True) worker_thread.start() @@ -643,13 +600,12 @@ def main(): buffer = "" try: - # Получаем генератор потока от AI + # Streaming от AI stream_generator = ask_ai_stream(list(chat_history)) - print("🤖 AI говорит: ", end="", flush=True) + print("🤖 AI: ", end="", flush=True) for chunk in stream_generator: - # Если в процессе генерации нас перебили (на ранних фразах), прерываем получение if interrupt_event.is_set(): break @@ -657,54 +613,43 @@ def main(): full_response += chunk print(chunk, end="", flush=True) - # Проверяем на конец предложения (. ! ? + пробел или конец строки) - # Эвристика: ищем знаки препинания, после которых идет пробел или перевод строки + # Конец предложения if re.search(r"[.!?\n]+(?:\s|$)", buffer): - # Очищаем и отправляем в очередь clean_chunk = clean_response(buffer, language="ru") if clean_chunk.strip(): tts_q.put((clean_chunk, "ru")) buffer = "" - # Отправляем остаток (если есть) + # Остаток if buffer.strip() and not interrupt_event.is_set(): clean_chunk = clean_response(buffer, language="ru") if clean_chunk.strip(): tts_q.put((clean_chunk, "ru")) except Exception as e: - print(f"\n❌ Ошибка стриминга: {e}") + print(f"\n❌ Ошибка: {e}") speak("Произошла ошибка при получении ответа.") - # Ждем, пока все договорится - # Добавляем poison pill, чтобы поток завершился, когда очередь пуста + # Ждем окончания озвучки tts_q.put(None) worker_thread.join() - print() # Перенос строки после вывода AI + print() - # Добавляем полный ответ AI в историю + # Сохраняем ответ chat_history.append({"role": "assistant", "content": full_response}) - - # Сохраняем для "повтори" last_response = clean_response(full_response, language="ru") - # После озвучки обязательно закрываем поток микрофона stop_wakeword_monitoring() - - # Включаем режим диалога (следующий запрос можно говорить без имени) skip_wakeword = True if interrupt_event.is_set(): - print("⏹️ Ответ прерван - слушаю следующий вопрос") - # Если перебили, цикл перезапустится и skip_wakeword уже True + print("⏹️ Ответ прерван") print() print("-" * 30) print() - # --- Шаг 6: Конец итерации, возврат в начало цикла --- - except KeyboardInterrupt: signal_handler(None, None) except Exception as e: diff --git a/ssp.py b/ssp.py deleted file mode 100644 index 84f35de..0000000 --- a/ssp.py +++ /dev/null @@ -1,10 +0,0 @@ -maxi = 0 -for i in range(84052, 84131): - k = 0 - for j in range(1, i + 1): - if i % j == 0: - k += 1 - if maxi < k: - maxi = k - f = i -print(maxi, f)