| 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. |
| """ |
|
|
| |
| |
| |
| 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") |
| ) |
|
|
| |
| |
| |
| 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"))) |
|
|
| |
| |
| |
| MAX_MISSING_FRAMES: int = field( |
| default_factory=lambda: int(os.getenv("MAX_MISSING_FRAMES", "5")) |
| ) |
|
|
| |
| |
| |
| 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")) |
| ) |
|
|
| |
| |
| |
| 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_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() |
|
|