diff --git a/README.md b/README.md index 51fd6cf..bbc37c0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ - Распознавание речи (Deepgram, RU/EN). - Озвучка (Silero TTS, RU/EN). - Перевод RU↔EN (Perplexity). -- Будильник с локальным распознаванием стоп-команд (Vosk). +- Будильник с голосовым отключением. - Управление громкостью (ALSA amixer). ## Требования @@ -85,7 +85,6 @@ Wake word (Porcupine) ──► STT (Deepgram) ──► Логика коман - `ai.py` — запросы к Perplexity (чат и перевод). - `cleaner.py` — очистка ответа и преобразование чисел (RU). - `alarm.py` — будильник и логика расписания. -- `local_stt.py` — локальный Vosk для стоп-команд. - `sound_level.py` — управление громкостью. ## Частые проблемы diff --git a/app/audio/local_stt.py b/app/audio/local_stt.py deleted file mode 100644 index 516fd53..0000000 --- a/app/audio/local_stt.py +++ /dev/null @@ -1,147 +0,0 @@ -""" -Local offline Speech-to-Text module using Vosk. -Used for simple command detection (like "stop") without internet. -""" - -# Модуль локального распознавания речи (Vosk). -# Работает полностью оффлайн (без интернета). -# Используется, когда нужно распознать простые команды (например, "стоп" во время будильника), -# чтобы не тратить трафик и время на обращение к облаку. - -import os -import sys -import json -import pyaudio -from vosk import Model, KaldiRecognizer -from config import VOSK_MODEL_PATH, SAMPLE_RATE - - -class LocalRecognizer: - """Класс для работы с Vosk.""" - - def __init__(self): - self.model = None - self.rec = None - self.pa = None - self.stream = None - - def initialize(self): - """Загрузка модели Vosk.""" - if not os.path.exists(VOSK_MODEL_PATH): - print(f"❌ Ошибка: Vosk модель не найдена по пути {VOSK_MODEL_PATH}") - return False - - print("📦 Инициализация локального STT (Vosk)...") - - # Трюк для подавления вывода логов Vosk в консоль (он очень шумный) - try: - null_fd = os.open(os.devnull, os.O_WRONLY) - old_stderr = os.dup(2) - sys.stderr.flush() - os.dup2(null_fd, 2) - os.close(null_fd) - - # Сама загрузка модели - self.model = Model(str(VOSK_MODEL_PATH)) - - # Возвращаем stderr обратно - os.dup2(old_stderr, 2) - os.close(old_stderr) - except Exception as e: - print(f"Error initializing Vosk: {e}") - return False - - self.rec = KaldiRecognizer(self.model, SAMPLE_RATE) - self.pa = pyaudio.PyAudio() - return True - - def listen_for_keywords(self, keywords: list, timeout: float = 10.0) -> str: - """ - Слушает микрофон заданное время и проверяет наличие ключевых слов. - - Args: - keywords: Список слов, которые мы ждем (например, ["стоп", "хватит"]). - timeout: Сколько секунд слушать. - - Returns: - Найденное слово или пустую строку. - """ - if not self.model: - if not self.initialize(): - return "" - - # Открываем поток микрофона - try: - stream = self.pa.open( - format=pyaudio.paInt16, - channels=1, - rate=SAMPLE_RATE, - input=True, - frames_per_buffer=4096, - ) - stream.start_stream() - except Exception as e: - print(f"❌ Ошибка микрофона: {e}") - return "" - - import time - - start_time = time.time() - - print(f"👂 Локальное слушание ожидает: {keywords}") - - detected_text = "" - - try: - while time.time() - start_time < timeout: - data = stream.read(4096, exception_on_overflow=False) - - # Vosk обрабатывает аудио чанками - if self.rec.AcceptWaveform(data): - # Полный результат - res = json.loads(self.rec.Result()) - text = res.get("text", "") - if text: - print(f"📝 Локально: {text}") - # Проверяем, есть ли ключевое слово в распознанном тексте - for kw in keywords: - if kw in text: - detected_text = text - break - else: - # Частичный результат (быстрее, чем полный) - res = json.loads(self.rec.PartialResult()) - partial = res.get("partial", "") - if partial: - for kw in keywords: - if kw in partial: - detected_text = partial - break - - if detected_text: - break - finally: - stream.stop_stream() - stream.close() - - return detected_text - - def cleanup(self): - if self.pa: - self.pa.terminate() - - -# Глобальный экземпляр -_local_recognizer = None - - -def get_local_recognizer(): - global _local_recognizer - if _local_recognizer is None: - _local_recognizer = LocalRecognizer() - return _local_recognizer - - -def listen_for_keywords(keywords: list, timeout: float = 5.0) -> str: - """Внешняя функция для поиска ключевых слов.""" - return get_local_recognizer().listen_for_keywords(keywords, timeout) diff --git a/app/audio/tts.py b/app/audio/tts.py index 61c25bf..deab8ad 100644 --- a/app/audio/tts.py +++ b/app/audio/tts.py @@ -133,9 +133,19 @@ class TextToSpeech: model = self._load_model("ru") speaker = self.speaker_ru - # Проверка наличия спикера в модели (защита от ошибок конфига) - if hasattr(model, "speakers") and speaker not in model.speakers: - if model.speakers: + # Проверка наличия спикера в модели (защита от ошибок конфига). + # Для русского языка сохраняем мужской голос по умолчанию. + if hasattr(model, "speakers") and model.speakers: + if language == "ru": + male_speakers = ("eugene", "aidar") + if speaker not in model.speakers or speaker not in male_speakers: + for candidate in male_speakers: + if candidate in model.speakers: + speaker = candidate + break + else: + speaker = model.speakers[0] + elif speaker not in model.speakers: speaker = model.speakers[0] # Разбиваем текст на куски diff --git a/app/audio/wakeword.py b/app/audio/wakeword.py index 5b38b74..f07ed4d 100644 --- a/app/audio/wakeword.py +++ b/app/audio/wakeword.py @@ -21,6 +21,8 @@ class WakeWordDetector: self.audio_stream = None self.pa = None self._stream_closed = True # Флаг состояния потока (закрыт/открыт) + self._last_hit_ts = 0.0 + self._hit_streak = 0 def initialize(self): """Инициализация Porcupine и PyAudio.""" @@ -118,6 +120,8 @@ class WakeWordDetector: Returns: True, если фраза обнаружена прямо сейчас. """ + import time + if not self.porcupine: self.initialize() @@ -131,8 +135,17 @@ class WakeWordDetector: keyword_index = self.porcupine.process(pcm) if keyword_index >= 0: - print("🛑 Wake word обнаружен во время ответа!") - return True + now = time.time() + if now - self._last_hit_ts < 0.6: + self._hit_streak += 1 + else: + self._hit_streak = 1 + self._last_hit_ts = now + + if self._hit_streak >= 2: + self._hit_streak = 0 + print("🛑 Wake word подтвержден во время ответа!") + return True return False except Exception: return False diff --git a/app/core/ai.py b/app/core/ai.py index ef80a48..096a545 100644 --- a/app/core/ai.py +++ b/app/core/ai.py @@ -21,7 +21,8 @@ SYSTEM_PROMPT = """Ты — Александр, умный голосовой а # Требует возвращать ТОЛЬКО перевод, без лишних слов ("Конечно, вот перевод..."). TRANSLATION_SYSTEM_PROMPT = """You are a translation engine. Translate from {source} to {target}. -Return only the translated text, without quotes, comments, or explanations.""" +Return only the translated text, without quotes, comments, or explanations. +Keep the translation максимально кратким и естественным, без лишних слов.""" def _send_request(messages, max_tokens, temperature, error_text): diff --git a/app/core/config.py b/app/core/config.py index 2f743e9..9fe8dcb 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -33,10 +33,6 @@ PORCUPINE_ACCESS_KEY = os.getenv("PORCUPINE_ACCESS_KEY") # Путь к файлу модели ключевого слова (.ppn), который лежит в папке assets/models PORCUPINE_KEYWORD_PATH = BASE_DIR / "assets" / "models" / "Alexandr_en_linux_v4_0_0.ppn" -# --- Настройки локального распознавания (Vosk) --- -# Используется для стоп-команд и будильника, когда не нужен интернет -VOSK_MODEL_PATH = BASE_DIR / "assets" / "models" / "vosk-model-ru-0.42" - # --- Параметры аудио --- # Частота дискретизации для микрофона (стандарт для распознавания речи) SAMPLE_RATE = 16000 diff --git a/app/features/alarm.py b/app/features/alarm.py index 2c55768..33131eb 100644 --- a/app/features/alarm.py +++ b/app/features/alarm.py @@ -9,7 +9,7 @@ import re from datetime import datetime from pathlib import Path from ..core.config import BASE_DIR -from ..audio.local_stt import listen_for_keywords +from ..audio.stt import listen # Файл базы данных будильников ALARM_FILE = BASE_DIR / "data" / "alarms.json" @@ -90,7 +90,7 @@ class AlarmClock: """ Логика срабатывания будильника. Запускает воспроизведение MP3 через mpg123 и слушает команду "Стоп". - Использует локальное распознавание (Vosk), чтобы не зависеть от интернета. + Использует облачное распознавание речи для остановки. """ print("🔔 БУДИЛЬНИК ЗВОНИТ! (Скажите 'Стоп' или 'Александр стоп')") @@ -117,11 +117,12 @@ class AlarmClock: # Цикл ожидания стоп-команды while True: - # Слушаем локально (без интернета) - text = listen_for_keywords(stop_words, timeout=3.0) + text = listen(timeout_seconds=3.0, detection_timeout=3.0) if text: - print(f"🛑 Будильник остановлен по команде: '{text}'") - break + 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}") diff --git a/ding.wav b/ding.wav new file mode 100644 index 0000000..451843f Binary files /dev/null and b/ding.wav differ diff --git a/requirements.txt b/requirements.txt index f607bd8..a49cf6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -68,7 +68,6 @@ triton==3.5.1 typing-inspect==0.9.0 typing_extensions==4.15.0 urllib3==2.6.2 -vosk==0.3.45 websockets==15.0.1 yarl==1.22.0 pygame