import os from dataclasses import dataclass, field from pathlib import Path from dotenv import load_dotenv BASE_DIR = Path(__file__).resolve().parent.parent load_dotenv(BASE_DIR / ".env") def _resolve_path(env_name: str, default_relative_path: str) -> str: raw_value = os.getenv(env_name) path = Path(raw_value) if raw_value else BASE_DIR / default_relative_path return str(path if path.is_absolute() else (BASE_DIR / path).resolve()) def _resolve_optional_path( env_name: str, default_relative_path: str | None = None, ) -> str | None: raw_value = os.getenv(env_name) if raw_value: path = Path(raw_value) return str(path if path.is_absolute() else (BASE_DIR / path).resolve()) if not default_relative_path: return None default_path = (BASE_DIR / default_relative_path).resolve() if default_path.exists(): return str(default_path) return None def _parse_origins() -> tuple[str, ...]: raw_origins = os.getenv( "CORS_ORIGINS", "http://127.0.0.1:5173,http://localhost:5173", ) return tuple( origin.strip() for origin in raw_origins.split(",") if origin.strip() ) def _parse_bool(name: str, default: bool) -> bool: raw_value = os.getenv(name) if raw_value is None: return default return raw_value.strip().lower() in {"1", "true", "yes", "on"} @dataclass(frozen=True) class Settings: """ Central configuration for the inspection system. """ # ----------------------------- # PATHS # ----------------------------- BASE_DIR: str = str(BASE_DIR) MODEL_PATH: str = field( default_factory=lambda: _resolve_path("MODEL_PATH", "models/steel_inspection.pt") ) IMAGE_FOLDER: str = field( default_factory=lambda: _resolve_path("IMAGE_FOLDER", "test_images") ) REPORT_FOLDER: str = field( default_factory=lambda: _resolve_path("REPORT_FOLDER", "reports") ) FRONTEND_DIST: str = field( default_factory=lambda: _resolve_path("FRONTEND_DIST", "dashboard/dist") ) # ----------------------------- # DEMO / SIMULATION SETTINGS # ----------------------------- IMAGE_REPEAT: int = field(default_factory=lambda: int(os.getenv("IMAGE_REPEAT", "5"))) BLANK_FRAMES: int = field(default_factory=lambda: int(os.getenv("BLANK_FRAMES", "3"))) # ----------------------------- # TRACKING / LIFECYCLE # ----------------------------- MAX_MISSING_FRAMES: int = field( default_factory=lambda: int(os.getenv("MAX_MISSING_FRAMES", "5")) ) # ----------------------------- # MODEL / INFERENCE SETTINGS # ----------------------------- CONF_THRESHOLD: float = field(default_factory=lambda: float(os.getenv("CONF_THRESHOLD", "0.20"))) SECONDARY_CONF_THRESHOLD: float = field( default_factory=lambda: float(os.getenv("SECONDARY_CONF_THRESHOLD", "0.25")) ) IMAGE_SIZE: int = field(default_factory=lambda: int(os.getenv("IMAGE_SIZE", "896"))) MIN_DEFECT_AREA: int = field(default_factory=lambda: int(os.getenv("MIN_DEFECT_AREA", "120"))) MAX_AREA_RATIO: float = field(default_factory=lambda: float(os.getenv("MAX_AREA_RATIO", "0.12"))) SURFACE_GATE_ENABLED: bool = field( default_factory=lambda: _parse_bool("SURFACE_GATE_ENABLED", False) ) SURFACE_DETECTOR_PATH: str | None = field( default_factory=lambda: _resolve_optional_path( "SURFACE_DETECTOR_PATH", "models/steel_surface_detector.pt", ) ) SURFACE_DETECTOR_CLASS_NAME: str = field( default_factory=lambda: os.getenv("SURFACE_DETECTOR_CLASS_NAME", "steel_surface") ) SURFACE_DETECTOR_CONFIDENCE: float = field( default_factory=lambda: float(os.getenv("SURFACE_DETECTOR_CONFIDENCE", "0.40")) ) SURFACE_DETECTOR_MIN_AREA_RATIO: float = field( default_factory=lambda: float(os.getenv("SURFACE_DETECTOR_MIN_AREA_RATIO", "0.12")) ) SURFACE_DETECTOR_EXPAND_RATIO: float = field( default_factory=lambda: float(os.getenv("SURFACE_DETECTOR_EXPAND_RATIO", "0.04")) ) SURFACE_DETECTOR_IMAGE_SIZE: int = field( default_factory=lambda: int(os.getenv("SURFACE_DETECTOR_IMAGE_SIZE", "640")) ) SURFACE_CLASSIFIER_PATH: str | None = field( default_factory=lambda: _resolve_optional_path( "SURFACE_CLASSIFIER_PATH", "models/steel_surface_classifier.pt", ) ) SURFACE_CLASSIFIER_STEEL_LABEL: str = field( default_factory=lambda: os.getenv("SURFACE_CLASSIFIER_STEEL_LABEL", "steel") ) SURFACE_MIN_STEEL_CONFIDENCE: float = field( default_factory=lambda: float(os.getenv("SURFACE_MIN_STEEL_CONFIDENCE", "0.55")) ) SURFACE_GRAY_DELTA: int = field( default_factory=lambda: int(os.getenv("SURFACE_GRAY_DELTA", "16")) ) SURFACE_MIN_GRAY_RATIO: float = field( default_factory=lambda: float(os.getenv("SURFACE_MIN_GRAY_RATIO", "0.72")) ) SURFACE_LOW_SAT_PIXEL_THRESHOLD: int = field( default_factory=lambda: int(os.getenv("SURFACE_LOW_SAT_PIXEL_THRESHOLD", "45")) ) SURFACE_MIN_LOW_SAT_RATIO: float = field( default_factory=lambda: float(os.getenv("SURFACE_MIN_LOW_SAT_RATIO", "0.78")) ) SURFACE_MAX_MEAN_SATURATION: float = field( default_factory=lambda: float(os.getenv("SURFACE_MAX_MEAN_SATURATION", "40")) ) SURFACE_MAX_COLORFULNESS: float = field( default_factory=lambda: float(os.getenv("SURFACE_MAX_COLORFULNESS", "24")) ) SURFACE_MAX_SKIN_RATIO: float = field( default_factory=lambda: float(os.getenv("SURFACE_MAX_SKIN_RATIO", "0.16")) ) SURFACE_MIN_TEXTURE_VARIANCE: float = field( default_factory=lambda: float(os.getenv("SURFACE_MIN_TEXTURE_VARIANCE", "80")) ) # ----------------------------- # API / UI # ----------------------------- CORS_ORIGINS: tuple[str, ...] = field(default_factory=_parse_origins) REPORT_FETCH_LIMIT: int = field( default_factory=lambda: int(os.getenv("REPORT_FETCH_LIMIT", "50")) ) MAX_UPLOAD_SIZE_MB: int = field( default_factory=lambda: int(os.getenv("MAX_UPLOAD_SIZE_MB", "8")) ) WEBSOCKET_HEARTBEAT_SECONDS: int = field( default_factory=lambda: int(os.getenv("WEBSOCKET_HEARTBEAT_SECONDS", "20")) ) ENABLE_LLM_REPORTS: bool = field( default_factory=lambda: _parse_bool("ENABLE_LLM_REPORTS", True) ) LLM_PROVIDER: str = field( default_factory=lambda: os.getenv("LLM_PROVIDER", "auto") ) OLLAMA_BASE_URL: str = field( default_factory=lambda: os.getenv("OLLAMA_BASE_URL", "http://127.0.0.1:11434") ) OLLAMA_MODEL: str = field( default_factory=lambda: os.getenv("OLLAMA_MODEL", "llama3") ) OLLAMA_TIMEOUT_SECONDS: float = field( default_factory=lambda: float(os.getenv("OLLAMA_TIMEOUT_SECONDS", "6")) ) HF_TOKEN: str | None = field(default_factory=lambda: os.getenv("HF_TOKEN")) HF_CHAT_MODEL: str = field( default_factory=lambda: os.getenv("HF_CHAT_MODEL", "meta-llama/Llama-3.1-8B-Instruct:cerebras") ) HF_ROUTER_BASE_URL: str = field( default_factory=lambda: os.getenv("HF_ROUTER_BASE_URL", "https://router.huggingface.co/v1") ) OPENROUTER_API_KEY: str | None = field(default_factory=lambda: os.getenv("OPENROUTER_API_KEY")) OPENROUTER_MODEL: str = field( default_factory=lambda: os.getenv("OPENROUTER_MODEL", "openrouter/free") ) OPENROUTER_BASE_URL: str = field( default_factory=lambda: os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1") ) # ----------------------------- # DATABASE # ----------------------------- DATABASE_URL: str | None = field(default_factory=lambda: os.getenv("DATABASE_URL")) @property def max_upload_bytes(self) -> int: return self.MAX_UPLOAD_SIZE_MB * 1024 * 1024 settings = Settings()