другая структура проекта + beads + александр повтори + комментарии везде + readme

This commit is contained in:
2026-01-09 04:14:50 +03:00
parent 242ead5355
commit ce28fede74
31 changed files with 1654 additions and 1333 deletions

180
app/audio/wakeword.py Normal file
View File

@@ -0,0 +1,180 @@
"""
Wake word detection module using Porcupine.
Listens for the "Alexandr" wake word.
"""
# Этот модуль отвечает за "уши" ассистента в режиме ожидания.
# Он использует библиотеку Porcupine для эффективного (мало CPU) обнаружения ключевой фразы "Alexandr".
import pvporcupine
import pyaudio
import struct
from ..core.config import PORCUPINE_ACCESS_KEY, PORCUPINE_KEYWORD_PATH
class WakeWordDetector:
"""Класс для обнаружения wake word с использованием Porcupine."""
def __init__(self):
self.porcupine = None
self.audio_stream = None
self.pa = None
self._stream_closed = True # Флаг состояния потока (закрыт/открыт)
def initialize(self):
"""Инициализация Porcupine и PyAudio."""
# Создаем экземпляр Porcupine с нашим ключом доступа и файлом модели (.ppn)
self.porcupine = pvporcupine.create(
access_key=PORCUPINE_ACCESS_KEY, keyword_paths=[str(PORCUPINE_KEYWORD_PATH)]
)
self.pa = pyaudio.PyAudio()
self._open_stream()
print("🎤 Ожидание wake word 'Alexandr'...")
def _open_stream(self):
"""Открытие аудиопотока с микрофона."""
if self.audio_stream and not self._stream_closed:
return # Уже открыт
# Если был открыт старый поток, пробуем закрыть
if self.audio_stream:
try:
self.audio_stream.close()
except:
pass
# Открываем поток с параметрами, которые требует Porcupine
self.audio_stream = self.pa.open(
rate=self.porcupine.sample_rate,
channels=1,
format=pyaudio.paInt16,
input=True,
frames_per_buffer=self.porcupine.frame_length,
)
self._stream_closed = False
def stop_monitoring(self):
"""Явная остановка и закрытие потока (чтобы освободить микрофон для других задач)."""
if self.audio_stream and not self._stream_closed:
try:
self.audio_stream.stop_stream()
self.audio_stream.close()
except:
pass
self._stream_closed = True
def wait_for_wakeword(self, timeout: float = None) -> bool:
"""
Блокирующая функция: ждет, пока не будет услышана фраза "Alexandr"
или пока не истечет timeout.
Args:
timeout: Максимальное время ожидания в секундах. None = ждать бесконечно.
Returns:
True, если фраза обнаружена. False, если вышел таймаут.
"""
import time
if not self.porcupine:
self.initialize()
# Убеждаемся, что поток открыт
self._open_stream()
start_time = time.time()
while True:
# Проверка таймаута
if timeout and (time.time() - start_time > timeout):
return False
# Читаем небольшой кусочек аудио (frame)
pcm = self.audio_stream.read(
self.porcupine.frame_length, exception_on_overflow=False
)
# Конвертируем байты в кортеж чисел (требование Porcupine)
pcm = struct.unpack_from("h" * self.porcupine.frame_length, pcm)
# Обрабатываем фрейм через Porcupine
keyword_index = self.porcupine.process(pcm)
# Если keyword_index >= 0, значит ключевое слово обнаружено
if keyword_index >= 0:
print("✅ Wake word обнаружен!")
# Важно: закрываем поток, чтобы освободить микрофон для STT (Deepgram)
self.stop_monitoring()
return True
def check_wakeword_once(self) -> bool:
"""
Неблокирующая проверка (один кадр).
Используется во время того, как ассистент говорит (TTS),
чтобы проверить, не пытается ли пользователь его перебить.
Returns:
True, если фраза обнаружена прямо сейчас.
"""
if not self.porcupine:
self.initialize()
try:
self._open_stream()
pcm = self.audio_stream.read(
self.porcupine.frame_length, exception_on_overflow=False
)
pcm = struct.unpack_from("h" * self.porcupine.frame_length, pcm)
keyword_index = self.porcupine.process(pcm)
if keyword_index >= 0:
print("🛑 Wake word обнаружен во время ответа!")
return True
return False
except Exception:
return False
def cleanup(self):
"""Освобождение ресурсов при выходе."""
self.stop_monitoring()
if self.pa:
self.pa.terminate()
if self.porcupine:
self.porcupine.delete()
# Глобальный экземпляр детектора (Singleton)
_detector = None
def get_detector() -> WakeWordDetector:
"""Получить или создать глобальный экземпляр детектора."""
global _detector
if _detector is None:
_detector = WakeWordDetector()
return _detector
def wait_for_wakeword(timeout: float = None) -> bool:
"""Внешняя функция для ожидания wake word."""
return get_detector().wait_for_wakeword(timeout)
def stop_monitoring():
"""Внешняя функция для остановки мониторинга."""
if _detector:
_detector.stop_monitoring()
def cleanup():
"""Внешняя функция очистки ресурсов."""
global _detector
if _detector:
_detector.cleanup()
_detector = None
def check_wakeword_once() -> bool:
"""Внешняя функция для быстрой проверки."""
return get_detector().check_wakeword_once()