From 7ca695848856480e6f50b4bd31add0e517f2876d Mon Sep 17 00:00:00 2001 From: future Date: Sun, 1 Mar 2026 12:49:39 +0300 Subject: [PATCH] Add configurable input device selection --- .env.example | 2 ++ README.md | 6 ++++-- app/audio/stt.py | 3 +++ app/audio/wakeword.py | 4 ++++ app/core/audio_manager.py | 31 +++++++++++++++++++++++++++++++ app/core/config.py | 6 ++++++ 6 files changed, 50 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 7228a0f..dc28f1f 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,8 @@ ANTHROPIC_API_VERSION=2023-06-01 DEEPGRAM_API_KEY=your_deepgram_api_key_here PORCUPINE_ACCESS_KEY=your_porcupine_access_key_here PORCUPINE_SENSITIVITY=0.8 +AUDIO_INPUT_DEVICE_INDEX= +AUDIO_INPUT_DEVICE_NAME= TTS_EN_SPEAKER=en_0 WEATHER_LAT=63.56 WEATHER_LON=53.69 diff --git a/README.md b/README.md index c33aefe..196bae6 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,8 @@ python run.py | `DEEPGRAM_API_KEY` | Да | - | Ключ Deepgram STT | | `PORCUPINE_ACCESS_KEY` | Да | - | Ключ PicoVoice Porcupine | | `PORCUPINE_SENSITIVITY` | Нет | `0.8` | Чувствительность wake word | +| `AUDIO_INPUT_DEVICE_INDEX` | Нет | - | Явный индекс входного устройства для wake word и STT | +| `AUDIO_INPUT_DEVICE_NAME` | Нет | - | Часть имени входного устройства, например `pulse` или `pipewire` | | `TTS_EN_SPEAKER` | Нет | `en_0` | Английский голос TTS | | `WEATHER_LAT` | Нет | - | Широта города по умолчанию | | `WEATHER_LON` | Нет | - | Долгота города по умолчанию | @@ -206,8 +208,8 @@ alexander_smart-speaker/ | Проблема | Что проверить | |---|---| -| Не реагирует на `Alexandr` | `PORCUPINE_ACCESS_KEY`, микрофон, чувствительность `PORCUPINE_SENSITIVITY` | -| STT не распознает речь | `DEEPGRAM_API_KEY`, сетевой доступ, выбранный микрофон | +| Не реагирует на `Alexandr` | `PORCUPINE_ACCESS_KEY`, микрофон, чувствительность `PORCUPINE_SENSITIVITY`, `AUDIO_INPUT_DEVICE_NAME` / `AUDIO_INPUT_DEVICE_INDEX` | +| STT не распознает речь | `DEEPGRAM_API_KEY`, сетевой доступ, выбранный микрофон, `AUDIO_INPUT_DEVICE_NAME` / `AUDIO_INPUT_DEVICE_INDEX` | | Нет звука | корректное аудиоустройство и доступность `pactl`/`amixer` | | Будильник/таймер не звонит | наличие `mpg123` в системе | | Ошибка про несколько AI API | в `.env` должен остаться только один незакомментированный AI ключ | diff --git a/app/audio/stt.py b/app/audio/stt.py index f23f972..fe7114d 100644 --- a/app/audio/stt.py +++ b/app/audio/stt.py @@ -128,13 +128,16 @@ class SpeechRecognizer: def _get_stream(self): """Открывает аудиопоток PyAudio, если он еще не открыт.""" if self.stream is None: + device_index, device_info = get_audio_manager().resolve_input_device() self.stream = self.pa.open( rate=SAMPLE_RATE, channels=1, format=pyaudio.paInt16, input=True, + input_device_index=device_index, frames_per_buffer=4096, ) + print(f"🎙️ STT input: {device_info.get('name', 'unknown')}") return self.stream async def _process_audio( diff --git a/app/audio/wakeword.py b/app/audio/wakeword.py index fc12ce6..c9c4f12 100644 --- a/app/audio/wakeword.py +++ b/app/audio/wakeword.py @@ -54,14 +54,18 @@ class WakeWordDetector: pass # Открываем поток с параметрами, которые требует Porcupine + device_index, device_info = get_audio_manager().resolve_input_device() self.audio_stream = self.pa.open( rate=self.porcupine.sample_rate, channels=1, format=pyaudio.paInt16, input=True, + input_device_index=device_index, frames_per_buffer=self.porcupine.frame_length, ) self._stream_closed = False + device_name = device_info.get("name", "unknown") + print(f"🎙️ Wake word input: {device_name}") def stop_monitoring(self): """Явная остановка и закрытие потока (чтобы освободить микрофон для других задач).""" diff --git a/app/core/audio_manager.py b/app/core/audio_manager.py index df89dff..d3c45c2 100644 --- a/app/core/audio_manager.py +++ b/app/core/audio_manager.py @@ -1,6 +1,8 @@ import pyaudio import threading +from .config import AUDIO_INPUT_DEVICE_INDEX, AUDIO_INPUT_DEVICE_NAME + class AudioManager: _instance = None @@ -17,6 +19,35 @@ class AudioManager: def get_pyaudio(self): return self.pa + def resolve_input_device(self): + """ + Возвращает индекс и информацию о входном устройстве. + Если индекс/имя не заданы в .env, используем системное default устройство. + """ + if AUDIO_INPUT_DEVICE_INDEX is not None: + info = self.pa.get_device_info_by_index(AUDIO_INPUT_DEVICE_INDEX) + if info.get("maxInputChannels", 0) <= 0: + raise ValueError( + f"AUDIO_INPUT_DEVICE_INDEX={AUDIO_INPUT_DEVICE_INDEX} не является входным устройством." + ) + return AUDIO_INPUT_DEVICE_INDEX, info + + if AUDIO_INPUT_DEVICE_NAME: + wanted = AUDIO_INPUT_DEVICE_NAME.lower() + for index in range(self.pa.get_device_count()): + info = self.pa.get_device_info_by_index(index) + if info.get("maxInputChannels", 0) <= 0: + continue + name = str(info.get("name", "")) + if wanted in name.lower(): + return index, info + raise ValueError( + f"Не найдено входное устройство по AUDIO_INPUT_DEVICE_NAME={AUDIO_INPUT_DEVICE_NAME!r}." + ) + + default_info = self.pa.get_default_input_device_info() + return None, default_info + def cleanup(self): if self.pa: self.pa.terminate() diff --git a/app/core/config.py b/app/core/config.py index b3c8261..40cb069 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -75,6 +75,12 @@ PORCUPINE_SENSITIVITY = float(os.getenv("PORCUPINE_SENSITIVITY", "0.8")) # Частота дискретизации для микрофона (стандарт для распознавания речи) SAMPLE_RATE = 16000 CHANNELS = 1 +# Явный выбор входного устройства. Можно задать либо индекс, либо часть имени. +_audio_input_device_index = os.getenv("AUDIO_INPUT_DEVICE_INDEX", "").strip() +AUDIO_INPUT_DEVICE_INDEX = ( + int(_audio_input_device_index) if _audio_input_device_index else None +) +AUDIO_INPUT_DEVICE_NAME = os.getenv("AUDIO_INPUT_DEVICE_NAME", "").strip() or None # --- Настройка времени --- # Устанавливаем часовой пояс на Москву, чтобы будильник работал корректно