Timer
This commit is contained in:
8
.env.example
Normal file
8
.env.example
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
PERPLEXITY_API_KEY=your_perplexity_api_key_here
|
||||||
|
PERPLEXITY_MODEL=llama-3.1-sonar-small-128k-chat
|
||||||
|
DEEPGRAM_API_KEY=your_deepgram_api_key_here
|
||||||
|
PORCUPINE_ACCESS_KEY=your_porcupine_access_key_here
|
||||||
|
TTS_EN_SPEAKER=en_0
|
||||||
|
WEATHER_LAT=63.56
|
||||||
|
WEATHER_LON=53.69
|
||||||
|
WEATHER_CITY=Ухта
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
- Озвучка (Silero TTS, RU/EN).
|
- Озвучка (Silero TTS, RU/EN).
|
||||||
- Перевод RU↔EN (Perplexity).
|
- Перевод RU↔EN (Perplexity).
|
||||||
- Будильник с голосовым отключением.
|
- Будильник с голосовым отключением.
|
||||||
|
- Таймер с голосовым отключением.
|
||||||
- Управление громкостью (ALSA amixer).
|
- Управление громкостью (ALSA amixer).
|
||||||
|
|
||||||
## Требования
|
## Требования
|
||||||
@@ -44,7 +45,9 @@ python main.py
|
|||||||
- Перевод: «переведи с английского» → сказать фразу на английском
|
- Перевод: «переведи с английского» → сказать фразу на английском
|
||||||
- Громкость: «громкость 5»
|
- Громкость: «громкость 5»
|
||||||
- Будильник: «будильник на 7:30», «разбуди на 8 15»
|
- Будильник: «будильник на 7:30», «разбуди на 8 15»
|
||||||
|
- Таймер: «таймер на 6 минут», «поставь будильник через 6 минут»
|
||||||
- Отмена будильников: «отмени будильник»
|
- Отмена будильников: «отмени будильник»
|
||||||
|
- Отмена таймеров: «отмени таймер»
|
||||||
- Стоп/сброс: «стоп», «хватит»
|
- Стоп/сброс: «стоп», «хватит»
|
||||||
|
|
||||||
## Объяснение работы
|
## Объяснение работы
|
||||||
|
|||||||
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`).
|
# Главный файл приложения (`main.py`).
|
||||||
# Здесь находится основной бесконечный цикл, который связывает все компоненты воедино.
|
# Здесь находится основной бесконечный цикл, который связывает все компоненты воедино.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import queue
|
||||||
|
import re
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import queue
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
# Для воспроизведения звуков (mp3)
|
# Для воспроизведения звуков (mp3)
|
||||||
@@ -32,18 +32,25 @@ else:
|
|||||||
_MIXER_IMPORT_ERROR = None
|
_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 (
|
from .audio.wakeword import (
|
||||||
wait_for_wakeword,
|
|
||||||
cleanup as cleanup_wakeword,
|
|
||||||
check_wakeword_once,
|
check_wakeword_once,
|
||||||
|
wait_for_wakeword,
|
||||||
|
)
|
||||||
|
from .audio.wakeword import (
|
||||||
|
cleanup as cleanup_wakeword,
|
||||||
|
)
|
||||||
|
from .audio.wakeword import (
|
||||||
stop_monitoring as stop_wakeword_monitoring,
|
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.ai import ask_ai, ask_ai_stream, translate_text
|
||||||
from .core.cleaner import clean_response
|
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.alarm import get_alarm_clock
|
||||||
|
from .features.timer import get_timer_manager
|
||||||
from .features.weather import get_weather_report
|
from .features.weather import get_weather_report
|
||||||
|
|
||||||
# Список стоп-слов, чтобы прервать диалог или остановить ассистента
|
# Список стоп-слов, чтобы прервать диалог или остановить ассистента
|
||||||
@@ -164,6 +171,7 @@ def main():
|
|||||||
get_recognizer().initialize() # Подключение к Deepgram
|
get_recognizer().initialize() # Подключение к Deepgram
|
||||||
init_tts() # Загрузка нейросети для синтеза речи (Silero)
|
init_tts() # Загрузка нейросети для синтеза речи (Silero)
|
||||||
alarm_clock = get_alarm_clock() # Загрузка будильников
|
alarm_clock = get_alarm_clock() # Загрузка будильников
|
||||||
|
timer_manager = get_timer_manager() # Загрузка таймеров
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# История чата (храним последние 10 обменов репликами для контекста)
|
# История чата (храним последние 10 обменов репликами для контекста)
|
||||||
@@ -182,6 +190,12 @@ def main():
|
|||||||
# Гарантируем, что микрофон детектора wake word освобожден
|
# Гарантируем, что микрофон детектора wake word освобожден
|
||||||
stop_wakeword_monitoring()
|
stop_wakeword_monitoring()
|
||||||
|
|
||||||
|
# --- Проверка таймеров ---
|
||||||
|
# Проверяем каждую итерацию. Если таймер сработал, он заблокирует выполнение, пока его не выключат.
|
||||||
|
if timer_manager.check_timers():
|
||||||
|
skip_wakeword = False
|
||||||
|
continue
|
||||||
|
|
||||||
# --- Проверка будильников ---
|
# --- Проверка будильников ---
|
||||||
# Проверяем каждую итерацию. Если будильник сработал, он заблокирует выполнение, пока его не выключат.
|
# Проверяем каждую итерацию. Если будильник сработал, он заблокирует выполнение, пока его не выключат.
|
||||||
if alarm_clock.check_alarms():
|
if alarm_clock.check_alarms():
|
||||||
@@ -256,6 +270,17 @@ def main():
|
|||||||
skip_wakeword = True
|
skip_wakeword = True
|
||||||
continue
|
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")
|
# Проверка команд будильника ("поставь будильник на 7")
|
||||||
alarm_response = alarm_clock.parse_command(user_text)
|
alarm_response = alarm_clock.parse_command(user_text)
|
||||||
if alarm_response:
|
if alarm_response:
|
||||||
|
|||||||
Reference in New Issue
Block a user