diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 0a29b997..f1d7f810 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -51,6 +51,8 @@ class Settings(BaseSettings): validation_alias="DATABASE_URL", ) storage_root: str = Field(default="./storage", validation_alias="STORAGE_ROOT") + # Analysis consumes only artifacts under storage_root. Provisioning + # workflows that stage tiles elsewhere before ingest can opt out. allow_external_artifact_paths: bool = Field( default=False, validation_alias="GEOINTEL_ALLOW_EXTERNAL_ARTIFACT_PATHS" ) @@ -88,12 +90,35 @@ class Settings(BaseSettings): default="https://doc.statbel.be/publications/DCAT/DCAT_opendata_datasets.ttl", validation_alias="SOURCE_CATALOG_STATBEL_DCAT_URL", ) - source_catalog_statbel_max_response_mb: int = Field(default=5, ge=1, le=10, validation_alias="SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB") - source_catalog_probe_timeout_seconds: int = Field(default=10, ge=1, le=60, validation_alias="SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS") - source_catalog_probe_max_response_mb: int = Field(default=2, ge=1, le=10, validation_alias="SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB") - source_catalog_probe_cache_ttl_seconds: int = Field(default=900, ge=0, le=86_400, validation_alias="SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS") + source_catalog_statbel_max_response_mb: int = Field( + default=5, + ge=1, + le=10, + validation_alias="SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB", + ) + source_catalog_probe_timeout_seconds: int = Field( + default=10, + ge=1, + le=60, + validation_alias="SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS", + ) + source_catalog_probe_max_response_mb: int = Field( + default=2, + ge=1, + le=10, + validation_alias="SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB", + ) + source_catalog_probe_cache_ttl_seconds: int = Field( + default=900, + ge=0, + le=86_400, + validation_alias="SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS", + ) grb_enabled: bool = Field(default=True, validation_alias="GRB_ENABLED") - grb_ogc_api_url: str = Field(default="https://geo.api.vlaanderen.be/GRB/ogc/features/v1", validation_alias="GRB_OGC_API_URL") + grb_ogc_api_url: str = Field( + default="https://geo.api.vlaanderen.be/GRB/ogc/features/v1", + validation_alias="GRB_OGC_API_URL", + ) grb_min_side_m: float = Field(default=10.0, gt=0, validation_alias="GRB_MIN_SIDE_M") grb_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="GRB_MAX_SIDE_M") grb_page_size: int = Field(default=1000, ge=1, le=1000, validation_alias="GRB_PAGE_SIZE") @@ -101,28 +126,99 @@ class Settings(BaseSettings): grb_max_features: int = Field(default=150_000, ge=1, validation_alias="GRB_MAX_FEATURES") grb_timeout_seconds: int = Field(default=180, ge=1, le=600, validation_alias="GRB_TIMEOUT_SECONDS") grb_max_response_mb: int = Field(default=20, ge=1, le=100, validation_alias="GRB_MAX_RESPONSE_MB") - grb_max_total_response_mb: int = Field(default=256, ge=1, le=2048, validation_alias="GRB_MAX_TOTAL_RESPONSE_MB") + grb_max_total_response_mb: int = Field( + default=256, + ge=1, + le=2048, + validation_alias="GRB_MAX_TOTAL_RESPONSE_MB", + ) grb_cache_ttl_hours: int = Field(default=24, ge=0, le=8760, validation_alias="GRB_CACHE_TTL_HOURS") official_vector_enabled: bool = Field(default=True, validation_alias="OFFICIAL_VECTOR_ENABLED") - bwk_wfs_url: str = Field(default="https://geo.api.vlaanderen.be/BWK/wfs", validation_alias="BWK_WFS_URL") - dov_soil_wfs_url: str = Field(default="https://www.dov.vlaanderen.be/geoserver/wfs", validation_alias="DOV_SOIL_WFS_URL") - official_vector_min_side_m: float = Field(default=10.0, gt=0, validation_alias="OFFICIAL_VECTOR_MIN_SIDE_M") - official_vector_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="OFFICIAL_VECTOR_MAX_SIDE_M") - official_vector_page_size: int = Field(default=1000, ge=1, le=2000, validation_alias="OFFICIAL_VECTOR_PAGE_SIZE") - official_vector_max_pages: int = Field(default=200, ge=1, le=1000, validation_alias="OFFICIAL_VECTOR_MAX_PAGES") - official_vector_max_features: int = Field(default=100_000, ge=1, validation_alias="OFFICIAL_VECTOR_MAX_FEATURES") - official_vector_timeout_seconds: int = Field(default=180, ge=1, le=600, validation_alias="OFFICIAL_VECTOR_TIMEOUT_SECONDS") - official_vector_max_response_mb: int = Field(default=20, ge=1, le=100, validation_alias="OFFICIAL_VECTOR_MAX_RESPONSE_MB") - official_vector_max_total_response_mb: int = Field(default=256, ge=1, le=2048, validation_alias="OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB") - official_vector_cache_ttl_hours: int = Field(default=24, ge=0, le=8760, validation_alias="OFFICIAL_VECTOR_CACHE_TTL_HOURS") + bwk_wfs_url: str = Field( + default="https://geo.api.vlaanderen.be/BWK/wfs", + validation_alias="BWK_WFS_URL", + ) + dov_soil_wfs_url: str = Field( + default="https://www.dov.vlaanderen.be/geoserver/wfs", + validation_alias="DOV_SOIL_WFS_URL", + ) + official_vector_min_side_m: float = Field( + default=10.0, + gt=0, + validation_alias="OFFICIAL_VECTOR_MIN_SIDE_M", + ) + official_vector_max_side_m: float = Field( + default=20_000.0, + gt=0, + validation_alias="OFFICIAL_VECTOR_MAX_SIDE_M", + ) + official_vector_page_size: int = Field( + default=1000, + ge=1, + le=2000, + validation_alias="OFFICIAL_VECTOR_PAGE_SIZE", + ) + official_vector_max_pages: int = Field( + default=200, + ge=1, + le=1000, + validation_alias="OFFICIAL_VECTOR_MAX_PAGES", + ) + official_vector_max_features: int = Field( + default=100_000, + ge=1, + validation_alias="OFFICIAL_VECTOR_MAX_FEATURES", + ) + official_vector_timeout_seconds: int = Field( + default=180, + ge=1, + le=600, + validation_alias="OFFICIAL_VECTOR_TIMEOUT_SECONDS", + ) + official_vector_max_response_mb: int = Field( + default=20, + ge=1, + le=100, + validation_alias="OFFICIAL_VECTOR_MAX_RESPONSE_MB", + ) + official_vector_max_total_response_mb: int = Field( + default=256, + ge=1, + le=2048, + validation_alias="OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB", + ) + official_vector_cache_ttl_hours: int = Field( + default=24, + ge=0, + le=8760, + validation_alias="OFFICIAL_VECTOR_CACHE_TTL_HOURS", + ) spw_picc_enabled: bool = Field(default=True, validation_alias="SPW_PICC_ENABLED") - spw_picc_mapserver_url: str = Field(default="https://geoservices.wallonie.be/arcgis/rest/services/TOPOGRAPHIE/PICC_VDIFF/MapServer", validation_alias="SPW_PICC_MAPSERVER_URL") + spw_picc_mapserver_url: str = Field( + default=( + "https://geoservices.wallonie.be/arcgis/rest/services/" + "TOPOGRAPHIE/PICC_VDIFF/MapServer" + ), + validation_alias="SPW_PICC_MAPSERVER_URL", + ) spw_flood_hazard_enabled: bool = Field(default=True, validation_alias="SPW_FLOOD_HAZARD_ENABLED") - spw_flood_hazard_mapserver_url: str = Field(default="https://geoservices.wallonie.be/arcgis/rest/services/EAU/ALEA_INOND/MapServer", validation_alias="SPW_FLOOD_HAZARD_MAPSERVER_URL") + spw_flood_hazard_mapserver_url: str = Field( + default=( + "https://geoservices.wallonie.be/arcgis/rest/services/" + "EAU/ALEA_INOND/MapServer" + ), + validation_alias="SPW_FLOOD_HAZARD_MAPSERVER_URL", + ) urbis_enabled: bool = Field(default=True, validation_alias="URBIS_ENABLED") - urbis_wfs_url: str = Field(default="https://geoservices-vector.irisnet.be/geoserver/urbisvector/ows", validation_alias="URBIS_WFS_URL") + urbis_wfs_url: str = Field( + default="https://geoservices-vector.irisnet.be/geoserver/urbisvector/ows", + validation_alias="URBIS_WFS_URL", + ) dhmv_enabled: bool = Field(default=True, validation_alias="DHMV_ENABLED") - dhmv_wcs_url: str = Field(default="https://geo.api.vlaanderen.be/DHMV/wcs", validation_alias="DHMV_WCS_URL") + dhmv_wcs_url: str = Field( + default="https://geo.api.vlaanderen.be/DHMV/wcs", + validation_alias="DHMV_WCS_URL", + ) dhmv_resolution_m: float = Field(default=5.0, ge=1.0, le=10.0, validation_alias="DHMV_RESOLUTION_M") dhmv_min_side_m: float = Field(default=10.0, gt=0, validation_alias="DHMV_MIN_SIDE_M") dhmv_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="DHMV_MAX_SIDE_M") @@ -130,7 +226,10 @@ class Settings(BaseSettings): dhmv_timeout_seconds: int = Field(default=300, ge=1, validation_alias="DHMV_TIMEOUT_SECONDS") dhmv_max_response_mb: int = Field(default=160, ge=1, validation_alias="DHMV_MAX_RESPONSE_MB") flood_hazard_enabled: bool = Field(default=True, validation_alias="FLOOD_HAZARD_ENABLED") - flood_hazard_wcs_url: str = Field(default="https://geoservice.waterinfo.be/OGRK/wcs", validation_alias="FLOOD_HAZARD_WCS_URL") + flood_hazard_wcs_url: str = Field( + default="https://geoservice.waterinfo.be/OGRK/wcs", + validation_alias="FLOOD_HAZARD_WCS_URL", + ) flood_hazard_resolution_m: float = Field(default=5.0, ge=2.0, le=20.0, validation_alias="FLOOD_HAZARD_RESOLUTION_M") flood_hazard_min_side_m: float = Field(default=10.0, gt=0, validation_alias="FLOOD_HAZARD_MIN_SIDE_M") flood_hazard_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="FLOOD_HAZARD_MAX_SIDE_M") @@ -138,49 +237,138 @@ class Settings(BaseSettings): flood_hazard_timeout_seconds: int = Field(default=300, ge=1, validation_alias="FLOOD_HAZARD_TIMEOUT_SECONDS") flood_hazard_max_response_mb: int = Field(default=160, ge=1, validation_alias="FLOOD_HAZARD_MAX_RESPONSE_MB") bathymetry_profiles_enabled: bool = Field(default=True, validation_alias="BATHYMETRY_PROFILES_ENABLED") - bathymetry_profiles_layer_url: str = Field(default="https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/0", validation_alias="BATHYMETRY_PROFILES_LAYER_URL") - bathymetry_watercourse_layer_url: str = Field(default="https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/1", validation_alias="BATHYMETRY_WATERCOURSE_LAYER_URL") - bathymetry_profiles_page_size: int = Field(default=1000, ge=1, le=2000, validation_alias="BATHYMETRY_PROFILES_PAGE_SIZE") - bathymetry_profiles_max_features: int = Field(default=50_000, ge=1, le=250_000, validation_alias="BATHYMETRY_PROFILES_MAX_FEATURES") - bathymetry_profiles_max_pages: int = Field(default=200, ge=1, le=5_000, validation_alias="BATHYMETRY_PROFILES_MAX_PAGES") - bathymetry_profiles_timeout_seconds: int = Field(default=120, ge=1, le=600, validation_alias="BATHYMETRY_PROFILES_TIMEOUT_SECONDS") - bathymetry_profiles_max_response_mb: int = Field(default=32, ge=1, le=256, validation_alias="BATHYMETRY_PROFILES_MAX_RESPONSE_MB") - bathymetry_raster_max_pixels: int = Field(default=30_000_000, ge=1, validation_alias="BATHYMETRY_RASTER_MAX_PIXELS") + bathymetry_profiles_layer_url: str = Field( + default="https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/0", + validation_alias="BATHYMETRY_PROFILES_LAYER_URL", + ) + bathymetry_watercourse_layer_url: str = Field( + default="https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/1", + validation_alias="BATHYMETRY_WATERCOURSE_LAYER_URL", + ) + bathymetry_profiles_page_size: int = Field( + default=1000, + ge=1, + le=2000, + validation_alias="BATHYMETRY_PROFILES_PAGE_SIZE", + ) + bathymetry_profiles_max_features: int = Field( + default=50_000, + ge=1, + le=250_000, + validation_alias="BATHYMETRY_PROFILES_MAX_FEATURES", + ) + bathymetry_profiles_max_pages: int = Field( + default=200, + ge=1, + le=5_000, + validation_alias="BATHYMETRY_PROFILES_MAX_PAGES", + ) + bathymetry_profiles_timeout_seconds: int = Field( + default=120, + ge=1, + le=600, + validation_alias="BATHYMETRY_PROFILES_TIMEOUT_SECONDS", + ) + bathymetry_profiles_max_response_mb: int = Field( + default=32, + ge=1, + le=256, + validation_alias="BATHYMETRY_PROFILES_MAX_RESPONSE_MB", + ) + bathymetry_raster_max_pixels: int = Field( + default=30_000_000, + ge=1, + validation_alias="BATHYMETRY_RASTER_MAX_PIXELS", + ) mdk_bathymetry_probe_enabled: bool = Field(default=True, validation_alias="MDK_BATHYMETRY_PROBE_ENABLED") - mdk_bathymetry_wcs_url: str = Field(default="https://bathy.agentschapmdk.be/spatialfusionserver/services/ows/wcs/EL_wcs", validation_alias="MDK_BATHYMETRY_WCS_URL") - mdk_bathymetry_probe_timeout_seconds: int = Field(default=20, ge=1, le=120, validation_alias="MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS") - mdk_bathymetry_probe_max_response_mb: int = Field(default=4, ge=1, le=16, validation_alias="MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB") + mdk_bathymetry_wcs_url: str = Field( + default="https://bathy.agentschapmdk.be/spatialfusionserver/services/ows/wcs/EL_wcs", + validation_alias="MDK_BATHYMETRY_WCS_URL", + ) + mdk_bathymetry_probe_timeout_seconds: int = Field( + default=20, + ge=1, + le=120, + validation_alias="MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS", + ) + mdk_bathymetry_probe_max_response_mb: int = Field( + default=4, + ge=1, + le=16, + validation_alias="MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB", + ) thematic_raster_enabled: bool = Field(default=True, validation_alias="THEMATIC_RASTER_ENABLED") - thematic_raster_wcs_url: str = Field(default="https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs", validation_alias="THEMATIC_RASTER_WCS_URL") - mdk_bathymetry_acquisition_enabled: bool = Field(default=False, validation_alias="MDK_BATHYMETRY_ACQUISITION_ENABLED") + thematic_raster_wcs_url: str = Field( + default="https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs", + validation_alias="THEMATIC_RASTER_WCS_URL", + ) + mdk_bathymetry_acquisition_enabled: bool = Field( + default=False, + validation_alias="MDK_BATHYMETRY_ACQUISITION_ENABLED", + ) mdk_bathymetry_coverage_id: str | None = Field(default=None, validation_alias="MDK_BATHYMETRY_COVERAGE_ID") mdk_bathymetry_request_crs: str = Field(default="EPSG:4326", validation_alias="MDK_BATHYMETRY_REQUEST_CRS") - mdk_bathymetry_max_bbox_deg2: float = Field(default=0.25, gt=0, validation_alias="MDK_BATHYMETRY_MAX_BBOX_DEG2") - mdk_bathymetry_acquisition_timeout_seconds: int = Field(default=120, ge=1, validation_alias="MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS") - mdk_bathymetry_acquisition_max_response_mb: int = Field(default=160, ge=1, validation_alias="MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB") + mdk_bathymetry_max_bbox_deg2: float = Field( + default=0.25, + gt=0, + validation_alias="MDK_BATHYMETRY_MAX_BBOX_DEG2", + ) + mdk_bathymetry_acquisition_timeout_seconds: int = Field( + default=120, + ge=1, + validation_alias="MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS", + ) + mdk_bathymetry_acquisition_max_response_mb: int = Field( + default=160, + ge=1, + validation_alias="MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB", + ) thematic_raster_min_side_m: float = Field(default=100.0, gt=0, validation_alias="THEMATIC_RASTER_MIN_SIDE_M") thematic_raster_max_side_m: float = Field(default=60_000.0, gt=0, validation_alias="THEMATIC_RASTER_MAX_SIDE_M") thematic_raster_max_pixels: int = Field(default=30_000_000, ge=1, validation_alias="THEMATIC_RASTER_MAX_PIXELS") thematic_raster_timeout_seconds: int = Field(default=300, ge=1, validation_alias="THEMATIC_RASTER_TIMEOUT_SECONDS") thematic_raster_max_response_mb: int = Field(default=160, ge=1, validation_alias="THEMATIC_RASTER_MAX_RESPONSE_MB") walous_enabled: bool = Field(default=True, validation_alias="WALOUS_ENABLED") - walous_source_dir: str = Field(default="/app/storage/source-cache/walous", validation_alias="WALOUS_SOURCE_DIR") - walous_analysis_resolution_m: float = Field(default=10.0, ge=1.0, le=100.0, validation_alias="WALOUS_ANALYSIS_RESOLUTION_M") + walous_source_dir: str = Field( + default="/app/storage/source-cache/walous", + validation_alias="WALOUS_SOURCE_DIR", + ) + walous_analysis_resolution_m: float = Field( + default=10.0, + ge=1.0, + le=100.0, + validation_alias="WALOUS_ANALYSIS_RESOLUTION_M", + ) walous_max_side_m: float = Field(default=60_000.0, gt=0, validation_alias="WALOUS_MAX_SIDE_M") walous_max_pixels: int = Field(default=36_000_000, ge=1, validation_alias="WALOUS_MAX_PIXELS") spw_terrain_enabled: bool = Field(default=True, validation_alias="SPW_TERRAIN_ENABLED") - spw_terrain_source_dir: str = Field(default="/app/storage/source-cache/spw-terrain", validation_alias="SPW_TERRAIN_SOURCE_DIR") - spw_terrain_analysis_resolution_m: float = Field(default=5.0, ge=1.0, le=10.0, validation_alias="SPW_TERRAIN_ANALYSIS_RESOLUTION_M") + spw_terrain_source_dir: str = Field( + default="/app/storage/source-cache/spw-terrain", + validation_alias="SPW_TERRAIN_SOURCE_DIR", + ) + spw_terrain_analysis_resolution_m: float = Field( + default=5.0, + ge=1.0, + le=10.0, + validation_alias="SPW_TERRAIN_ANALYSIS_RESOLUTION_M", + ) spw_terrain_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="SPW_TERRAIN_MAX_SIDE_M") spw_terrain_max_pixels: int = Field(default=12_000_000, ge=1, validation_alias="SPW_TERRAIN_MAX_PIXELS") redis_url: str | None = Field(default=None, validation_alias="REDIS_URL") log_level: str = Field(default="INFO", validation_alias="GEOINTEL_LOG_LEVEL") sql_log_level: str = Field(default="WARNING", validation_alias="GEOINTEL_SQL_LOG_LEVEL") - reconcile_interrupted_runs_on_startup: bool = Field(default=False, validation_alias="GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP") + reconcile_interrupted_runs_on_startup: bool = Field( + default=False, + validation_alias="GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP", + ) aoi_worker_enabled: bool = Field(default=False, validation_alias="GEOINTEL_AOI_WORKER_ENABLED") aoi_worker_poll_seconds: float = Field(default=2.0, ge=0.5, le=60.0, validation_alias="GEOINTEL_AOI_WORKER_POLL_SECONDS") + # Executes queued detection.run / segmentation.run jobs so tiled GPU + # inference never blocks an HTTP request. analysis_worker_enabled: bool = Field(default=False, validation_alias="GEOINTEL_ANALYSIS_WORKER_ENABLED") - analysis_worker_poll_seconds: float = Field(default=2.0, ge=0.5, le=60.0, validation_alias="GEOINTEL_ANALYSIS_WORKER_POLL_SECONDS") + analysis_worker_poll_seconds: float = Field( + default=2.0, ge=0.5, le=60.0, validation_alias="GEOINTEL_ANALYSIS_WORKER_POLL_SECONDS" + ) database_statement_timeout_ms: int = Field(default=5_000, validation_alias="DATABASE_STATEMENT_TIMEOUT_MS") yolo_enabled: bool = Field(default=False, validation_alias="YOLO_ENABLED") yolo_models_dir: str = Field(default="/app/models", validation_alias="YOLO_MODELS_DIR") @@ -193,8 +381,17 @@ class Settings(BaseSettings): # deployments may opt out explicitly, but production validation below # forbids disabling this guard when YOLO is enabled. yolo_enforce_validation_scope: bool = Field(default=True, validation_alias="YOLO_ENFORCE_VALIDATION_SCOPE") - yolo_validation_scope_manifest_path: str | None = Field(default=None, validation_alias="YOLO_VALIDATION_SCOPE_MANIFEST_PATH") - yolo_validation_scope_manifest_sha256: str | None = Field(default=None, validation_alias="YOLO_VALIDATION_SCOPE_MANIFEST_SHA256") + yolo_validation_scope_manifest_path: str | None = Field( + default=None, + validation_alias="YOLO_VALIDATION_SCOPE_MANIFEST_PATH", + ) + yolo_validation_scope_manifest_sha256: str | None = Field( + default=None, + validation_alias="YOLO_VALIDATION_SCOPE_MANIFEST_SHA256", + ) + # Deprecated compatibility field. Mutable Area names are never an + # inference authorization boundary; deployments must use the immutable + # checksum-bound scope manifest above. yolo_validated_area_names: str = Field(default="Mol,Kempen", validation_alias="YOLO_VALIDATED_AREA_NAMES") yolo_device: str = Field(default="cpu", validation_alias="YOLO_DEVICE") yolo_require_cuda: bool = Field(default=False, validation_alias="YOLO_REQUIRE_CUDA") @@ -202,29 +399,57 @@ class Settings(BaseSettings): yolo_max_tiles: int = Field(default=100, validation_alias="YOLO_MAX_TILES") yolo_max_detections: int = Field(default=1000, validation_alias="YOLO_MAX_DETECTIONS") yolo_duplicate_iou_threshold: float = Field(default=0.5, ge=0.0, le=1.0, validation_alias="YOLO_DUPLICATE_IOU_THRESHOLD") - yolo_suppress_tile_edge_detections: bool = Field(default=True, validation_alias="YOLO_SUPPRESS_TILE_EDGE_DETECTIONS") - yolo_containment_nms_threshold: float = Field(default=0.85, ge=0.0, le=1.0, validation_alias="YOLO_CONTAINMENT_NMS_THRESHOLD") + yolo_suppress_tile_edge_detections: bool = Field( + default=True, validation_alias="YOLO_SUPPRESS_TILE_EDGE_DETECTIONS" + ) + # Intersection over the smaller box. The candidate evaluation freezes this + # during calibration; serving a promoted model at a different value means + # the runtime suppresses detections the gate counted. + yolo_containment_nms_threshold: float = Field( + default=0.85, ge=0.0, le=1.0, validation_alias="YOLO_CONTAINMENT_NMS_THRESHOLD" + ) yolo_batch_size: int = Field(default=1, validation_alias="YOLO_BATCH_SIZE") yolo_seg_enabled: bool = Field(default=False, validation_alias="YOLO_SEG_ENABLED") yolo_seg_model_path: str | None = Field(default=None, validation_alias="YOLO_SEG_MODEL_PATH") yolo_seg_model_id: str = Field(default="yolo-seg-configured", validation_alias="YOLO_SEG_MODEL_ID") - yolo_seg_model_display_name: str = Field(default="Configured YOLO segmentation", validation_alias="YOLO_SEG_MODEL_DISPLAY_NAME") + yolo_seg_model_display_name: str = Field( + default="Configured YOLO segmentation", + validation_alias="YOLO_SEG_MODEL_DISPLAY_NAME", + ) yolo_seg_model_version: str | None = Field(default=None, validation_alias="YOLO_SEG_MODEL_VERSION") sam_enabled: bool = Field(default=False, validation_alias="SAM_ENABLED") sam_model_path: str | None = Field(default=None, validation_alias="SAM_MODEL_PATH") sam_model_id: str = Field(default="sam-configured", validation_alias="SAM_MODEL_ID") - sam_model_display_name: str = Field(default="Configured SAM segmentation", validation_alias="SAM_MODEL_DISPLAY_NAME") + sam_model_display_name: str = Field( + default="Configured SAM segmentation", + validation_alias="SAM_MODEL_DISPLAY_NAME", + ) sam_model_version: str | None = Field(default=None, validation_alias="SAM_MODEL_VERSION") segmentation_max_masks_per_tile: int = Field(default=300, ge=1, validation_alias="SEGMENTATION_MAX_MASKS_PER_TILE") - segmentation_containment_nms_threshold: float = Field(default=0.85, ge=0.0, le=1.0, validation_alias="SEGMENTATION_CONTAINMENT_NMS_THRESHOLD") - segmentation_duplicate_iou_threshold: float = Field(default=0.5, ge=0.0, le=1.0, validation_alias="SEGMENTATION_DUPLICATE_IOU_THRESHOLD") + # Masks and boxes overlap differently, so segmentation carries its own + # containment value rather than borrowing the detector's. + segmentation_containment_nms_threshold: float = Field( + default=0.85, + ge=0.0, + le=1.0, + validation_alias="SEGMENTATION_CONTAINMENT_NMS_THRESHOLD", + ) + segmentation_duplicate_iou_threshold: float = Field( + default=0.5, + ge=0.0, + le=1.0, + validation_alias="SEGMENTATION_DUPLICATE_IOU_THRESHOLD", + ) ollama_enabled: bool = Field(default=False, validation_alias="OLLAMA_ENABLED") ollama_base_url: str = Field(default="http://127.0.0.1:11434", validation_alias="OLLAMA_BASE_URL") ollama_default_model: str = Field(default="qwen3.5:9b", validation_alias="OLLAMA_DEFAULT_MODEL") ollama_timeout_seconds: int = Field(default=120, ge=5, le=600, validation_alias="OLLAMA_TIMEOUT_SECONDS") ollama_max_output_tokens: int = Field(default=1_200, ge=100, le=4_000, validation_alias="OLLAMA_MAX_OUTPUT_TOKENS") ollama_context_tokens: int = Field(default=16_384, ge=4_096, le=131_072, validation_alias="OLLAMA_CONTEXT_TOKENS") - cors_origins: list[str] | str = Field(default=["http://localhost:5173", "http://127.0.0.1:5173"], validation_alias="CORS_ORIGINS") + cors_origins: list[str] | str = Field( + default=["http://localhost:5173", "http://127.0.0.1:5173"], + validation_alias="CORS_ORIGINS", + ) @field_validator("cors_origins", mode="before") @classmethod @@ -261,18 +486,19 @@ class Settings(BaseSettings): production = self.app_env.strip().lower() in {"prod", "production"} if production and self.yolo_enabled: if not self.yolo_enforce_validation_scope: - raise ValueError( - "Production YOLO requires GEOINTEL_YOLO_ENFORCE_VALIDATION_SCOPE=true" - ) + raise ValueError("Production YOLO requires YOLO_ENFORCE_VALIDATION_SCOPE=true") if not (self.yolo_validation_scope_manifest_path or "").strip(): - raise ValueError( - "Production YOLO requires GEOINTEL_YOLO_VALIDATION_SCOPE_MANIFEST_PATH" - ) + raise ValueError("Production YOLO requires YOLO_VALIDATION_SCOPE_MANIFEST_PATH") manifest_hash = (self.yolo_validation_scope_manifest_sha256 or "").strip().lower() if len(manifest_hash) != 64 or any(ch not in "0123456789abcdef" for ch in manifest_hash): raise ValueError( - "Production YOLO requires a 64-character SHA-256 validation scope hash" + "Production YOLO requires YOLO_VALIDATION_SCOPE_MANIFEST_SHA256 " + "as a 64-character SHA-256" ) + if not self.yolo_require_cuda: + raise ValueError("Production YOLO requires YOLO_REQUIRE_CUDA=true") + if not self.yolo_device.strip().lower().startswith("cuda"): + raise ValueError("Production YOLO requires YOLO_DEVICE to select a CUDA device") return self