from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path def _load_dotenv() -> None: here = Path(__file__).resolve() module_dir = here.parent candidates: list[Path] = [module_dir / ".env"] # Optional project-root fallback when running from full repository layout. if len(here.parents) > 2: candidates.append(here.parents[2] / ".env") for env_path in candidates: if not env_path.exists(): continue for raw_line in env_path.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) key = key.strip() value = value.strip().strip('"').strip("'") if key and key not in os.environ: os.environ[key] = value break _load_dotenv() def _env_bool(name: str, default: bool) -> bool: raw = os.environ.get(name) if raw is None: return default return raw.strip().lower() in {"1", "true", "yes", "on"} @dataclass class VoiceRuntimeConfig: groq_api_key: str = os.environ.get("GROQ_API_KEY", "") groq_model_id: str = os.environ.get("GROQ_MODEL_ID", "whisper-large-v3-turbo") sample_rate: int = int(os.environ.get("VOICE_SAMPLE_RATE", "16000")) silence_threshold_db: float = float(os.environ.get("VOICE_SILENCE_THRESHOLD_DB", "-40")) min_silence_sec: float = float(os.environ.get("VOICE_MIN_SILENCE_SEC", "0.30")) keep_padding_sec: float = float(os.environ.get("VOICE_KEEP_PADDING_SEC", "0.05")) analysis_window_ms: int = int(os.environ.get("VOICE_ANALYSIS_WINDOW_MS", "10")) diarization_enabled: bool = _env_bool("VOICE_DIARIZATION_ENABLED", True) diarization_model_id: str = os.environ.get("VOICE_DIARIZATION_MODEL_ID", "pyannote/speaker-diarization-3.1") hf_token: str = os.environ.get("HF_TOKEN", "") diarization_min_speakers: int = int(os.environ.get("VOICE_DIARIZATION_MIN_SPEAKERS", "0")) diarization_max_speakers: int = int(os.environ.get("VOICE_DIARIZATION_MAX_SPEAKERS", "0")) @classmethod def from_env(cls) -> "VoiceRuntimeConfig": return cls()