Files
smart-speaker/app/core/config.py
2026-04-09 21:03:02 +03:00

251 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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", "")