diff --git a/.env.example b/.env.example index ea382c2..92d72e4 100644 --- a/.env.example +++ b/.env.example @@ -40,6 +40,9 @@ PORCUPINE_SENSITIVITY=0.8 # AUDIO_INPUT_DEVICE_INDEX=2 # AUDIO_OUTPUT_DEVICE_NAME=pulse # AUDIO_OUTPUT_DEVICE_INDEX=5 +# STT start sound (played after wake word before listening) +# STT_START_SOUND_PATH=~/Music/alisa-golosovoj-pomoschnik.mp3 +# STT_START_SOUND_VOLUME=0.25 TTS_EN_SPEAKER=en_0 WEATHER_LAT=63.56 WEATHER_LON=53.69 diff --git a/README.md b/README.md index 5136edd..37a9ef7 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,8 @@ python run.py | `AUDIO_INPUT_DEVICE_INDEX` | Нет | auto | Индекс PortAudio для микрофона (приоритетнее `AUDIO_INPUT_DEVICE_NAME`) | | `AUDIO_OUTPUT_DEVICE_NAME` | Нет | auto | Подстрока имени динамика/выхода (например `pulse`) | | `AUDIO_OUTPUT_DEVICE_INDEX` | Нет | auto | Индекс PortAudio для вывода (приоритетнее `AUDIO_OUTPUT_DEVICE_NAME`) | +| `STT_START_SOUND_PATH` | Нет | `~/Music/alisa-golosovoj-pomoschnik.mp3` | Короткий звук после wake word и перед стартом STT (wav/mp3) | +| `STT_START_SOUND_VOLUME` | Нет | `0.25` | Громкость звука старта STT (0..1) | | `TTS_EN_SPEAKER` | Нет | `en_0` | Английский голос TTS | | `WEATHER_LAT` | Нет | - | Широта города по умолчанию | | `WEATHER_LON` | Нет | - | Долгота города по умолчанию | diff --git a/app/core/config.py b/app/core/config.py index 7c96c6c..1d6dcc8 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -125,6 +125,20 @@ os.environ["TZ"] = "Europe/Moscow" time.tzset() # --- Настройки синтеза речи (TTS) --- + +# --- Sound effects (SFX) --- +# Короткий "beep" после wake word и перед запуском STT, чтобы пользователь понял: +# можно начинать говорить. Поддерживает wav/mp3 (если pygame mixer поддерживает mp3), +# иначе будет использован mpg123 как fallback. +_stt_sfx_default = Path.home() / "Music" / "alisa-golosovoj-pomoschnik.mp3" +STT_START_SOUND_PATH = os.getenv("STT_START_SOUND_PATH", "").strip() or str( + _stt_sfx_default +) +try: + STT_START_SOUND_VOLUME = float(os.getenv("STT_START_SOUND_VOLUME", "0.25")) +except Exception: + STT_START_SOUND_VOLUME = 0.25 +STT_START_SOUND_VOLUME = max(0.0, min(1.0, STT_START_SOUND_VOLUME)) # Голос для русского языка (eugene - мужской голос) TTS_SPEAKER = "eugene" # Доступные (ru): aidar, baya, kseniya, xenia, eugene # Голос для английского языка diff --git a/app/main.py b/app/main.py index f642005..e0e91f7 100644 --- a/app/main.py +++ b/app/main.py @@ -7,6 +7,8 @@ import signal import sys import time from collections import deque +from pathlib import Path +import subprocess # Для воспроизведения звуков (mp3) try: @@ -34,7 +36,12 @@ from .audio.wakeword import ( stop_monitoring as stop_wakeword_monitoring, ) from .core.ai import ask_ai_stream, interpret_assistant_intent, translate_text -from .core.config import BASE_DIR, WAKE_WORD +from .core.config import ( + BASE_DIR, + STT_START_SOUND_PATH, + STT_START_SOUND_VOLUME, + WAKE_WORD, +) from .core.cleaner import clean_response from .core.commands import is_stop_command from .core.smalltalk import get_smalltalk_response @@ -221,6 +228,7 @@ def main(): # Инициализация звуковой системы для эффектов (опционально) ding_sound = None + stt_start_sound_path = Path(STT_START_SOUND_PATH).expanduser() if mixer is None: print( "Warning: pygame mixer not available; sound effects disabled." @@ -232,12 +240,58 @@ def main(): except Exception as exc: print(f"Warning: pygame mixer init failed; sound effects disabled. ({exc})") else: - ding_sound_path = BASE_DIR / "assets" / "sounds" / "ding.wav" - if ding_sound_path.exists(): - ding_sound = mixer.Sound(str(ding_sound_path)) - ding_sound.set_volume(0.3) - else: - print(f"⚠️ Звук {ding_sound_path} не найден") + # Приоритет: внешний звук для старта STT (обычно в ~/Music), + # fallback: assets/sounds/ding.wav + candidate_paths = [ + stt_start_sound_path, + BASE_DIR / "assets" / "sounds" / "ding.wav", + ] + for candidate in candidate_paths: + if not candidate or not Path(candidate).exists(): + continue + try: + ding_sound = mixer.Sound(str(candidate)) + ding_sound.set_volume(float(STT_START_SOUND_VOLUME)) + break + except Exception as exc: + print(f"⚠️ Не удалось загрузить звук {candidate}: {exc}") + ding_sound = None + + def _play_stt_start_sfx_fallback(): + """Fallback для систем без pygame/mixer или без поддержки mp3.""" + if not stt_start_sound_path.exists(): + return False + # mpg123 scale factor: 0..32768 (примерно). Делаем тихо. + scale = int(32768 * float(STT_START_SOUND_VOLUME)) + scale = max(0, min(32768, scale)) + try: + subprocess.run( + ["mpg123", "-q", "-f", str(scale), str(stt_start_sound_path)], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return True + except FileNotFoundError: + return False + except Exception: + return False + + def play_stt_start_sfx(): + """Проиграть короткий звук старта STT синхронно (чтобы не попасть в распознавание).""" + if ding_sound is not None: + try: + ch = ding_sound.play() + # Если pygame не вернул канал, ничего не ждем. + if ch is None: + return True + # Ждем завершения звука. + while ch.get_busy(): + time.sleep(0.01) + return True + except Exception: + pass + return _play_stt_start_sfx_fallback() get_recognizer().initialize() # Подключение к Deepgram init_tts() # Загрузка нейросети для синтеза речи (Silero) @@ -298,8 +352,7 @@ def main(): continue # Звук активации - if ding_sound: - ding_sound.play() + play_stt_start_sfx() # Слушаем команду try: