88
99from __future__ import annotations
1010
11+ import json
1112import logging
1213import os
14+ import re
15+ from collections .abc import Mapping
1316from dataclasses import dataclass , field
1417from pathlib import Path
18+ from types import MappingProxyType
19+ from typing import Any
20+
21+ from .detection_constants import ALLOWED_DETECTION_MODELS
1522
1623logger = logging .getLogger (__name__ )
1724
@@ -57,6 +64,21 @@ class AppConfig:
5764 episode_cache_max_mb : int = 100
5865 """Max memory budget for the LRU cache in megabytes. 0 means count-only."""
5966
67+ detection_models_dir : str = "./models"
68+ """Directory containing approved YOLO weight files."""
69+
70+ detection_model_digests : Mapping [str , str ] = field (default_factory = lambda : MappingProxyType ({}))
71+ """Approved model identifiers mapped to reviewed SHA-256 checkpoint digests."""
72+
73+ detection_cache_max_size : int = 100
74+ """Maximum number of episode detection summaries retained in memory."""
75+
76+ detection_cache_ttl_seconds : int = 3600
77+ """Seconds before an episode detection summary expires."""
78+
79+ detection_confidence_threshold : float = 0.1
80+ """Default detection confidence when the request omits an override."""
81+
6082 vlm_judge_enabled : bool = False
6183 """Whether the VLM-as-judge router is mounted."""
6284
@@ -117,6 +139,12 @@ def load_config(env_path: Path | None = None) -> AppConfig:
117139 episode_cache_capacity = int (os .environ .get ("EPISODE_CACHE_CAPACITY" , "32" ))
118140 episode_cache_max_mb = int (os .environ .get ("EPISODE_CACHE_MAX_MB" , "100" ))
119141
142+ detection_models_dir = os .environ .get ("DETECTION_MODELS_DIR" , "./models" )
143+ detection_model_digests = _detection_model_digests_env ()
144+ detection_cache_max_size = _positive_int_env ("DETECTION_CACHE_MAX_SIZE" , 100 )
145+ detection_cache_ttl_seconds = _positive_int_env ("DETECTION_CACHE_TTL_SECONDS" , 3600 )
146+ detection_confidence_threshold = _bounded_float_env ("DETECTION_CONFIDENCE_THRESHOLD" , 0.1 )
147+
120148 vlm_judge_enabled = os .environ .get ("VLM_JUDGE_ENABLED" , "false" ).lower () == "true"
121149 vlm_judge_backend = os .environ .get ("VLM_JUDGE_BACKEND" , "echo" ).lower ()
122150 vlm_judge_model_id = os .environ .get ("VLM_JUDGE_MODEL_ID" , "Qwen/Qwen3-VL-4B-Instruct" )
@@ -139,6 +167,11 @@ def load_config(env_path: Path | None = None) -> AppConfig:
139167 cors_origins = cors_origins ,
140168 episode_cache_capacity = episode_cache_capacity ,
141169 episode_cache_max_mb = episode_cache_max_mb ,
170+ detection_models_dir = detection_models_dir ,
171+ detection_model_digests = detection_model_digests ,
172+ detection_cache_max_size = detection_cache_max_size ,
173+ detection_cache_ttl_seconds = detection_cache_ttl_seconds ,
174+ detection_confidence_threshold = detection_confidence_threshold ,
142175 vlm_judge_enabled = vlm_judge_enabled ,
143176 vlm_judge_backend = vlm_judge_backend ,
144177 vlm_judge_model_id = vlm_judge_model_id ,
@@ -151,6 +184,40 @@ def load_config(env_path: Path | None = None) -> AppConfig:
151184 )
152185
153186
187+ def _positive_int_env (name : str , default : int ) -> int :
188+ value = int (os .environ .get (name , str (default )))
189+ if value <= 0 :
190+ raise ValueError (f"{ name } must be greater than zero" )
191+ return value
192+
193+
194+ def _bounded_float_env (name : str , default : float ) -> float :
195+ value = float (os .environ .get (name , str (default )))
196+ if not 0.0 <= value <= 1.0 :
197+ raise ValueError (f"{ name } must be between 0.0 and 1.0" )
198+ return value
199+
200+
201+ def _detection_model_digests_env () -> Mapping [str , str ]:
202+ name = "DETECTION_MODEL_DIGESTS"
203+ raw_value = os .environ .get (name , "{}" )
204+ try :
205+ value : Any = json .loads (raw_value )
206+ except json .JSONDecodeError as exc :
207+ raise ValueError (f"{ name } must be a JSON object" ) from exc
208+ if not isinstance (value , dict ):
209+ raise ValueError (f"{ name } must be a JSON object" )
210+
211+ digests : dict [str , str ] = {}
212+ for model_name , digest in value .items ():
213+ if model_name not in ALLOWED_DETECTION_MODELS :
214+ raise ValueError (f"{ name } contains an unapproved model identifier" )
215+ if not isinstance (digest , str ) or re .fullmatch (r"[0-9a-fA-F]{64}" , digest ) is None :
216+ raise ValueError (f"{ name } values must be SHA-256 digests" )
217+ digests [model_name ] = digest .lower ()
218+ return MappingProxyType (digests )
219+
220+
154221def create_annotation_storage (config : AppConfig ):
155222 """
156223 Create the annotation storage adapter based on config.
0 commit comments