import json import os from dataclasses import dataclass from pathlib import Path def _parse_bool(val: str) -> bool: return val.strip().lower() in {"1", "true", "yes", "on"} @dataclass class Config: hotkey: str = "Cmd+m" ffmpeg_input: str = "pulse:default" ffmpeg_path: str = "" whisper_model: str = "base" whisper_lang: str = "en" whisper_device: str = "cpu" whisper_extra_args: str = "" whisper_timeout_sec: int = 300 record_timeout_sec: int = 120 segment_sec: int = 5 streaming: bool = False injection_backend: str = "clipboard" ai_enabled: bool = False ai_provider: str = "ollama" ai_model: str = "llama3.2:3b" ai_temperature: float = 0.0 ai_system_prompt_file: str = "" ai_base_url: str = "http://localhost:11434" ai_api_key: str = "" ai_timeout_sec: int = 20 def default_path() -> Path: return Path.home() / ".config" / "lel" / "config.json" def load(path: str | None) -> Config: cfg = Config() p = Path(path) if path else default_path() if p.exists(): data = json.loads(p.read_text(encoding="utf-8")) for k, v in data.items(): if hasattr(cfg, k): setattr(cfg, k, v) # env overrides if os.getenv("WHISPER_MODEL"): cfg.whisper_model = os.environ["WHISPER_MODEL"] if os.getenv("WHISPER_LANG"): cfg.whisper_lang = os.environ["WHISPER_LANG"] if os.getenv("WHISPER_DEVICE"): cfg.whisper_device = os.environ["WHISPER_DEVICE"] if os.getenv("WHISPER_EXTRA_ARGS"): cfg.whisper_extra_args = os.environ["WHISPER_EXTRA_ARGS"] if os.getenv("WHISPER_FFMPEG_IN"): cfg.ffmpeg_input = os.environ["WHISPER_FFMPEG_IN"] if os.getenv("WHISPER_STREAM"): cfg.streaming = _parse_bool(os.environ["WHISPER_STREAM"]) if os.getenv("WHISPER_SEGMENT_SEC"): cfg.segment_sec = int(os.environ["WHISPER_SEGMENT_SEC"]) if os.getenv("WHISPER_TIMEOUT_SEC"): cfg.whisper_timeout_sec = int(os.environ["WHISPER_TIMEOUT_SEC"]) if os.getenv("LEL_FFMPEG_PATH"): cfg.ffmpeg_path = os.environ["LEL_FFMPEG_PATH"] if os.getenv("LEL_RECORD_TIMEOUT_SEC"): cfg.record_timeout_sec = int(os.environ["LEL_RECORD_TIMEOUT_SEC"]) if os.getenv("LEL_HOTKEY"): cfg.hotkey = os.environ["LEL_HOTKEY"] if os.getenv("LEL_INJECTION_BACKEND"): cfg.injection_backend = os.environ["LEL_INJECTION_BACKEND"] if os.getenv("LEL_AI_ENABLED"): cfg.ai_enabled = _parse_bool(os.environ["LEL_AI_ENABLED"]) if os.getenv("LEL_AI_PROVIDER"): cfg.ai_provider = os.environ["LEL_AI_PROVIDER"] if os.getenv("LEL_AI_MODEL"): cfg.ai_model = os.environ["LEL_AI_MODEL"] if os.getenv("LEL_AI_TEMPERATURE"): cfg.ai_temperature = float(os.environ["LEL_AI_TEMPERATURE"]) if os.getenv("LEL_AI_SYSTEM_PROMPT_FILE"): cfg.ai_system_prompt_file = os.environ["LEL_AI_SYSTEM_PROMPT_FILE"] if os.getenv("LEL_AI_BASE_URL"): cfg.ai_base_url = os.environ["LEL_AI_BASE_URL"] if os.getenv("LEL_AI_API_KEY"): cfg.ai_api_key = os.environ["LEL_AI_API_KEY"] if os.getenv("LEL_AI_TIMEOUT_SEC"): cfg.ai_timeout_sec = int(os.environ["LEL_AI_TIMEOUT_SEC"]) if not cfg.hotkey: raise ValueError("hotkey cannot be empty") if cfg.record_timeout_sec <= 0: raise ValueError("record_timeout_sec must be > 0") if cfg.whisper_timeout_sec <= 0: raise ValueError("whisper_timeout_sec must be > 0") return cfg def redacted_dict(cfg: Config) -> dict: d = cfg.__dict__.copy() d["ai_api_key"] = "" return d