251 lines
11 KiB
Python
251 lines
11 KiB
Python
"""
|
||
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", "")
|