File size: 7,996 Bytes
bbc3fdf
 
 
 
aac350d
 
 
 
 
 
 
bbc3fdf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aac350d
bbc3fdf
 
 
 
 
 
 
9bd3ee0
bbc3fdf
9bd3ee0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7e25f7a
 
 
9bd3ee0
 
 
 
 
 
 
 
bbc3fdf
f5eeb1c
 
 
 
 
 
 
 
 
 
 
 
bbc3fdf
 
 
 
 
 
 
 
 
 
9bd3ee0
aac350d
bbc3fdf
 
9bd3ee0
bbc3fdf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aac350d
bbc3fdf
 
23d337e
 
 
 
 
9bd3ee0
23d337e
9bd3ee0
 
 
bbc3fdf
 
 
 
 
 
 
 
aac350d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbc3fdf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
"""
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()