Timer
This commit is contained in:
175
app/features/timer.py
Normal file
175
app/features/timer.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""Timer module."""
|
||||
|
||||
# Модуль таймера.
|
||||
# Отвечает за установку таймеров (в оперативной памяти), их проверку и воспроизведение звука.
|
||||
|
||||
import subprocess
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from ..core.config import BASE_DIR
|
||||
from ..audio.stt import listen
|
||||
|
||||
# Звуковой файл сигнала (используем тот же, что и для будильника)
|
||||
ALARM_SOUND = BASE_DIR / "assets" / "sounds" / "Apex-1.mp3"
|
||||
|
||||
|
||||
class TimerManager:
|
||||
def __init__(self):
|
||||
# Список активных таймеров: {"end_time": datetime, "label": str}
|
||||
self.timers = []
|
||||
|
||||
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"])
|
||||
print(f"⏳ Таймер установлен на {label} (до {end_time.strftime('%H:%M:%S')})")
|
||||
|
||||
def cancel_all_timers(self):
|
||||
"""Отмена всех таймеров."""
|
||||
count = len(self.timers)
|
||||
self.timers = []
|
||||
print(f"🔕 Все таймеры ({count}) отменены.")
|
||||
|
||||
def check_timers(self):
|
||||
"""
|
||||
Проверка: не истек ли какой-то таймер?
|
||||
Вызывается в главном цикле.
|
||||
Возвращает 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)
|
||||
|
||||
print(f"⌛ ТАЙМЕР ИСТЕК: {label}")
|
||||
self.trigger_timer(label)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def trigger_timer(self, label: str):
|
||||
"""
|
||||
Логика срабатывания таймера.
|
||||
Запускает воспроизведение MP3 и слушает команду "Стоп".
|
||||
"""
|
||||
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"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
stop_words = [
|
||||
"стоп",
|
||||
"хватит",
|
||||
"тихо",
|
||||
"замолчи",
|
||||
"отмена",
|
||||
"александр стоп",
|
||||
"спасибо",
|
||||
]
|
||||
|
||||
# Цикл ожидания стоп-команды
|
||||
while True:
|
||||
text = listen(timeout_seconds=3.0, detection_timeout=3.0)
|
||||
if text:
|
||||
text_lower = text.lower()
|
||||
if any(word in text_lower for word in stop_words):
|
||||
print(f"🛑 Таймер остановлен по команде: '{text}'")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка во время таймера: {e}")
|
||||
finally:
|
||||
# Обязательно убиваем процесс плеера
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=1)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
print("🔕 Таймер выключен.")
|
||||
|
||||
def parse_command(self, text: str) -> str | None:
|
||||
"""
|
||||
Парсинг команды установки таймера.
|
||||
Примеры: "таймер на 5 минут", "засеки 10 секунд".
|
||||
"""
|
||||
text = text.lower()
|
||||
|
||||
# Ключевые слова для таймера
|
||||
if not any(word in text for word in ["таймер", "засеки", "поставь таймер"]):
|
||||
return None
|
||||
|
||||
if "отмени" in text or "удали" in text:
|
||||
self.cancel_all_timers()
|
||||
return "Хорошо, все таймеры отменены."
|
||||
|
||||
# Поиск времени
|
||||
# Ищем комбинации: число + (час/мин/сек)
|
||||
# Пример: "1 час 30 минут", "5 минут", "30 секунд"
|
||||
|
||||
total_seconds = 0
|
||||
found_time = False
|
||||
parts = []
|
||||
|
||||
# Часы
|
||||
match_hours = re.search(r"(\d+)\s*(?:час|часа|часов)", text)
|
||||
if match_hours:
|
||||
h = int(match_hours.group(1))
|
||||
total_seconds += h * 3600
|
||||
parts.append(f"{h} ч")
|
||||
found_time = True
|
||||
|
||||
# Минуты
|
||||
match_minutes = re.search(r"(\d+)\s*(?:мин|минуту|минуты|минут)", text)
|
||||
if match_minutes:
|
||||
m = int(match_minutes.group(1))
|
||||
total_seconds += m * 60
|
||||
parts.append(f"{m} мин")
|
||||
found_time = True
|
||||
|
||||
# Секунды
|
||||
match_seconds = re.search(r"(\d+)\s*(?:сек|секунду|секунды|секунд)", text)
|
||||
if match_seconds:
|
||||
s = int(match_seconds.group(1))
|
||||
total_seconds += s
|
||||
parts.append(f"{s} сек")
|
||||
found_time = True
|
||||
|
||||
if found_time and total_seconds > 0:
|
||||
label = " ".join(parts)
|
||||
self.add_timer(total_seconds, label)
|
||||
return f"Засек {label}."
|
||||
|
||||
# Если сказали "таймер", но не нашли время
|
||||
return "Я не понял, на сколько поставить таймер. Скажите, например, 'таймер на 5 минут'."
|
||||
|
||||
|
||||
# Глобальный экземпляр
|
||||
_timer_manager = None
|
||||
|
||||
|
||||
def get_timer_manager():
|
||||
global _timer_manager
|
||||
if _timer_manager is None:
|
||||
_timer_manager = TimerManager()
|
||||
return _timer_manager
|
||||
41
app/main.py
41
app/main.py
@@ -14,12 +14,12 @@ Flow:
|
||||
# Главный файл приложения (`main.py`).
|
||||
# Здесь находится основной бесконечный цикл, который связывает все компоненты воедино.
|
||||
|
||||
import os
|
||||
import queue
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import queue
|
||||
import re
|
||||
import os
|
||||
from collections import deque
|
||||
|
||||
# Для воспроизведения звуков (mp3)
|
||||
@@ -32,18 +32,25 @@ 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
|
||||
from .audio.tts import initialize as init_tts
|
||||
from .audio.tts import speak
|
||||
from .audio.wakeword import (
|
||||
wait_for_wakeword,
|
||||
cleanup as cleanup_wakeword,
|
||||
check_wakeword_once,
|
||||
wait_for_wakeword,
|
||||
)
|
||||
from .audio.wakeword import (
|
||||
cleanup as cleanup_wakeword,
|
||||
)
|
||||
from .audio.wakeword import (
|
||||
stop_monitoring as stop_wakeword_monitoring,
|
||||
)
|
||||
from .audio.stt import listen, cleanup as cleanup_stt, get_recognizer
|
||||
from .core.ai import ask_ai, ask_ai_stream, translate_text
|
||||
from .core.cleaner import clean_response
|
||||
from .audio.tts import speak, initialize as init_tts
|
||||
from .audio.sound_level import set_volume, parse_volume_text
|
||||
from .features.alarm import get_alarm_clock
|
||||
from .features.timer import get_timer_manager
|
||||
from .features.weather import get_weather_report
|
||||
|
||||
# Список стоп-слов, чтобы прервать диалог или остановить ассистента
|
||||
@@ -164,6 +171,7 @@ def main():
|
||||
get_recognizer().initialize() # Подключение к Deepgram
|
||||
init_tts() # Загрузка нейросети для синтеза речи (Silero)
|
||||
alarm_clock = get_alarm_clock() # Загрузка будильников
|
||||
timer_manager = get_timer_manager() # Загрузка таймеров
|
||||
print()
|
||||
|
||||
# История чата (храним последние 10 обменов репликами для контекста)
|
||||
@@ -182,6 +190,12 @@ def main():
|
||||
# Гарантируем, что микрофон детектора wake word освобожден
|
||||
stop_wakeword_monitoring()
|
||||
|
||||
# --- Проверка таймеров ---
|
||||
# Проверяем каждую итерацию. Если таймер сработал, он заблокирует выполнение, пока его не выключат.
|
||||
if timer_manager.check_timers():
|
||||
skip_wakeword = False
|
||||
continue
|
||||
|
||||
# --- Проверка будильников ---
|
||||
# Проверяем каждую итерацию. Если будильник сработал, он заблокирует выполнение, пока его не выключат.
|
||||
if alarm_clock.check_alarms():
|
||||
@@ -256,6 +270,17 @@ def main():
|
||||
skip_wakeword = True
|
||||
continue
|
||||
|
||||
# Проверка команд таймера ("поставь таймер на 6 минут")
|
||||
timer_response = timer_manager.parse_command(user_text)
|
||||
if timer_response:
|
||||
clean_timer_response = clean_response(timer_response, language="ru")
|
||||
completed = speak(
|
||||
clean_timer_response, check_interrupt=check_wakeword_once
|
||||
)
|
||||
last_response = clean_timer_response
|
||||
skip_wakeword = not completed
|
||||
continue
|
||||
|
||||
# Проверка команд будильника ("поставь будильник на 7")
|
||||
alarm_response = alarm_clock.parse_command(user_text)
|
||||
if alarm_response:
|
||||
|
||||
Reference in New Issue
Block a user