"""Centralised, environment-driven configuration. Every provider choice (LLM, ASR, TTS, storage) is a plain env var so the same image runs as the free HF Space ("light" mode) or against the local GPU box ("premium" mode) without code changes. The `Literal` types are intentionally restricted to *implemented* providers: adding an implementation means widening the Literal, so the config can never silently point at a backend that does not exist yet. """ from functools import lru_cache from typing import Literal from pydantic import SecretStr from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", ) # --- App --- app_env: Literal["dev", "prod"] = "dev" log_level: str = "INFO" # --- Languages --- default_source_lang: str = "fr" default_target_lang: str = "en" # --- LLM --- llm_provider: Literal["fake", "gemini", "openai", "mistral", "ollama"] = "fake" llm_model: str = "gemini-2.5-flash" llm_api_key: SecretStr | None = None llm_base_url: str | None = None llm_timeout_s: float = 30.0 # --- CEFR classifier (M1) --- cefr_model_path: str | None = None # local ONNX artifact dir (dev) — takes precedence cefr_model_id: str | None = None # HF model repo id (Space; HF_TOKEN env for private repos) cache_dir: str = ".cache/tutor" # content-addressed cache for LLM products # --- ASR (M2) / TTS / storage --- asr_provider: Literal["fake", "faster_whisper"] = "fake" asr_model: str = "small" # faster-whisper size; see docs/evals/m2_asr_latency.md asr_compute_type: str = "int8" asr_cpu_threads: int = 2 tts_provider: Literal["fake"] = "fake" # M2 TTS is browser-side (Web Speech API) storage_backend: Literal["memory"] = "memory" # --- Gradio --- host: str = "0.0.0.0" port: int = 7860 gradio_auth_username: str | None = None gradio_auth_password: SecretStr | None = None @lru_cache def get_settings() -> Settings: """Process-wide settings singleton (tests build `Settings` directly instead).""" return Settings()