from __future__ import annotations from pathlib import Path from pydantic_settings import BaseSettings, SettingsConfigDict ROOT = Path(__file__).resolve().parent.parent CATEGORIES = ( "groceries", "dining", "transport", "household", "health", "entertainment", "utilities", "office", "travel", "other", ) DOC_KINDS = ("receipt", "invoice", "document") ACCEPTED_SUFFIXES = { ".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".tif", ".tiff", ".pdf", ".txt", } class Settings(BaseSettings): model_config = SettingsConfigDict( env_prefix="RECEIPT_", env_file=str(ROOT / ".env"), env_file_encoding="utf-8", extra="ignore", ) root_dir: Path = ROOT data_dir: Path = ROOT / "data" inbox_dir: Path = ROOT / "inbox" processing_dir: Path = ROOT / "processing" processed_dir: Path = ROOT / "processed" failed_dir: Path = ROOT / "failed" exports_dir: Path = ROOT / "exports" idle_seconds: float = 30.0 # Gemma 4 12B Unified is the default omni brain (vision extract + embed). # It does not fit on the Lamp (6 GB). Point these URLs at the GPU box. llm_backend: str = "gemma" llm_base_url: str = "http://127.0.0.1:8080/v1" llm_model: str = "google/gemma-4-12B-it" llm_api_key: str = "local" llm_accepts_images: bool = True llm_max_tokens: int = 8192 llm_timeout_s: float = 180.0 # auto: Studio API if up, else vLLM, else Hermes custom_providers. # direct: RECEIPT_LLM_BASE_URL. hermes: Hermes-discovered Gemma. studio: POST /api/inbox. llm_route: str = "auto" studio_url: str = "" gpu_host: str = "" hermes_base_url: str = "" hermes_model: str = "google/gemma-4-12B-it" hermes_config_path: Path = Path.home() / ".hermes" / "config.yaml" embed_backend: str = "omni" embed_base_url: str = "" embed_model: str = "google/gemma-4-12B-it" embed_dim: int = 3840 embed_api_key: str = "local" embed_timeout_s: float = 120.0 embed_prefix: bool = True ocr_backend: str = "none" ocr_base_url: str = "http://127.0.0.1:11434/v1" ocr_model: str = "" ocr_api_key: str = "local" camera_url: str = "http://127.0.0.1:5001" snapshot_width: int = 1280 snapshot_quality: int = 85 sku_auto: float = 0.88 sku_review: float = 0.72 vendor_auto: float = 0.82 vendor_review: float = 0.65 ui_host: str = "127.0.0.1" ui_port: int = 7860 ui_share_lan: bool = False jpeg_max_edge: int = 2048 def model_post_init(self, _context: object) -> None: if not self.embed_base_url: object.__setattr__(self, "embed_base_url", self.llm_base_url) if not self.embed_model: object.__setattr__(self, "embed_model", self.llm_model) @property def db_path(self) -> Path: return self.data_dir / "receipts.db" def ensure_dirs(self) -> None: for path in ( self.data_dir, self.inbox_dir, self.processing_dir, self.processed_dir, self.failed_dir, self.exports_dir, ): path.mkdir(parents=True, exist_ok=True) def load_settings(**overrides: object) -> Settings: settings = Settings(**overrides) settings.ensure_dirs() return settings