""" Configuration module for smart speaker. Loads environment variables from .env file. """ # Этот модуль отвечает за конфигурацию всего проекта. # Он загружает настройки из файла .env (переменные окружения) и определяет константы. import os import re import time from io import StringIO from pathlib import Path from dotenv import dotenv_values # Базовая директория проекта (корневая папка, где лежит .env) BASE_DIR = Path(__file__).resolve().parents[2] def _load_project_env(env_path: Path) -> None: """ Загружает .env, игнорируя строковый "шум" без формата KEY=VALUE. Это делает конфиг устойчивым к человеческим комментариям без символа '#'. """ if not env_path.exists(): return raw_text = env_path.read_text(encoding="utf-8") sanitized_lines = [] for line in raw_text.splitlines(): stripped = line.strip() if not stripped or stripped.startswith("#"): sanitized_lines.append(line) continue if "=" in line: key = line.split("=", 1)[0].strip() if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", key): sanitized_lines.append(line) continue # Игнорируем невалидные строки, чтобы dotenv не шумел warning'ами. sanitized_lines.append(f"# ignored invalid env line: {line}") parsed = dotenv_values(stream=StringIO("\n".join(sanitized_lines))) for key, value in parsed.items(): if key and value is not None and os.getenv(key) is None: os.environ[key] = value # Загружаем переменные из .env в корневом каталоге _load_project_env(BASE_DIR / ".env") # --- Настройки AI --- # AI_PROVIDER опционален. Приоритет у единственного активного AI API key. # Если активных ключей несколько, AI-модуль вернет ошибку конфигурации. AI_PROVIDER = os.getenv("AI_PROVIDER", "openrouter").strip().lower() # OpenRouter OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY") OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "openai/gpt-4o-mini") OPENROUTER_API_URL = os.getenv( "OPENROUTER_API_URL", "https://openrouter.ai/api/v1/chat/completions" ) def _read_clamped_float_env(name: str, default: str, minimum: float, maximum: float) -> float: try: value = float(os.getenv(name, default)) except Exception: value = float(default) return max(minimum, min(maximum, value)) def _read_clamped_int_env(name: str, default: str, minimum: int, maximum: int) -> int: try: value = int(os.getenv(name, default)) except Exception: value = int(default) return max(minimum, min(maximum, value)) # OpenAI OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini") OPENAI_API_URL = os.getenv( "OPENAI_API_URL", "https://api.openai.com/v1/chat/completions" ) # Gemini (через официальный OpenAI-compatible endpoint Google) GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-flash") GEMINI_API_URL = os.getenv( "GEMINI_API_URL", "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", ) # Z.ai ZAI_API_KEY = os.getenv("ZAI_API_KEY") ZAI_MODEL = os.getenv("ZAI_MODEL", "glm-5") ZAI_API_URL = os.getenv( "ZAI_API_URL", "https://api.z.ai/api/paas/v4/chat/completions" ) # Anthropic Claude ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") ANTHROPIC_MODEL = os.getenv("ANTHROPIC_MODEL", "claude-sonnet-4-20250514") ANTHROPIC_API_URL = os.getenv( "ANTHROPIC_API_URL", "https://api.anthropic.com/v1/messages" ) ANTHROPIC_API_VERSION = os.getenv("ANTHROPIC_API_VERSION", "2023-06-01") # Ollama (локальные модели; OpenAI-compatible endpoint) # Обычно Ollama слушает http://localhost:11434 и предоставляет /v1/chat/completions. OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "llama3.1:8b") OLLAMA_API_URL = os.getenv( "OLLAMA_API_URL", "http://localhost:11434/v1/chat/completions" ) AI_CHAT_TEMPERATURE = _read_clamped_float_env("AI_CHAT_TEMPERATURE", "0.9", 0.0, 2.0) AI_CHAT_MAX_TOKENS = _read_clamped_int_env("AI_CHAT_MAX_TOKENS", "220", 80, 700) AI_CHAT_MAX_CHARS = _read_clamped_int_env("AI_CHAT_MAX_CHARS", "320", 120, 1200) AI_INTENT_TEMPERATURE = _read_clamped_float_env("AI_INTENT_TEMPERATURE", "0.0", 0.0, 1.0) AI_TRANSLATION_TEMPERATURE = _read_clamped_float_env( "AI_TRANSLATION_TEMPERATURE", "0.2", 0.0, 1.0 ) # --- Настройки распознавания речи (Deepgram) --- # Ключ для облачного STT (Speech-to-Text) DEEPGRAM_API_KEY = os.getenv("DEEPGRAM_API_KEY") # --- Настройки активации голосом (Porcupine) --- # Ключ доступа PicoVoice PORCUPINE_ACCESS_KEY = os.getenv("PORCUPINE_ACCESS_KEY") # Wake word label and common ASR aliases. WAKE_WORD = "Waltron" WAKE_WORD_ALIASES = ( "waltron", "voltron", "волтрон", "уолтрон", "валтрон", ) # Путь к файлу модели ключевого слова (.ppn), который лежит в папке assets/models PORCUPINE_KEYWORD_PATH = BASE_DIR / "assets" / "models" / "Waltron_en_linux_v4_0_0.ppn" # Чувствительность wake word (0..1). Выше = ловит легче, но больше ложных срабатываний. PORCUPINE_SENSITIVITY = float(os.getenv("PORCUPINE_SENSITIVITY", "0.8")) # Антифантомный фильтр wake word по RMS-сигналу. # Чем выше WAKEWORD_MIN_RMS / WAKEWORD_RMS_MULTIPLIER, тем меньше ложных срабатываний, # но тем выше риск не распознать очень тихую активацию. try: WAKEWORD_MIN_RMS = float(os.getenv("WAKEWORD_MIN_RMS", "120")) except Exception: WAKEWORD_MIN_RMS = 120.0 WAKEWORD_MIN_RMS = max(0.0, WAKEWORD_MIN_RMS) try: WAKEWORD_RMS_MULTIPLIER = float(os.getenv("WAKEWORD_RMS_MULTIPLIER", "1.7")) except Exception: WAKEWORD_RMS_MULTIPLIER = 1.7 WAKEWORD_RMS_MULTIPLIER = max(1.0, WAKEWORD_RMS_MULTIPLIER) try: WAKEWORD_HIT_COOLDOWN_SECONDS = float( os.getenv("WAKEWORD_HIT_COOLDOWN_SECONDS", "1.2") ) except Exception: WAKEWORD_HIT_COOLDOWN_SECONDS = 1.2 WAKEWORD_HIT_COOLDOWN_SECONDS = max(0.0, WAKEWORD_HIT_COOLDOWN_SECONDS) try: WAKEWORD_REOPEN_GRACE_SECONDS = float( os.getenv("WAKEWORD_REOPEN_GRACE_SECONDS", "0.45") ) except Exception: WAKEWORD_REOPEN_GRACE_SECONDS = 0.45 WAKEWORD_REOPEN_GRACE_SECONDS = max(0.0, WAKEWORD_REOPEN_GRACE_SECONDS) WAKEWORD_ENABLE_FALLBACK_STT = ( os.getenv("WAKEWORD_ENABLE_FALLBACK_STT", "0").strip().lower() in {"1", "true", "yes", "on"} ) # При активации wake word музыка приглушается до указанного процента от текущего уровня. WAKEWORD_MUSIC_DUCK_PERCENT = _read_clamped_int_env( "WAKEWORD_MUSIC_DUCK_PERCENT", "20", 1, 100 ) WAKEWORD_MUSIC_DUCK_RATIO = WAKEWORD_MUSIC_DUCK_PERCENT / 100.0 # --- Параметры аудио --- # Частота дискретизации для микрофона (стандарт для распознавания речи) SAMPLE_RATE = 16000 CHANNELS = 1 # Выбор устройства ввода (микрофона). # Если не задано, используем default input device PortAudio (если есть). # Пример: # - AUDIO_INPUT_DEVICE_NAME=pulse # - AUDIO_INPUT_DEVICE_INDEX=2 AUDIO_INPUT_DEVICE_NAME = os.getenv("AUDIO_INPUT_DEVICE_NAME", "").strip() or None _audio_index_raw = os.getenv("AUDIO_INPUT_DEVICE_INDEX", "").strip() try: AUDIO_INPUT_DEVICE_INDEX = int(_audio_index_raw) if _audio_index_raw else None except Exception: AUDIO_INPUT_DEVICE_INDEX = None # Выбор устройства вывода (динамик). # Если не задано, используем default output device PortAudio (если есть). # Пример: # - AUDIO_OUTPUT_DEVICE_NAME=pulse # - AUDIO_OUTPUT_DEVICE_INDEX=5 AUDIO_OUTPUT_DEVICE_NAME = os.getenv("AUDIO_OUTPUT_DEVICE_NAME", "").strip() or None _audio_out_index_raw = os.getenv("AUDIO_OUTPUT_DEVICE_INDEX", "").strip() try: AUDIO_OUTPUT_DEVICE_INDEX = ( int(_audio_out_index_raw) if _audio_out_index_raw else None ) except Exception: AUDIO_OUTPUT_DEVICE_INDEX = None # --- Настройка времени --- # Устанавливаем часовой пояс на Москву, чтобы будильник работал корректно 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 = BASE_DIR / "assets" / "sounds" / "alisa-golosovoj-pomoschnik.mp3" if not _stt_sfx_default.exists(): _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) # Звук старта STT всегда на 100% громкости, чтобы по уровню был как обычный TTS-ответ. STT_START_SOUND_VOLUME = 1.0 # Голос для русского языка (eugene - мужской голос) TTS_SPEAKER = "eugene" # Доступные (ru): aidar, baya, kseniya, xenia, eugene # Голос для английского языка TTS_EN_SPEAKER = os.getenv("TTS_EN_SPEAKER", "en_0") # Частота дискретизации для воспроизведения (качество звука) TTS_SAMPLE_RATE = 48000 # Скорость TTS: 1.0 = обычная, <1.0 = медленнее, >1.0 = быстрее. # По умолчанию чуть медленнее для более разборчивой речи. TTS_SPEED = _read_clamped_float_env("TTS_SPEED", "0.96", 0.85, 1.15) # --- Настройки погоды --- WEATHER_LAT = os.getenv("WEATHER_LAT") WEATHER_LON = os.getenv("WEATHER_LON") WEATHER_CITY = os.getenv("WEATHER_CITY", "Ухта") # --- Настройки Navidrome (музыка) --- NAVIDROME_URL = os.getenv("NAVIDROME_URL", "").strip().rstrip("/") NAVIDROME_USERNAME = os.getenv("NAVIDROME_USERNAME", "").strip() NAVIDROME_PASSWORD = os.getenv("NAVIDROME_PASSWORD", "")