""" Centralized configuration for Face Intel. All settings are environment-driven via pydantic-settings so the same code runs in dev, test, and production without code changes. After the refactor, `settings` is still importable for backward compatibility with provider modules that read tuning knobs, BUT every stateful service (storage, cache, orchestrator, services) receives its dependencies through constructor injection — never by importing this module directly. """ from __future__ import annotations from pathlib import Path from typing import List from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict BASE_DIR = Path(__file__).resolve().parent.parent DATA_DIR = BASE_DIR / "data" MODELS_DIR = DATA_DIR / "models" GALLERY_DIR = DATA_DIR / "gallery" UPLOADS_DIR = DATA_DIR / "uploads" JOBS_DIR = DATA_DIR / "jobs" UI_DIR = BASE_DIR / "ui" / "static" # Ensure runtime dirs exist (idempotent) for _d in (DATA_DIR, MODELS_DIR, GALLERY_DIR, UPLOADS_DIR, JOBS_DIR): _d.mkdir(parents=True, exist_ok=True) class Settings(BaseSettings): """Environment-driven settings. All fields map to FI_* env vars.""" model_config = SettingsConfigDict( env_file=".env", env_prefix="FI_", case_sensitive=False, extra="ignore", ) # ------------------------------------------------------------------ # # Core # ------------------------------------------------------------------ # app_name: str = "Face Intel" app_version: str = "1.0.0" environment: str = "development" host: str = "0.0.0.0" port: int = 8000 debug: bool = False # ------------------------------------------------------------------ # # Provider enable flags # ------------------------------------------------------------------ # # Detection enable_haar: bool = True enable_dnn: bool = False # requires model download # Recognition enable_insightface: bool = False # requires onnxruntime + model # Reverse search enable_serpapi: bool = False # requires API key enable_social_lookup: bool = True # pure stdlib, always available # Image analysis enable_image_quality: bool = True enable_image_properties: bool = True # Metadata enable_exif: bool = True # Forensics enable_image_integrity: bool = True enable_duplicate_detector: bool = True enable_ela: bool = True enable_image_similarity: bool = True # OCR (optional — requires onnxruntime) enable_ocr: bool = False # Object detection (optional — requires onnxruntime) enable_yolov8: bool = False # QR code + barcode detection (pure OpenCV — always available) enable_qr_code: bool = True enable_barcode: bool = True # Scene classification (optional — requires onnxruntime) enable_places365: bool = False # NSFW detection (optional — requires onnxruntime) enable_nudenet: bool = False # AI image detection (optional — requires onnxruntime) enable_ai_image_detector: bool = False # Embeddings (optional — requires onnxruntime) enable_mobilenet_embed: bool = False # ------------------------------------------------------------------ # # Reverse face search (PimEyes-style indexed search) # ------------------------------------------------------------------ # enable_face_index: bool = True face_index_path: str = str(DATA_DIR / "face_index.db") face_index_top_k: int = 10 face_index_threshold: float = 0.5 face_index_max_faces: int = 100_000 face_index_crawler_user_agent: str = "FaceIntel/1.0" face_index_crawler_seed_wikipedia: bool = False face_index_crawler_seed_imdb: bool = False # ------------------------------------------------------------------ # # Detection tuning # ------------------------------------------------------------------ # dnn_confidence_threshold: float = 0.7 haar_scale_factor: float = 1.1 haar_min_neighbors: int = 5 # ------------------------------------------------------------------ # # Recognition tuning # ------------------------------------------------------------------ # insightface_model_pack: str = "buffalo_s" # small variant for low-RAM recognition_match_threshold: float = 0.5 # ------------------------------------------------------------------ # # Scraping (used by reverse-search providers) # ------------------------------------------------------------------ # scrape_timeout: int = 30 scrape_max_images: int = 50 user_agent: str = ( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" ) # ------------------------------------------------------------------ # # Reverse image search # ------------------------------------------------------------------ # reverse_search_max_results: int = 20 serpapi_key: str = "" # ------------------------------------------------------------------ # # Orchestrator # ------------------------------------------------------------------ # orchestrator_timeout_seconds: float = 90.0 orchestrator_max_concurrency: int = 8 retry_max_attempts: int = 3 retry_initial_backoff_seconds: float = 0.5 retry_max_backoff_seconds: float = 8.0 # ------------------------------------------------------------------ # # Cache # ------------------------------------------------------------------ # cache_enabled: bool = True cache_ttl_seconds: int = 3600 cache_max_entries: int = 1000 # ------------------------------------------------------------------ # # Health # ------------------------------------------------------------------ # health_check_interval_seconds: int = 60 circuit_breaker_failure_threshold: int = 5 circuit_breaker_recovery_seconds: int = 120 # ------------------------------------------------------------------ # # Storage # ------------------------------------------------------------------ # db_path: str = str(DATA_DIR / "face_intel.db") audit_log_path: str = str(DATA_DIR / "audit.log") job_retention_days: int = 7 # ------------------------------------------------------------------ # # API # ------------------------------------------------------------------ # rate_limit_per_minute: int = 30 cors_origins: List[str] = Field(default_factory=lambda: ["*"]) require_consent_header: bool = True consent_header_name: str = "X-Consent-Statement" max_image_bytes: int = 20 * 1024 * 1024 # 20 MB upload limit job_timeout_seconds: float = 300.0 # 5 min overall job cap max_request_body_bytes: int = 25 * 1024 * 1024 # 25 MB HTTP body cap # ------------------------------------------------------------------ # # Model management # ------------------------------------------------------------------ # models_dir: str = str(MODELS_DIR) models_auto_download: bool = True # auto-download on first use onnx_intra_op_threads: int = 2 # limit CPU threads for low-RAM # ------------------------------------------------------------------ # # Logging # ------------------------------------------------------------------ # log_level: str = "INFO" log_json: bool = False def make_settings(**overrides) -> Settings: """Factory used by the DI container to build a Settings instance. Tests can override individual fields without touching env vars: make_settings(environment="test", cache_enabled=False) """ if overrides: # Build a fresh instance with overrides merged base = Settings() data = base.model_dump() data.update(overrides) return Settings(**data) return Settings() # Default singleton — fine for read-only config consumption by providers. # Stateful services MUST be injected, not import this directly. settings = Settings()