chore: sync local changes

This commit is contained in:
2026-03-01 12:55:17 +03:00
parent 27ee32be38
commit f1bc254c6b
8 changed files with 192 additions and 292 deletions

11
.qwen/settings.json Normal file
View File

@@ -0,0 +1,11 @@
{
"$version": 3,
"model": {
"name": "coder-model"
},
"general": {
"checkpointing": {
"enabled": true
}
}
}

13
11.py Normal file
View File

@@ -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))

View File

@@ -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

View File

@@ -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"(?<!\w)_(.+?)_(?!\w)", r"\1", text)
# Удаление зачеркнутого ~~text~~
# Удаление зачеркнутого
text = re.sub(r"~~(.+?)~~", r"\1", text)
# Удаление заголовков Markdown (# Header)
# Заголовки
text = re.sub(r"^#{1,6}\s*", "", text, flags=re.MULTILINE)
# Удаление картинок ![alt](url) -> удаляем полностью
# Картинки
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)

View File

@@ -1,6 +1,4 @@
"""
Short, human-like responses for small talk.
"""
"""Small talk responses."""
from __future__ import annotations

View File

@@ -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 "Я не понял, на сколько поставить таймер."
# Глобальный экземпляр

View File

@@ -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:

10
ssp.py
View File

@@ -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)