"""Runtime configuration for AI Prof. Both models are reached over OpenAI-compatible HTTP, which is what the common self-hosted runtimes expose: - MiniCPM-V (eyes) -> llama.cpp ``llama-server`` with ``--mmproj`` - Nemotron (brain) -> vLLM / llama.cpp chat endpoint If a base URL is left blank, that model falls back to MOCK mode so the whole app still runs end-to-end with no weights — useful for UI work and demos. """ from __future__ import annotations import os from dataclasses import dataclass from dotenv import load_dotenv load_dotenv() def _clean(value: str | None) -> str | None: """Treat blank / placeholder env values as unset.""" if value is None: return None value = value.strip() return value or None @dataclass(frozen=True) class ModelConfig: base_url: str | None api_key: str model: str @property def is_live(self) -> bool: return self.base_url is not None @property def openai_base_url(self) -> str | None: """Return an OpenAI-compatible base URL ending in /v1.""" if self.base_url is None: return None base = self.base_url.rstrip("/") return base if base.endswith("/v1") else f"{base}/v1" @dataclass(frozen=True) class Config: vision: ModelConfig brain: ModelConfig tts: ModelConfig stt: ModelConfig slide_dpi: int tts_voice: str deck_cache_dir: str hf_deck_cache_repo: str | None hf_token: str | None hf_deck_cache_write: bool @property def fully_mocked(self) -> bool: return not self.vision.is_live and not self.brain.is_live def load_config() -> Config: return Config( vision=ModelConfig( base_url=_clean(os.getenv("VISION_BASE_URL")), api_key=_clean(os.getenv("VISION_API_KEY")) or "sk-no-key-required", model=_clean(os.getenv("VISION_MODEL")) or "minicpm-v", ), brain=ModelConfig( base_url=_clean(os.getenv("BRAIN_BASE_URL")), api_key=_clean(os.getenv("BRAIN_API_KEY")) or "sk-no-key-required", model=_clean(os.getenv("BRAIN_MODEL")) or "nemotron-3-nano", ), tts=ModelConfig( base_url=_clean(os.getenv("TTS_BASE_URL")), api_key=_clean(os.getenv("TTS_API_KEY")) or "sk-no-key-required", model=_clean(os.getenv("TTS_MODEL")) or "tts-1", ), stt=ModelConfig( base_url=_clean(os.getenv("STT_BASE_URL")), api_key=_clean(os.getenv("STT_API_KEY")) or "sk-no-key-required", model=_clean(os.getenv("STT_MODEL")) or "whisper-1", ), slide_dpi=int(_clean(os.getenv("SLIDE_DPI")) or "150"), tts_voice=_clean(os.getenv("TTS_VOICE")) or "default", deck_cache_dir=_clean(os.getenv("DECK_CACHE_DIR")) or ".cache/ai-prof/decks", hf_deck_cache_repo=_clean(os.getenv("HF_DECK_CACHE_REPO")), hf_token=_clean(os.getenv("HF_TOKEN")), hf_deck_cache_write=( (_clean(os.getenv("HF_DECK_CACHE_WRITE")) or "").lower() in {"1", "true", "yes", "on"} ), ) CONFIG = load_config()