Tile handling produced results that were wrong before any model quality
question arose:
- orthophoto tiles reached the model through PIL convert("RGB"), which
truncates the high byte of a 16-bit product and treats a 4-band RGB+NIR
tile's infrared channel as colour. Tiles are now read with rasterio, the
visible bands are chosen explicitly, and values are percentile-stretched
across all three bands together so hue is preserved;
- an object wider than the tile overlap was truncated by both tiles into two
boxes that barely intersect, so IoU suppression kept both: two false
positives and one missed footprint per seam building. Suppression now also
compares overlap against the smaller box, and boxes cut by an interior tile
edge are dropped in favour of the neighbouring tile's complete view;
- georeferencing fell back to an assumed EPSG:4326 when a manifest carried no
CRS, producing geometry that renders plausibly in the wrong place. QA
already refused such a tile; inference now fails closed too.
Segmentation QA scored candidates against every reference feature in the
dataset, so every building outside the inferred tiles counted as a false
negative. It now applies the same persisted tile coverage that detection QA
has always used, including the indexed ST_Intersects prefilter.
Duplicate suppression uses an STRtree instead of the O(n^2) scan, tiles are
predicted in batches of YOLO_BATCH_SIZE (a setting that existed but was never
read), and detection/segmentation runs can be queued through /run-async for a
polling background worker rather than holding an HTTP worker thread for
minutes of GPU work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
461 lines
22 KiB
Python
461 lines
22 KiB
Python
from pydantic import Field, field_validator, model_validator
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(
|
|
env_file=".env",
|
|
env_file_encoding="utf-8",
|
|
extra="ignore",
|
|
populate_by_name=True,
|
|
)
|
|
|
|
app_env: str = Field(default="development", validation_alias="GEOINTEL_ENV")
|
|
app_version: str = Field(
|
|
default="1.0.0",
|
|
validation_alias="GEOINTEL_APP_VERSION",
|
|
)
|
|
build_sha: str | None = Field(default=None, validation_alias="GEOINTEL_BUILD_SHA")
|
|
build_time: str | None = Field(default=None, validation_alias="GEOINTEL_BUILD_TIME")
|
|
api_prefix: str = Field(default="/api/v1", validation_alias="GEOINTEL_API_PREFIX")
|
|
auth_enabled: bool = Field(default=False, validation_alias="GEOINTEL_AUTH_ENABLED")
|
|
auth_username: str | None = Field(default=None, validation_alias="GEOINTEL_AUTH_USERNAME")
|
|
auth_password_hash: str | None = Field(default=None, validation_alias="GEOINTEL_AUTH_PASSWORD_HASH")
|
|
auth_session_secret: str | None = Field(default=None, validation_alias="GEOINTEL_AUTH_SESSION_SECRET")
|
|
auth_session_ttl_seconds: int = Field(
|
|
default=43_200,
|
|
ge=900,
|
|
le=604_800,
|
|
validation_alias="GEOINTEL_AUTH_SESSION_TTL_SECONDS",
|
|
)
|
|
guest_access_enabled: bool = Field(
|
|
default=True,
|
|
validation_alias="GEOINTEL_GUEST_ACCESS_ENABLED",
|
|
)
|
|
guest_display_name: str = Field(
|
|
default="Gast",
|
|
min_length=1,
|
|
max_length=64,
|
|
validation_alias="GEOINTEL_GUEST_DISPLAY_NAME",
|
|
)
|
|
guest_session_ttl_seconds: int = Field(
|
|
default=7_200,
|
|
ge=900,
|
|
le=86_400,
|
|
validation_alias="GEOINTEL_GUEST_SESSION_TTL_SECONDS",
|
|
)
|
|
database_url: str = Field(
|
|
default="postgresql+psycopg://geointel:geointel@localhost:5432/geointel?connect_timeout=1",
|
|
validation_alias="DATABASE_URL",
|
|
)
|
|
storage_root: str = Field(default="./storage", validation_alias="STORAGE_ROOT")
|
|
max_upload_mb: int = Field(default=500, validation_alias="MAX_UPLOAD_MB")
|
|
orthophoto_enabled: bool = Field(default=True, validation_alias="ORTHOPHOTO_ENABLED")
|
|
orthophoto_wms_url: str = Field(
|
|
default="https://geo.api.vlaanderen.be/OMWRGBMRVL/wms",
|
|
validation_alias="ORTHOPHOTO_WMS_URL",
|
|
)
|
|
orthophoto_wms_layer: str = Field(default="Ortho", validation_alias="ORTHOPHOTO_WMS_LAYER")
|
|
spw_orthophoto_wms_url: str = Field(
|
|
default="https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer",
|
|
validation_alias="SPW_ORTHOPHOTO_WMS_URL",
|
|
)
|
|
brussels_orthophoto_wms_url: str = Field(
|
|
default="https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows",
|
|
validation_alias="BRUSSELS_ORTHOPHOTO_WMS_URL",
|
|
)
|
|
orthophoto_resolution_m: float = Field(default=1.0, gt=0, validation_alias="ORTHOPHOTO_RESOLUTION_M")
|
|
orthophoto_min_side_m: float = Field(default=128.0, gt=0, validation_alias="ORTHOPHOTO_MIN_SIDE_M")
|
|
orthophoto_max_side_m: float = Field(default=1024.0, gt=0, validation_alias="ORTHOPHOTO_MAX_SIDE_M")
|
|
orthophoto_timeout_seconds: int = Field(default=120, ge=1, validation_alias="ORTHOPHOTO_TIMEOUT_SECONDS")
|
|
orthophoto_max_response_mb: int = Field(default=32, ge=1, validation_alias="ORTHOPHOTO_MAX_RESPONSE_MB")
|
|
orthophoto_cache_ttl_hours: int = Field(default=24, ge=0, validation_alias="ORTHOPHOTO_CACHE_TTL_HOURS")
|
|
source_catalog_probe_enabled: bool = Field(default=True, validation_alias="SOURCE_CATALOG_PROBE_ENABLED")
|
|
source_catalog_grb_wfs_url: str = Field(
|
|
default="https://geo.api.vlaanderen.be/GRB/wfs",
|
|
validation_alias="SOURCE_CATALOG_GRB_WFS_URL",
|
|
)
|
|
source_catalog_alz_release_url: str = Field(
|
|
default="https://landbouwcijfers.vlaanderen.be/open-geodata-landbouwgebruikspercelen",
|
|
validation_alias="SOURCE_CATALOG_ALZ_RELEASE_URL",
|
|
)
|
|
source_catalog_statbel_dcat_url: str = Field(
|
|
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",
|
|
)
|
|
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_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")
|
|
grb_max_pages: int = Field(default=200, ge=1, le=1000, validation_alias="GRB_MAX_PAGES")
|
|
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_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",
|
|
)
|
|
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_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",
|
|
)
|
|
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",
|
|
)
|
|
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_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")
|
|
dhmv_max_pixels: int = Field(default=12_000_000, ge=1, validation_alias="DHMV_MAX_PIXELS")
|
|
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_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")
|
|
flood_hazard_max_pixels: int = Field(default=12_000_000, ge=1, validation_alias="FLOOD_HAZARD_MAX_PIXELS")
|
|
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_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",
|
|
)
|
|
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",
|
|
)
|
|
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",
|
|
)
|
|
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_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_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",
|
|
)
|
|
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"
|
|
)
|
|
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")
|
|
yolo_model_path: str | None = Field(default=None, validation_alias="YOLO_MODEL_PATH")
|
|
yolo_model_id: str = Field(default="yolo-configured", validation_alias="YOLO_MODEL_ID")
|
|
yolo_model_display_name: str = Field(default="Configured YOLO detector", validation_alias="YOLO_MODEL_DISPLAY_NAME")
|
|
yolo_model_version: str | None = Field(default=None, validation_alias="YOLO_MODEL_VERSION")
|
|
yolo_model_classes: str = Field(default="building", validation_alias="YOLO_MODEL_CLASSES")
|
|
yolo_enforce_validation_scope: bool = Field(default=False, 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",
|
|
)
|
|
# 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")
|
|
yolo_image_size: int = Field(default=640, validation_alias="YOLO_IMAGE_SIZE")
|
|
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_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_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_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_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",
|
|
)
|
|
|
|
@field_validator("cors_origins", mode="before")
|
|
@classmethod
|
|
def parse_cors_origins(cls, value: object) -> list[str]:
|
|
if isinstance(value, str):
|
|
return [item.strip() for item in value.split(",") if item.strip()]
|
|
if isinstance(value, list):
|
|
return value
|
|
if value is None:
|
|
return ["http://localhost:5173", "http://127.0.0.1:5173"]
|
|
return [str(value)]
|
|
|
|
@field_validator("ollama_base_url")
|
|
@classmethod
|
|
def validate_ollama_base_url(cls, value: str) -> str:
|
|
normalized = value.strip().rstrip("/")
|
|
if not normalized.startswith(("http://", "https://")):
|
|
raise ValueError("OLLAMA_BASE_URL must use http or https")
|
|
return normalized
|
|
|
|
@model_validator(mode="after")
|
|
def validate_operator_auth(self) -> "Settings":
|
|
self.guest_display_name = self.guest_display_name.strip()
|
|
if not self.guest_display_name:
|
|
raise ValueError("GEOINTEL_GUEST_DISPLAY_NAME must not be blank")
|
|
if not self.auth_enabled:
|
|
return self
|
|
if not (self.auth_username or "").strip():
|
|
raise ValueError("GEOINTEL_AUTH_USERNAME is required when authentication is enabled")
|
|
if not (self.auth_password_hash or "").startswith("pbkdf2_sha256$"):
|
|
raise ValueError("GEOINTEL_AUTH_PASSWORD_HASH must be a PBKDF2-SHA256 hash")
|
|
if len(self.auth_session_secret or "") < 32:
|
|
raise ValueError("GEOINTEL_AUTH_SESSION_SECRET must contain at least 32 characters")
|
|
return self
|
|
|
|
|
|
def get_settings() -> Settings:
|
|
return Settings()
|