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.""" """AI module."""
# Модуль общения с искусственным интеллектом.
# Обрабатывает запросы пользователя и переводы через выбранный API-провайдер.
import json import json
import re import re
@@ -31,8 +28,7 @@ from .config import (
_HTTP = requests.Session() _HTTP = requests.Session()
# Системный промпт (инструкция) для AI. # Системный промпт
# Задает личность ассистента: имя "Александр", стиль общения, краткость.
SYSTEM_PROMPT = """Ты — Александр, умный голосовой ассистент с человеческим поведением. SYSTEM_PROMPT = """Ты — Александр, умный голосовой ассистент с человеческим поведением.
Веди себя как живой человек: будь дружелюбным, естественным и немного эмоциональным, где это уместно. Веди себя как живой человек: будь дружелюбным, естественным и немного эмоциональным, где это уместно.
Твоя главная цель — помогать пользователю и поддерживать интересный диалог. Твоя главная цель — помогать пользователю и поддерживать интересный диалог.
@@ -45,8 +41,7 @@ SYSTEM_PROMPT += (
'"language":"ru","style":["дружелюбный","естественный","краткий"],"format":"plain"}' '"language":"ru","style":["дружелюбный","естественный","краткий"],"format":"plain"}'
) )
# Системный промпт для режима переводчика. # Промпт для перевода
# Требует возвращать ТОЛЬКО перевод, без лишних слов ("Конечно, вот перевод...").
TRANSLATION_SYSTEM_PROMPT = """You are a translation engine. TRANSLATION_SYSTEM_PROMPT = """You are a translation engine.
Translate from {source} to {target}. Translate from {source} to {target}.
Return 2-3 short translation variants only. Return 2-3 short translation variants only.
@@ -69,9 +64,7 @@ _PROVIDER_ALIASES = {
"zai": "zai", "zai": "zai",
} }
# В реальном .env у пользователя должен быть активен только один AI-ключ. # В .env нужен только один AI-ключ
# Поэтому настройки храним в одном словаре, а ниже отдельно проверяем конфликт
# конфигурации, чтобы ассистент не делал "лучшее предположение" молча.
_PROVIDER_SETTINGS = { _PROVIDER_SETTINGS = {
"perplexity": { "perplexity": {
"provider": "perplexity", "provider": "perplexity",
@@ -140,8 +133,8 @@ def _normalize_provider_name(provider_name: str) -> str:
def _get_provider_settings(): def _get_provider_settings():
# Сначала ищем реально активные ключи. Это главный источник истины: """Определяет какой AI провайдер использовать."""
# если ключ один, используем именно его, даже если AI_PROVIDER указан иначе. # Ищем активные ключи
configured = [ configured = [
cfg cfg
for cfg in _PROVIDER_SETTINGS.values() for cfg in _PROVIDER_SETTINGS.values()
@@ -150,7 +143,11 @@ def _get_provider_settings():
if len(configured) == 1: if len(configured) == 1:
cfg = configured[0] cfg = configured[0]
requested = _normalize_provider_name(AI_PROVIDER) 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( print(
f"⚠️ AI_PROVIDER={AI_PROVIDER!r} не совпадает с единственным " f"⚠️ AI_PROVIDER={AI_PROVIDER!r} не совпадает с единственным "
f"активным ключом {cfg['name']}. Используем {cfg['name']}." f"активным ключом {cfg['name']}. Используем {cfg['name']}."
@@ -223,6 +220,7 @@ def _build_headers(cfg):
def _split_system_messages(messages): def _split_system_messages(messages):
"""Извлекает system prompt из списка сообщений."""
system_parts = [] system_parts = []
chat_messages = [] chat_messages = []
@@ -238,7 +236,6 @@ def _split_system_messages(messages):
role = "user" role = "user"
chat_messages.append({"role": role, "content": content}) chat_messages.append({"role": role, "content": content})
# Anthropic хранит системную инструкцию отдельно от обычной истории чата.
return "\n\n".join(system_parts), chat_messages return "\n\n".join(system_parts), chat_messages

View File

@@ -1,24 +1,13 @@
""" """Text cleaner for TTS."""
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.
import re import re
import pymorphy3 import pymorphy3
from num2words import num2words from num2words import num2words
from .roman import roman_to_int from .roman import roman_to_int
# Инициализация морфологического анализатора (для определения падежей)
morph = pymorphy3.MorphAnalyzer() morph = pymorphy3.MorphAnalyzer()
# Карта предлогов и падежей. # Предлоги и падежи
# Помогает понять, в какой падеж ставить число после предлога.
PREPOSITION_CASES = { PREPOSITION_CASES = {
"в": "loct", # В ком/чем? (Предложный) или Винительный. Часто loct для годов. "в": "loct", # В ком/чем? (Предложный) или Винительный. Часто loct для годов.
"во": "loct", "во": "loct",
@@ -55,7 +44,7 @@ PREPOSITION_CASES = {
"про": "accs", "про": "accs",
} }
# Соответствие падежей pymorphy и библиотеки num2words # Соответствие падежей
PYMORPHY_TO_NUM2WORDS = { PYMORPHY_TO_NUM2WORDS = {
"nomn": "nominative", "nomn": "nominative",
"gent": "genitive", "gent": "genitive",
@@ -69,14 +58,14 @@ PYMORPHY_TO_NUM2WORDS = {
"loc2": "prepositional", "loc2": "prepositional",
} }
# Соответствие родов pymorphy и num2words # Роды
PYMORPHY_TO_GENDER = { PYMORPHY_TO_GENDER = {
"masc": "m", "masc": "m",
"femn": "f", "femn": "f",
"neut": "n", "neut": "n",
} }
# Названия месяцев в родительном падеже (для поиска дат в тексте) # Месяца
MONTHS_GENITIVE = [ MONTHS_GENITIVE = [
"января", "января",
"февраля", "февраля",
@@ -92,10 +81,10 @@ MONTHS_GENITIVE = [
"декабря", "декабря",
] ]
# Леммы единиц времени (для корректного падежа числительных) # Время
TIME_UNIT_LEMMAS = {"час", "минута", "секунда"} TIME_UNIT_LEMMAS = {"час", "минута", "секунда"}
# Суффиксы порядковых числительных для формата "1968-й", "1968-го", "1968-му", "1968-м" и т.п. # Суффиксы порядковых
_ORDINAL_SUFFIX_MAP = { _ORDINAL_SUFFIX_MAP = {
# Masculine # Masculine
"ого": ("genitive", "m"), "ого": ("genitive", "m"),
@@ -126,20 +115,15 @@ _ORDINAL_SUFFIX_MAP = {
def get_case_from_preposition(prep_token): def get_case_from_preposition(prep_token):
"""Определяет падеж по предлогу.""" """Падеж по предлогу."""
if not prep_token: if not prep_token:
return None return None
return PREPOSITION_CASES.get(prep_token.lower()) return PREPOSITION_CASES.get(prep_token.lower())
def convert_number(number_str, context_type="cardinal", case="nominative", gender="m"): def convert_number(number_str, context_type="cardinal", case="nominative", gender="m"):
""" """Число в слова."""
Обертка над num2words для конвертации числа в строку.
cardinal - количественное (один, два)
ordinal - порядковое (первый, второй)
"""
try: try:
# Обработка дробей (замена запятой на точку)
if "." in number_str or "," in number_str: if "." in number_str or "," in number_str:
num_val = float(number_str.replace(",", ".")) num_val = float(number_str.replace(",", "."))
else: else:
@@ -152,20 +136,18 @@ def convert_number(number_str, context_type="cardinal", case="nominative", gende
def numbers_to_words(text: str) -> str: def numbers_to_words(text: str) -> str:
""" """Замена цифр на слова."""
Интеллектуальная замена цифр на слова с учетом контекста (даты, года, падежи).
"""
if not text: if not text:
return "" return ""
preps_list = "|".join(map(re.escape, PREPOSITION_CASES.keys())) preps_list = "|".join(map(re.escape, PREPOSITION_CASES.keys()))
# 0. Обработка короткой записи годов с суффиксом: "1968-й", "1968-го", "1968-му", "1968-м", "в 1968-м году" # Года с суффиксом
def replace_year_suffix_match(match): def replace_year_suffix_match(match):
prep = match.group(1) # Предлог (в, во, о...) prep = match.group(1)
year_str = match.group(2) # Само число year_str = match.group(2)
suffix = match.group(3) # Суффикс порядкового числительного suffix = match.group(3)
year_word = match.group(4) # Слово "год", "году" и т.д. (опционально) year_word = match.group(4)
case = None case = None
gender = None gender = None
@@ -191,7 +173,9 @@ def numbers_to_words(text: str) -> str:
if not gender: if not gender:
gender = "m" 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 "" prefix = f"{prep} " if prep else ""
if year_word: if year_word:
@@ -206,25 +190,23 @@ def numbers_to_words(text: str) -> str:
text, text,
) )
# 1. Обработка годов: "в 1999 году", "2024 год" # Года
def replace_year_match(match): def replace_year_match(match):
prep = match.group(1) # Предлог (в, с, к...) prep = match.group(1)
year_str = match.group(2) # Само число year_str = match.group(2)
year_word = match.group(3) # Слово "год", "году" и т.д. year_word = match.group(3)
# Определяем падеж слова "год" через pymorphy # Падеж
parsed = morph.parse(year_word)[0] parsed = morph.parse(year_word)[0]
case_tag = parsed.tag.case case_tag = parsed.tag.case
nw_case = PYMORPHY_TO_NUM2WORDS.get(case_tag, "nominative") nw_case = PYMORPHY_TO_NUM2WORDS.get(case_tag, "nominative")
# FIX: Pymorphy часто определяет "год" как accs (винительный), что для num2words # Без предлога - именительный
# превращается в родительный (для одушевленных?), давая "2024 года".
# Если предлога нет, принудительно ставим именительный.
if not prep and year_word.lower().startswith("год"): if not prep and year_word.lower().startswith("год"):
nw_case = "nominative" nw_case = "nominative"
# Конвертируем число в порядковое числительное (тысяча девятьсот девяносто девятом) # Конвертируем
words = convert_number( words = convert_number(
year_str, context_type="ordinal", case=nw_case, gender="m" year_str, context_type="ordinal", case=nw_case, gender="m"
) )
@@ -239,7 +221,7 @@ def numbers_to_words(text: str) -> str:
text, text,
) )
# 2. Обработка дат: "25 июня", "с 1 мая" # Даты
month_regex = "|".join(MONTHS_GENITIVE) month_regex = "|".join(MONTHS_GENITIVE)
def replace_date_match(match): def replace_date_match(match):
@@ -247,7 +229,7 @@ def numbers_to_words(text: str) -> str:
day_str = match.group(2) day_str = match.group(2)
month_word = match.group(3) month_word = match.group(3)
# По умолчанию родительный падеж ("двадцать пятого июня") # По умолчанию родительный
case = "genitive" case = "genitive"
if prep: if prep:
@@ -266,7 +248,7 @@ def numbers_to_words(text: str) -> str:
if morph_case: if morph_case:
case = PYMORPHY_TO_NUM2WORDS.get(morph_case, "genitive") case = PYMORPHY_TO_NUM2WORDS.get(morph_case, "genitive")
# Используем средний род ('n') для дат (число - средний род: пятое, пятого) # Средний род для дат
words = convert_number(day_str, context_type="ordinal", case=case, gender="n") words = convert_number(day_str, context_type="ordinal", case=case, gender="n")
prefix = f"{prep} " if prep else "" prefix = f"{prep} " if prep else ""
@@ -279,7 +261,7 @@ def numbers_to_words(text: str) -> str:
text, text,
) )
# 3. Обработка всех остальных чисел (Количественные: пять столов, десять минут) # Остальные числа
def replace_cardinal_match(match): def replace_cardinal_match(match):
prep = match.group(1) prep = match.group(1)
num_str = match.group(2) num_str = match.group(2)
@@ -294,7 +276,7 @@ def numbers_to_words(text: str) -> str:
if morph_case: if morph_case:
case = PYMORPHY_TO_NUM2WORDS.get(morph_case, "nominative") case = PYMORPHY_TO_NUM2WORDS.get(morph_case, "nominative")
# Если есть следующее слово, проверяем его род (для "2 минуты" -> "две") # Проверяем род
if next_word: if next_word:
word_clean = next_word.strip() word_clean = next_word.strip()
parsed = morph.parse(word_clean)[0] parsed = morph.parse(word_clean)[0]
@@ -302,21 +284,19 @@ def numbers_to_words(text: str) -> str:
morph_gender = parsed.tag.gender morph_gender = parsed.tag.gender
gender = PYMORPHY_TO_GENDER.get(morph_gender, "m") gender = PYMORPHY_TO_GENDER.get(morph_gender, "m")
# Спец-случай: "на 1 час" -> "на один час" (не "одного") # Спец-случай: "на 1 час"
# Для неодушевленных муж./ср. рода в винительном падеже if (
# числительные должны совпадать с именительным. prep_clean == "на"
if ( and parsed.normal_form in TIME_UNIT_LEMMAS
prep_clean == "на" and parsed.tag.gender in ("masc", "neut")
and parsed.normal_form in TIME_UNIT_LEMMAS ):
and parsed.tag.gender in ("masc", "neut") case = "nominative"
):
case = "nominative"
words = convert_number( words = convert_number(
num_str, context_type="cardinal", case=case, gender=gender num_str, context_type="cardinal", case=case, gender=gender
) )
# Если конвертация вернула пустую строку (сбой?), возвращаем цифры # Если конвертация не удалась - возвращаем цифры
if not words: if not words:
words = num_str words = num_str
@@ -336,11 +316,7 @@ def numbers_to_words(text: str) -> str:
def roman_numerals_to_words(text: str) -> str: def roman_numerals_to_words(text: str) -> str:
""" """Римские в слова."""
Преобразует римские цифры в порядковые числительные с учетом
морфологии предыдущего слова.
Пример: "Ивана III" -> "Ивана третьего".
"""
if not text: if not text:
return "" return ""
@@ -380,63 +356,53 @@ def roman_numerals_to_words(text: str) -> str:
def clean_response(text: str, language: str = "ru") -> str: def clean_response(text: str, language: str = "ru") -> str:
""" """Очистка текста для TTS."""
Основная функция очистки.
Убирает Markdown, ссылки, мусор и преобразует числа.
Args:
text: Сырой текст от AI.
language: Язык (для конвертации чисел, работает только для ru).
"""
if not text: if not text:
return "" return ""
# Удаление ссылок на источники [1], [citation needed] # Удаление ссылок
text = re.sub(r"\x5B\d+\x5D", "", text) text = re.sub(r"\x5B\d+\x5D", "", text)
text = re.sub(r"\x5Bcitation\s*needed\x5D", "", text, flags=re.IGNORECASE) text = re.sub(r"\x5Bcitation\s*needed\x5D", "", text, flags=re.IGNORECASE)
text = re.sub(r"\x5Bsource\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 = 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"\*(.+?)\*", r"\1", text)
text = re.sub(r"(?<!\w)_(.+?)_(?!\w)", r"\1", text) text = re.sub(r"(?<!\w)_(.+?)_(?!\w)", r"\1", text)
# Удаление зачеркнутого ~~text~~ # Удаление зачеркнутого
text = re.sub(r"~~(.+?)~~", r"\1", text) text = re.sub(r"~~(.+?)~~", r"\1", text)
# Удаление заголовков Markdown (# Header) # Заголовки
text = re.sub(r"^#{1,6}\s*", "", text, flags=re.MULTILINE) text = re.sub(r"^#{1,6}\s*", "", text, flags=re.MULTILINE)
# Удаление картинок ![alt](url) -> удаляем полностью # Картинки
text = re.sub(r"!\x5B([^\x5D]*)\x5D\([^)]+\)", "", text) text = re.sub(r"!\x5B([^\x5D]*)\x5D\([^)]+\)", "", text)
# Удаление ссылок [text](url) -> оставляем только text # Ссылки
# \x5B = [, \x5D = ]
text = re.sub(r"\x5B([^\x5D]+)\x5D\([^)]+\)", r"\1", text) text = re.sub(r"\x5B([^\x5D]+)\x5D\([^)]+\)", r"\1", text)
# Удаление inline кода `code` # Код
text = re.sub(r"`([^`]+)`", r"\1", text) text = re.sub(r"`([^`]+)`", r"\1", text)
# Удаление блоков кода ```code```
text = re.sub(r"```[\s\S]*?```", "", text) text = re.sub(r"```[\s\S]*?```", "", text)
# Удаление маркеров списков (-, *, 1.) # Списки
text = re.sub(r"^\s*[-*+]\s+", "", text, flags=re.MULTILINE) 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*\d+\.\s+", "", text, flags=re.MULTILINE)
# Удаление цитат > # Цитаты
text = re.sub(r"^\s*>\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) text = re.sub(r"^[-*_]{3,}\s*$", "", text, flags=re.MULTILINE)
# Удаление HTML тегов # HTML теги
text = re.sub(r"<[^>]+>", "", text) text = re.sub(r"<[^>]+>", "", text)
# Удаление фразы "— это, скорее всего" в корректировках произношения # Корректировки
text = re.sub( text = re.sub(
r"([—-])\s*это,\s*скорее\s*всего\b\s*,?\s*", r"([—-])\s*это,\s*скорее\s*всего\b\s*,?\s*",
r"\1 ", r"\1 ",
@@ -445,7 +411,7 @@ def clean_response(text: str, language: str = "ru") -> str:
) )
text = re.sub(r"[—-]\s*([.!?])", r"\1", text) text = re.sub(r"[—-]\s*([.!?])", r"\1", text)
# Remove informal slang greetings at the beginning of sentences/responses # Удаление сленга
text = re.sub( text = re.sub(
r"^(Эй|Хэй|Слушай|Так|Ну|Короче|В\s+общем)[,!?:]?\s*", r"^(Эй|Хэй|Слушай|Так|Ну|Короче|В\s+общем)[,!?:]?\s*",
"", "",
@@ -453,13 +419,13 @@ def clean_response(text: str, language: str = "ru") -> str:
flags=re.IGNORECASE | re.MULTILINE, flags=re.IGNORECASE | re.MULTILINE,
) )
# Convert Roman numerals and Arabic digits to words for Russian. # Числа в слова
if language == "ru": if language == "ru":
text = roman_numerals_to_words(text) text = roman_numerals_to_words(text)
if re.search(r"\d", text): if re.search(r"\d", text):
text = numbers_to_words(text) text = numbers_to_words(text)
# Remove extra whitespace # Чистка пробелов
text = re.sub(r"\n{3,}", "\n\n", text) text = re.sub(r"\n{3,}", "\n\n", text)
text = re.sub(r" +", " ", text) text = re.sub(r" +", " ", text)

View File

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

View File

@@ -1,8 +1,5 @@
"""Timer module.""" """Timer module."""
# Модуль таймера.
# Отвечает за установку таймеров (в оперативной памяти), их проверку и воспроизведение звука.
import subprocess import subprocess
import re import re
import json import json
@@ -25,7 +22,7 @@ ALARM_SOUND = BASE_DIR / "assets" / "sounds" / "Apex-1.mp3"
TIMER_FILE = BASE_DIR / "data" / "timers.json" TIMER_FILE = BASE_DIR / "data" / "timers.json"
ASK_TIMER_TIME_PROMPT = "На какое время мне поставить таймер?" ASK_TIMER_TIME_PROMPT = "На какое время мне поставить таймер?"
# --- Number words parsing helpers (ru) --- # Числа словами
_NUMBER_UNITS = { _NUMBER_UNITS = {
"ноль": 0, "ноль": 0,
"один": 1, "один": 1,
@@ -103,13 +100,19 @@ _UNIT_LEMMAS = {
"мин": "minutes", "мин": "minutes",
"сек": "seconds", "сек": "seconds",
} }
_UNIT_LEMMAS = {
"час": "hours",
"минута": "minutes",
"секунда": "seconds",
"мин": "minutes",
"сек": "seconds",
}
_UNIT_FORMS = { _UNIT_FORMS = {
"hours": ("час", "часа", "часов"), "hours": ("час", "часа", "часов"),
"minutes": ("минуту", "минуты", "минут"), "minutes": ("минуту", "минуты", "минут"),
"seconds": ("секунду", "секунды", "секунд"), "seconds": ("секунду", "секунды", "секунд"),
} }
# Optional ordinal formatting for list numbering.
try: try:
from num2words import num2words from num2words import num2words
except Exception: except Exception:
@@ -251,12 +254,11 @@ def _format_ordinal_index(index: int) -> str:
class TimerManager: class TimerManager:
def __init__(self): def __init__(self):
# Список активных таймеров: {"end_time": datetime, "label": str}
self.timers = [] self.timers = []
self.load_timers() self.load_timers()
def load_timers(self): def load_timers(self):
"""Загрузка списка таймеров из JSON файла.""" """Загрузка из файла."""
if TIMER_FILE.exists(): if TIMER_FILE.exists():
try: try:
with open(TIMER_FILE, "r", encoding="utf-8") as f: 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"]) self.timers = sorted(timers, key=lambda x: x["end_time"])
def save_timers(self): def save_timers(self):
"""Сохранение списка таймеров в JSON файл.""" """Сохранение в файл."""
payload = [ payload = [
{"end_time": t["end_time"].isoformat(), "label": t.get("label", "")} {"end_time": t["end_time"].isoformat(), "label": t.get("label", "")}
for t in self.timers for t in self.timers
@@ -289,7 +291,7 @@ class TimerManager:
print(f"❌ Ошибка сохранения таймеров: {e}") print(f"❌ Ошибка сохранения таймеров: {e}")
def describe_timers(self) -> str: def describe_timers(self) -> str:
"""Возвращает текстовое описание активных таймеров.""" """Описание активных таймеров."""
if not self.timers: if not self.timers:
return "Активных таймеров нет." return "Активных таймеров нет."
@@ -312,38 +314,29 @@ class TimerManager:
return "Активные таймеры: " + "; ".join(items) + "." return "Активные таймеры: " + "; ".join(items) + "."
def add_timer(self, seconds: int, label: str): def add_timer(self, seconds: int, label: str):
"""Добавление нового таймера.""" """Добавить таймер."""
end_time = datetime.now() + timedelta(seconds=seconds) end_time = datetime.now() + timedelta(seconds=seconds)
self.timers.append({"end_time": end_time, "label": label}) self.timers.append({"end_time": end_time, "label": label})
# Сортируем, чтобы ближайший был первым
self.timers.sort(key=lambda x: x["end_time"]) self.timers.sort(key=lambda x: x["end_time"])
self.save_timers() 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): def cancel_all_timers(self):
"""Отмена всех таймеров.""" """Отменить все таймеры."""
count = len(self.timers) count = len(self.timers)
self.timers = [] self.timers = []
self.save_timers() self.save_timers()
print(f"🔕 Все таймеры ({count}) отменены.") print(f"🔕 Таймеры отменены: {count}")
def check_timers(self): def check_timers(self):
""" """Проверка таймеров. Возвращает True если сработал."""
Проверка: не истек ли какой-то таймер?
Вызывается в главном цикле.
Возвращает True, если таймер сработал (и был обработан).
"""
if not self.timers: if not self.timers:
return False return False
now = datetime.now() now = datetime.now()
# Смотрим первый (самый ранний) таймер
# Используем индекс 0, так как список отсортирован
first_timer = self.timers[0] first_timer = self.timers[0]
if now >= first_timer["end_time"]: if now >= first_timer["end_time"]:
# Таймер сработал!
# Удаляем его из списка
label = first_timer["label"] label = first_timer["label"]
self.timers.pop(0) self.timers.pop(0)
self.save_timers() self.save_timers()
@@ -355,36 +348,30 @@ class TimerManager:
return False return False
def trigger_timer(self, label: str): def trigger_timer(self, label: str):
""" """Срабатывание таймера."""
Логика срабатывания таймера. print(f"🔔 ТАЙМЕР {label}!")
Запускает воспроизведение MP3 и слушает команду "Стоп".
"""
print(f"🔔 ТАЙМЕР НА {label} СРАБОТАЛ! (Скажите 'Стоп')")
# Запуск плеера mpg123 в бесконечном цикле
cmd = ["mpg123", "-q", "--loop", "-1", str(ALARM_SOUND)] cmd = ["mpg123", "-q", "--loop", "-1", str(ALARM_SOUND)]
try: try:
process = subprocess.Popen(cmd) process = subprocess.Popen(cmd)
except FileNotFoundError: except FileNotFoundError:
print( print("❌ mpg123 не найден. Установите: sudo apt install mpg123")
"❌ Ошибка: mpg123 не найден. Установите его: sudo apt install mpg123"
)
return return
try: try:
# Цикл ожидания стоп-команды
while True: 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 text:
if is_stop_command(text, mode="lenient"): if is_stop_command(text, mode="lenient"):
print(f"🛑 Таймер остановлен по команде: '{text}'") print(f"🛑 Остановлен: '{text}'")
break break
except Exception as e: except Exception as e:
print(f"❌ Ошибка во время таймера: {e}") print(f"❌ Ошибка: {e}")
finally: finally:
# Обязательно убиваем процесс плеера
process.terminate() process.terminate()
try: try:
process.wait(timeout=1) process.wait(timeout=1)
@@ -393,10 +380,7 @@ class TimerManager:
print("🔕 Таймер выключен.") print("🔕 Таймер выключен.")
def parse_command(self, text: str) -> str | None: def parse_command(self, text: str) -> str | None:
""" """Парсинг команды таймера."""
Парсинг команды установки таймера.
Примеры: "таймер на 5 минут", "засеки 10 секунд".
"""
text = _normalize_timer_text(text.lower()) text = _normalize_timer_text(text.lower())
# Ключевые слова для таймера # Ключевые слова для таймера
@@ -413,9 +397,6 @@ class TimerManager:
return "Хорошо, все таймеры отменены." return "Хорошо, все таймеры отменены."
# Поиск времени # Поиск времени
# Ищем комбинации: число + (час/мин/сек)
# Пример: "1 час 30 минут", "5 минут", "30 секунд"
total_seconds = 0 total_seconds = 0
parts = [] parts = []
hours = None hours = None
@@ -438,7 +419,7 @@ class TimerManager:
if match_seconds: if match_seconds:
seconds = int(match_seconds.group(1)) seconds = int(match_seconds.group(1))
# Дополняем числительные словами (например, "одну минуту") # Числа словами
word_values = _extract_word_time_values(text) word_values = _extract_word_time_values(text)
if hours is None and word_values["hours"] is not None: if hours is None and word_values["hours"] is not None:
hours = word_values["hours"] hours = word_values["hours"]
@@ -456,9 +437,7 @@ class TimerManager:
found_time = any(value is not None for value in [hours, minutes, seconds]) found_time = any(value is not None for value in [hours, minutes, seconds])
if found_time: if found_time:
total_seconds = ( total_seconds = (hours or 0) * 3600 + (minutes or 0) * 60 + (seconds or 0)
(hours or 0) * 3600 + (minutes or 0) * 60 + (seconds or 0)
)
if has_fractional: if has_fractional:
total_seconds = int(round(total_seconds)) total_seconds = int(round(total_seconds))
h = total_seconds // 3600 h = total_seconds // 3600
@@ -481,15 +460,16 @@ class TimerManager:
self.add_timer(total_seconds, label) self.add_timer(total_seconds, label)
return f"Поставил таймер на {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 ASK_TIMER_TIME_PROMPT
# Если сказали "таймер", но не нашли время. return "Я не понял, на сколько поставить таймер."
return "Я не понял, на сколько поставить таймер. Скажите, например, 'таймер на 5 минут'."
# Глобальный экземпляр # Глобальный экземпляр

View File

@@ -1,18 +1,8 @@
""" """
Smart Speaker - Main Application 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 os
import queue import queue
@@ -32,7 +22,7 @@ except Exception as exc:
else: else:
_MIXER_IMPORT_ERROR = None _MIXER_IMPORT_ERROR = None
# Импорт наших модулей # Наши модули
from .audio.sound_level import parse_volume_text, set_volume from .audio.sound_level import parse_volume_text, set_volume
from .audio.stt import cleanup as cleanup_stt from .audio.stt import cleanup as cleanup_stt
from .audio.stt import get_recognizer, listen from .audio.stt import get_recognizer, listen
@@ -174,11 +164,10 @@ _CITY_PATTERNS = [
), ),
] ]
def signal_handler(sig, frame): def signal_handler(sig, frame):
""" """Обработчик Ctrl+C."""
Обработчик сигнала Ctrl+C. print("\n\n👋 Завершение работы...")
Позволяет корректно завершить работу программы, освободив ресурсы (микрофон, модели).
"""
print("\n\n👋 Завершение работы...") print("\n\n👋 Завершение работы...")
try: try:
cleanup_wakeword() # Остановка Porcupine cleanup_wakeword() # Остановка Porcupine
@@ -192,13 +181,8 @@ def signal_handler(sig, frame):
def parse_translation_request(text: str): def parse_translation_request(text: str):
""" """Проверяет, является ли фраза запросом на перевод."""
Определяет, является ли фраза запросом на перевод. text_lower = text.lower().strip()
Пример: "Переведи на английский привет мир"
Возвращает словарь: {'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(): def main():
""" """Точка входа."""
Основная функция (точка входа). print("=" * 50)
"""
print("=" * 50) print("=" * 50)
print("🔊 УМНАЯ КОЛОНКА") print("🔊 УМНАЯ КОЛОНКА")
print("=" * 50) print("=" * 50)
@@ -231,7 +214,6 @@ def main():
# Устанавливаем перехватчик Ctrl+C # Устанавливаем перехватчик Ctrl+C
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
# Предварительная инициализация моделей
print("⏳ Инициализация моделей...") print("⏳ Инициализация моделей...")
# Инициализация звуковой системы для эффектов (опционально) # Инициализация звуковой системы для эффектов (опционально)
@@ -262,70 +244,61 @@ def main():
cities_game = get_cities_game() # Игра "Города" cities_game = get_cities_game() # Игра "Города"
print() print()
# История чата (храним последние 10 обменов репликами для контекста) # История чата
chat_history = deque(maxlen=20) chat_history = deque(maxlen=20)
# Переменная для хранения последнего ответа ассистента # Последний ответ ассистента
last_response = None last_response = None
# Переменная, указывающая, нужно ли пропускать ожидание wake word # Режим диалога (без wake word)
# (True = режим диалога, слушаем сразу. False = ждем "Alexandr")
skip_wakeword = False skip_wakeword = False
# После ответа ассистент ждет продолжение фразы 4 секунды.
# Если речи нет, выходим из диалога и снова ждем wake word.
followup_idle_timeout_seconds = 4.0 followup_idle_timeout_seconds = 4.0
# Контекст уточнения "на какое время поставить ...". # Контекст уточнения времени для таймера/будильника
# Может быть: "timer", "alarm".
pending_time_target = None pending_time_target = None
# Переменная для отслеживания последней проверки здоровья STT # Проверка здоровья STT
last_stt_check = time.time() last_stt_check = time.time()
# БЕСКОНЕЧНЫЙ ЦИКЛ РАБОТЫ # ГЛАВНЫЙ ЦИКЛ
while True: while True:
# Периодическая проверка здоровья STT каждые 10 минут # Периодическая проверка STT
if time.time() - last_stt_check > 600: # 10 минут = 600 секунд if time.time() - last_stt_check > 600:
try: try:
recognizer = get_recognizer() recognizer = get_recognizer()
if hasattr(recognizer, 'check_connection_health'): if hasattr(recognizer, "check_connection_health"):
recognizer.check_connection_health() recognizer.check_connection_health()
last_stt_check = time.time() last_stt_check = time.time()
except Exception as e: except Exception as e:
print(f"Ошибка при проверке здоровья STT: {e}") print(f"Ошибка при проверке STT: {e}")
try: try:
# Гарантируем, что микрофон детектора wake word освобожден # Освобождаем микрофон wake word
stop_wakeword_monitoring() stop_wakeword_monitoring()
# --- Проверка таймеров --- # Проверяем таймеры
# Проверяем каждую итерацию. Если таймер сработал, он заблокирует выполнение, пока его не выключат.
if timer_manager.check_timers(): if timer_manager.check_timers():
skip_wakeword = False skip_wakeword = False
continue continue
# --- Проверка будильников --- # Проверяем будильники
# Проверяем каждую итерацию. Если будильник сработал, он заблокирует выполнение, пока его не выключат.
if alarm_clock.check_alarms(): if alarm_clock.check_alarms():
# Если будильник прозвенел и был выключен пользователем, сбрасываем режим диалога
skip_wakeword = False skip_wakeword = False
continue continue
# --- Шаг 1: Активация --- # Ждем wake word
if not skip_wakeword: if not skip_wakeword:
# Ожидание фразы "Alexandr". Используем таймаут 0.5 сек, чтобы чаще проверять будильники.
detected = wait_for_wakeword(timeout=0.5) detected = wait_for_wakeword(timeout=0.5)
# Если время вышло, а фразы не было — начинаем цикл заново (проверяем будильники) # Если время вышлопроверяем будильники
if not detected: if not detected:
continue continue
# Воспроизводим звук активации # Звук активации
if ding_sound: if ding_sound:
ding_sound.play() ding_sound.play()
# Фраза активации услышана: # Слушаем команду
# до 5с ждём начало речи, после начала завершаем STT по 3с тишины.
try: try:
user_text = listen(timeout_seconds=5.0, fast_stop=True) user_text = listen(timeout_seconds=5.0, fast_stop=True)
except Exception as e: except Exception as e:
@@ -338,12 +311,8 @@ def main():
print(f"Ошибка переинициализации STT: {init_error}") print(f"Ошибка переинициализации STT: {init_error}")
continue # Продолжаем цикл continue # Продолжаем цикл
else: else:
# Режим диалога (Follow-up): ждем продолжения речи без "Alexandr" # Follow-up режим — без wake word
print( print(f"👂 Слушаю ({followup_idle_timeout_seconds:.0f} сек)...")
"👂 Слушаю продолжение диалога "
f"({followup_idle_timeout_seconds:.0f} сек)..."
)
# Ждем начала речи 4 сек. Если начали говорить, слушаем до 7 сек.
try: try:
user_text = listen( user_text = listen(
timeout_seconds=7.0, timeout_seconds=7.0,
@@ -362,13 +331,12 @@ def main():
continue continue
if not user_text: if not user_text:
# Пользователь промолчал — выходим из режима диалога, засыпаем. # Молчание — возвращаемся к ожиданию
skip_wakeword = False skip_wakeword = False
continue continue
# --- Шаг 2: Анализ распознанного текста --- # Анализ текста
if not user_text: if not user_text:
# Пустой ввод: без лишних ответов возвращаемся к ожиданию wake word.
skip_wakeword = False skip_wakeword = False
continue continue
@@ -384,13 +352,12 @@ def main():
skip_wakeword = False skip_wakeword = False
continue continue
print("_" * 50) print("_" * 50)
print("💤 Жду 'Alexandr' для активации...") print("💤 Жду 'Alexandr'...")
skip_wakeword = False skip_wakeword = False
continue continue
# Проверка на команду "Повтори" / "Еще раз" # Проверка на "Повтори"
user_text_lower = user_text.lower().strip() user_text_lower = user_text.lower().strip()
# Проверяем точное совпадение или если фраза начинается с "повтори" (но не "повтори за мной")
if user_text_lower in _REPEAT_PHRASES or ( if user_text_lower in _REPEAT_PHRASES or (
user_text_lower.startswith("повтори") user_text_lower.startswith("повтори")
and "за мной" not in user_text_lower and "за мной" not in user_text_lower
@@ -400,11 +367,10 @@ def main():
speak(last_response) speak(last_response)
else: else:
speak("Я еще ничего не говорил.") speak("Я еще ничего не говорил.")
# После повтора остаемся в диалоге
skip_wakeword = True skip_wakeword = True
continue continue
# Короткие ответы на small-talk ("как дела" и т.п.) # Small-talk
smalltalk_response = get_smalltalk_response(user_text) smalltalk_response = get_smalltalk_response(user_text)
if smalltalk_response: if smalltalk_response:
clean_smalltalk = clean_response(smalltalk_response, language="ru") clean_smalltalk = clean_response(smalltalk_response, language="ru")
@@ -424,7 +390,7 @@ def main():
): ):
command_text = f"будильник {command_text}" command_text = f"будильник {command_text}"
# Проверка команд таймера ("поставь таймер на 6 минут") # Таймеры
stopwatch_response = stopwatch_manager.parse_command(command_text) stopwatch_response = stopwatch_manager.parse_command(command_text)
if stopwatch_response: if stopwatch_response:
clean_stopwatch_response = clean_response( clean_stopwatch_response = clean_response(
@@ -435,7 +401,7 @@ def main():
skip_wakeword = True skip_wakeword = True
continue continue
# Проверка команд таймера ("поставь таймер на 6 минут") # Таймер
timer_response = timer_manager.parse_command(command_text) timer_response = timer_manager.parse_command(command_text)
if timer_response: if timer_response:
clean_timer_response = clean_response(timer_response, language="ru") clean_timer_response = clean_response(timer_response, language="ru")
@@ -449,7 +415,7 @@ def main():
skip_wakeword = not completed skip_wakeword = not completed
continue continue
# Проверка команд будильника ("поставь будильник на 7") # Будильник
alarm_response = alarm_clock.parse_command(command_text) alarm_response = alarm_clock.parse_command(command_text)
if alarm_response: if alarm_response:
clean_alarm_response = clean_response(alarm_response, language="ru") clean_alarm_response = clean_response(alarm_response, language="ru")
@@ -461,10 +427,9 @@ def main():
skip_wakeword = alarm_response == ASK_ALARM_TIME_PROMPT skip_wakeword = alarm_response == ASK_ALARM_TIME_PROMPT
continue continue
# Проверка команды громкости ("громкость 5") # Громкость
if user_text.lower().startswith("громкость"): if user_text.lower().startswith("громкость"):
try: try:
# Убираем слово "громкость" и ищем число
vol_str = user_text.lower().replace("громкость", "", 1).strip() vol_str = user_text.lower().replace("громкость", "", 1).strip()
level = parse_volume_text(vol_str) level = parse_volume_text(vol_str)
@@ -489,32 +454,31 @@ def main():
skip_wakeword = True skip_wakeword = True
continue continue
# Проверка команды "Погода" # Погода
# Проверяем, содержит ли запрос информацию о конкретном городе
requested_city = None requested_city = None
user_text_lower = user_text.lower() user_text_lower = user_text.lower()
# Проверяем наличие упоминания города в запросе (например, "погода в Нью-Йорке", "какая погода в Москве")
for pattern in _CITY_PATTERNS: for pattern in _CITY_PATTERNS:
match = pattern.search(user_text_lower) match = pattern.search(user_text_lower)
if match: if match:
potential_city = match.group(1).strip() potential_city = match.group(1).strip()
# Проверяем, что это не местоимение или другое слово, а реально название города
if ( if (
potential_city potential_city
and len(potential_city) > 1 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 break
# Проверяем, содержит ли запрос одну из погодных команд
has_weather_trigger = any( has_weather_trigger = any(
trigger in user_text_lower for trigger in _WEATHER_TRIGGERS trigger in user_text_lower for trigger in _WEATHER_TRIGGERS
) )
if has_weather_trigger: if has_weather_trigger:
from .features.weather import get_weather_report from .features.weather import get_weather_report
weather_report = get_weather_report(requested_city) weather_report = get_weather_report(requested_city)
clean_report = clean_response(weather_report, language="ru") clean_report = clean_response(weather_report, language="ru")
speak(clean_report) speak(clean_report)
@@ -522,7 +486,7 @@ def main():
skip_wakeword = True skip_wakeword = True
continue continue
# Проверка музыкальных команд ("включи музыку", "пауза", и т.д.) # Музыка
music_controller = get_music_controller() music_controller = get_music_controller()
music_response = music_controller.parse_command(user_text) music_response = music_controller.parse_command(user_text)
if music_response: if music_response:
@@ -532,14 +496,14 @@ def main():
skip_wakeword = True skip_wakeword = True
continue continue
# Проверка запроса на перевод # Перевод
translation_request = parse_translation_request(user_text) translation_request = parse_translation_request(user_text)
if translation_request: if translation_request:
source_lang = translation_request["source_lang"] source_lang = translation_request["source_lang"]
target_lang = translation_request["target_lang"] target_lang = translation_request["target_lang"]
text_to_translate = translation_request["text"] text_to_translate = translation_request["text"]
# Если сказано только "переведи на английский", спрашиваем "что перевести?" # Если сказано только "переведи" — спрашиваем
if not text_to_translate: if not text_to_translate:
prompt = ( prompt = (
"Скажи фразу на английском." "Скажи фразу на английском."
@@ -547,7 +511,6 @@ def main():
else "Скажи фразу на русском." else "Скажи фразу на русском."
) )
speak(prompt) speak(prompt)
# Слушаем саму фразу на нужном языке
try: try:
text_to_translate = listen( text_to_translate = listen(
timeout_seconds=7.0, detection_timeout=5.0, lang=source_lang timeout_seconds=7.0, detection_timeout=5.0, lang=source_lang
@@ -569,31 +532,30 @@ def main():
skip_wakeword = False skip_wakeword = False
continue continue
# Выполняем перевод через AI # Перевод через AI
translated_text = translate_text( translated_text = translate_text(
text_to_translate, source_lang, target_lang text_to_translate, source_lang, target_lang
) )
# Очищаем результат (убираем лишние символы)
clean_text = clean_response(translated_text, language=target_lang) clean_text = clean_response(translated_text, language=target_lang)
# Сохраняем для повтора
last_response = clean_text last_response = clean_text
# Озвучиваем перевод на целевом языке # Озвучиваем
completed = speak( completed = speak(
clean_text, clean_text,
check_interrupt=check_wakeword_once, check_interrupt=check_wakeword_once,
language=target_lang, language=target_lang,
) )
stop_wakeword_monitoring() stop_wakeword_monitoring()
skip_wakeword = True # Остаемся в диалоге skip_wakeword = True
if not completed: if not completed:
print("⏹️ Перевод прерван - слушаю следующий вопрос") print("⏹️ Перевод прерван")
continue continue
# Игра "Города" # Игра "Города"
cities_response = cities_game.handle(user_text) cities_response = cities_game.handle(user_text)
cities_response = cities_game.handle(user_text)
if cities_response: if cities_response:
clean_cities_response = clean_response(cities_response, language="ru") clean_cities_response = clean_response(cities_response, language="ru")
speak(clean_cities_response) speak(clean_cities_response)
@@ -601,41 +563,36 @@ def main():
skip_wakeword = True skip_wakeword = True
continue continue
# --- Шаг 3: Запрос к AI (Streaming) --- # AI запрос
# Добавляем сообщение пользователя в историю
chat_history.append({"role": "user", "content": user_text}) chat_history.append({"role": "user", "content": user_text})
# Очередь для предложений, которые нужно озвучить # Очередь для TTS
tts_q = queue.Queue() tts_q = queue.Queue()
# Флаг прерывания для worker-а
interrupt_event = threading.Event() interrupt_event = threading.Event()
def tts_worker(): def tts_worker():
"""Фоновый поток, читающий предложения из очереди и озвучивающий их.""" """Фоновый поток для озвучки."""
while True: while True:
item = tts_q.get() item = tts_q.get()
if item is None: # Poison pill (сигнал остановки) if item is None:
tts_q.task_done() tts_q.task_done()
break break
text, lang = item text, lang = item
# Если уже было прерывание, просто пропускаем (чистим очередь)
if interrupt_event.is_set(): if interrupt_event.is_set():
tts_q.task_done() tts_q.task_done()
continue continue
# Озвучиваем
completed = speak( completed = speak(
text, check_interrupt=check_wakeword_once, language=lang text, check_interrupt=check_wakeword_once, language=lang
) )
if not completed: if not completed:
interrupt_event.set() # Сообщаем всем, что нас перебили interrupt_event.set()
tts_q.task_done() tts_q.task_done()
# Запускаем поток озвучки
worker_thread = threading.Thread(target=tts_worker, daemon=True) worker_thread = threading.Thread(target=tts_worker, daemon=True)
worker_thread.start() worker_thread.start()
@@ -643,13 +600,12 @@ def main():
buffer = "" buffer = ""
try: try:
# Получаем генератор потока от AI # Streaming от AI
stream_generator = ask_ai_stream(list(chat_history)) stream_generator = ask_ai_stream(list(chat_history))
print("🤖 AI говорит: ", end="", flush=True) print("🤖 AI: ", end="", flush=True)
for chunk in stream_generator: for chunk in stream_generator:
# Если в процессе генерации нас перебили (на ранних фразах), прерываем получение
if interrupt_event.is_set(): if interrupt_event.is_set():
break break
@@ -657,54 +613,43 @@ def main():
full_response += chunk full_response += chunk
print(chunk, end="", flush=True) print(chunk, end="", flush=True)
# Проверяем на конец предложения (. ! ? + пробел или конец строки) # Конец предложения
# Эвристика: ищем знаки препинания, после которых идет пробел или перевод строки
if re.search(r"[.!?\n]+(?:\s|$)", buffer): if re.search(r"[.!?\n]+(?:\s|$)", buffer):
# Очищаем и отправляем в очередь
clean_chunk = clean_response(buffer, language="ru") clean_chunk = clean_response(buffer, language="ru")
if clean_chunk.strip(): if clean_chunk.strip():
tts_q.put((clean_chunk, "ru")) tts_q.put((clean_chunk, "ru"))
buffer = "" buffer = ""
# Отправляем остаток (если есть) # Остаток
if buffer.strip() and not interrupt_event.is_set(): if buffer.strip() and not interrupt_event.is_set():
clean_chunk = clean_response(buffer, language="ru") clean_chunk = clean_response(buffer, language="ru")
if clean_chunk.strip(): if clean_chunk.strip():
tts_q.put((clean_chunk, "ru")) tts_q.put((clean_chunk, "ru"))
except Exception as e: except Exception as e:
print(f"\n❌ Ошибка стриминга: {e}") print(f"\n❌ Ошибка: {e}")
speak("Произошла ошибка при получении ответа.") speak("Произошла ошибка при получении ответа.")
# Ждем, пока все договорится # Ждем окончания озвучки
# Добавляем poison pill, чтобы поток завершился, когда очередь пуста
tts_q.put(None) tts_q.put(None)
worker_thread.join() worker_thread.join()
print() # Перенос строки после вывода AI print()
# Добавляем полный ответ AI в историю # Сохраняем ответ
chat_history.append({"role": "assistant", "content": full_response}) chat_history.append({"role": "assistant", "content": full_response})
# Сохраняем для "повтори"
last_response = clean_response(full_response, language="ru") last_response = clean_response(full_response, language="ru")
# После озвучки обязательно закрываем поток микрофона
stop_wakeword_monitoring() stop_wakeword_monitoring()
# Включаем режим диалога (следующий запрос можно говорить без имени)
skip_wakeword = True skip_wakeword = True
if interrupt_event.is_set(): if interrupt_event.is_set():
print("⏹️ Ответ прерван - слушаю следующий вопрос") print("⏹️ Ответ прерван")
# Если перебили, цикл перезапустится и skip_wakeword уже True
print() print()
print("-" * 30) print("-" * 30)
print() print()
# --- Шаг 6: Конец итерации, возврат в начало цикла ---
except KeyboardInterrupt: except KeyboardInterrupt:
signal_handler(None, None) signal_handler(None, None)
except Exception as e: 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)