Add local model asset catalog
This commit is contained in:
@@ -6,6 +6,7 @@ STORAGE_ROOT=./storage
|
|||||||
MAX_UPLOAD_MB=500
|
MAX_UPLOAD_MB=500
|
||||||
CORS_ORIGINS=http://localhost:1202,http://127.0.0.1:1202
|
CORS_ORIGINS=http://localhost:1202,http://127.0.0.1:1202
|
||||||
YOLO_ENABLED=false
|
YOLO_ENABLED=false
|
||||||
|
YOLO_MODELS_DIR=/app/models
|
||||||
YOLO_MODEL_PATH=
|
YOLO_MODEL_PATH=
|
||||||
YOLO_MODEL_ID=yolo-configured
|
YOLO_MODEL_ID=yolo-configured
|
||||||
YOLO_MODEL_DISPLAY_NAME=Configured YOLO detector
|
YOLO_MODEL_DISPLAY_NAME=Configured YOLO detector
|
||||||
|
|||||||
@@ -7,6 +7,15 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Sprint 118 Local model and reference catalog clarity (2026-07-06)
|
||||||
|
|
||||||
|
- Added a read-only local model asset catalog endpoint at `GET /api/v1/detection/model-assets`.
|
||||||
|
- Added `YOLO_MODELS_DIR` so Docker/Unraid runtimes can expose mounted model files as selectable assets without downloading weights.
|
||||||
|
- Detection runs and YOLO preflight can now accept `model_asset_id` for `yolo-configured`, with backend-side resolution to a cataloged local file.
|
||||||
|
- Detection Lab now shows a local model asset picker with active-file, size and checksum context.
|
||||||
|
- Provider Capabilities now explicitly labels GRB/OSM/manual/fixture as reference-data source capabilities, not AI model choices.
|
||||||
|
- Added regression coverage for the backend model asset catalog and frontend model asset wiring.
|
||||||
|
|
||||||
## Sprint 117 Safe local YOLO model activation (2026-07-06)
|
## Sprint 117 Safe local YOLO model activation (2026-07-06)
|
||||||
|
|
||||||
- Added `scripts/configure_yolo_model.py` to configure an existing local YOLO model into the Unraid/Tower `.env` file without downloading weights, loading a model or running inference.
|
- Added `scripts/configure_yolo_model.py` to configure an existing local YOLO model into the Unraid/Tower `.env` file without downloading weights, loading a model or running inference.
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ bash scripts/live_migration_smoke.sh
|
|||||||
- detection service boundary for creating jobs, analysis runs and dependency-aware unavailable responses.
|
- detection service boundary for creating jobs, analysis runs and dependency-aware unavailable responses.
|
||||||
- Added detection endpoints:
|
- Added detection endpoints:
|
||||||
- `GET /api/v1/detection/models`
|
- `GET /api/v1/detection/models`
|
||||||
|
- `GET /api/v1/detection/model-assets`
|
||||||
- `POST /api/v1/detection/run`
|
- `POST /api/v1/detection/run`
|
||||||
- `GET /api/v1/detection/runs/{analysis_run_id}`
|
- `GET /api/v1/detection/runs/{analysis_run_id}`
|
||||||
- `GET /api/v1/detection/runs/{analysis_run_id}/detections`
|
- `GET /api/v1/detection/runs/{analysis_run_id}/detections`
|
||||||
@@ -239,6 +240,7 @@ Configured YOLO requires:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
YOLO_ENABLED=true
|
YOLO_ENABLED=true
|
||||||
|
YOLO_MODELS_DIR=/absolute/path/to/models
|
||||||
YOLO_MODEL_PATH=/absolute/path/to/local-model.pt
|
YOLO_MODEL_PATH=/absolute/path/to/local-model.pt
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -260,6 +262,7 @@ directory, mounted as `/app/models` by default:
|
|||||||
```bash
|
```bash
|
||||||
GEOINTEL_MODELS_PATH=/mnt/user/appdata/geointel/models
|
GEOINTEL_MODELS_PATH=/mnt/user/appdata/geointel/models
|
||||||
YOLO_ENABLED=true
|
YOLO_ENABLED=true
|
||||||
|
YOLO_MODELS_DIR=/app/models
|
||||||
YOLO_MODEL_PATH=/app/models/local-model.pt
|
YOLO_MODEL_PATH=/app/models/local-model.pt
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -275,6 +278,19 @@ python scripts/configure_yolo_model.py \
|
|||||||
The smoke loads only the supplied local model file, does not run inference and
|
The smoke loads only the supplied local model file, does not run inference and
|
||||||
does not download weights.
|
does not download weights.
|
||||||
|
|
||||||
|
The backend also exposes a read-only model asset catalog for the mounted model
|
||||||
|
directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:1202/api/v1/detection/model-assets
|
||||||
|
```
|
||||||
|
|
||||||
|
The catalog lists local `.pt`, `.onnx` and `.engine` files with size, SHA-256
|
||||||
|
and active-model status. Detection runs may submit `model_asset_id` with
|
||||||
|
`model_id="yolo-configured"` to use a cataloged local model for that run. The
|
||||||
|
backend resolves the ID to a file inside `YOLO_MODELS_DIR`; browser clients do
|
||||||
|
not send arbitrary model paths.
|
||||||
|
|
||||||
Configured YOLO inference uses raster tile artifacts from the existing tile
|
Configured YOLO inference uses raster tile artifacts from the existing tile
|
||||||
manifest flow. Single-band or otherwise non-RGB tile images are converted to a
|
manifest flow. Single-band or otherwise non-RGB tile images are converted to a
|
||||||
temporary RGB prediction image before inference; georeferencing still comes
|
temporary RGB prediction image before inference; georeferencing still comes
|
||||||
@@ -286,6 +302,7 @@ Optional tuning:
|
|||||||
YOLO_MODEL_ID=yolo-configured
|
YOLO_MODEL_ID=yolo-configured
|
||||||
YOLO_MODEL_DISPLAY_NAME="Configured YOLO detector"
|
YOLO_MODEL_DISPLAY_NAME="Configured YOLO detector"
|
||||||
YOLO_MODEL_VERSION=local-v1
|
YOLO_MODEL_VERSION=local-v1
|
||||||
|
YOLO_MODELS_DIR=/app/models
|
||||||
YOLO_CONFIG_DIR=/app/storage/ultralytics
|
YOLO_CONFIG_DIR=/app/storage/ultralytics
|
||||||
YOLO_DEVICE=cpu
|
YOLO_DEVICE=cpu
|
||||||
YOLO_IMAGE_SIZE=640
|
YOLO_IMAGE_SIZE=640
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from sqlalchemy.orm import Session
|
|||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.schemas import DetectionQaRequest, DetectionRunRequest
|
from app.schemas import DetectionQaRequest, DetectionRunRequest
|
||||||
from app.services.detection_service import DetectionService
|
from app.services.detection_service import DetectionService
|
||||||
|
from app.services.model_asset_catalog_service import ModelAssetCatalogService
|
||||||
from app.services.model_registry_service import ModelRegistryService
|
from app.services.model_registry_service import ModelRegistryService
|
||||||
from app.services.yolo_preflight_service import YoloPreflightService
|
from app.services.yolo_preflight_service import YoloPreflightService
|
||||||
from app.utils.response import envelope
|
from app.utils.response import envelope
|
||||||
@@ -20,12 +21,22 @@ def list_detection_models() -> dict:
|
|||||||
return envelope({"models": [model.model_dump() for model in ModelRegistryService.list_model_capabilities()]})
|
return envelope({"models": [model.model_dump() for model in ModelRegistryService.list_model_capabilities()]})
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/model-assets", response_model=dict)
|
||||||
|
def list_detection_model_assets() -> dict:
|
||||||
|
return envelope(ModelAssetCatalogService.list_assets().model_dump())
|
||||||
|
|
||||||
|
|
||||||
@router.get("/yolo/preflight", response_model=dict)
|
@router.get("/yolo/preflight", response_model=dict)
|
||||||
def get_yolo_preflight(tile_manifest_path: str | None = None, check_model_load: bool = False) -> dict:
|
def get_yolo_preflight(
|
||||||
|
tile_manifest_path: str | None = None,
|
||||||
|
check_model_load: bool = False,
|
||||||
|
model_asset_id: str | None = None,
|
||||||
|
) -> dict:
|
||||||
return envelope(
|
return envelope(
|
||||||
YoloPreflightService.run(
|
YoloPreflightService.run(
|
||||||
tile_manifest_path=tile_manifest_path,
|
tile_manifest_path=tile_manifest_path,
|
||||||
check_model_load=check_model_load,
|
check_model_load=check_model_load,
|
||||||
|
model_asset_id=model_asset_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,6 +48,7 @@ def run_detection(payload: DetectionRunRequest, db: Session = Depends(get_db)) -
|
|||||||
project_id=payload.project_id,
|
project_id=payload.project_id,
|
||||||
dataset_id=payload.dataset_id,
|
dataset_id=payload.dataset_id,
|
||||||
model_id=payload.model_id,
|
model_id=payload.model_id,
|
||||||
|
model_asset_id=payload.model_asset_id,
|
||||||
confidence_threshold=payload.confidence_threshold,
|
confidence_threshold=payload.confidence_threshold,
|
||||||
class_filter=payload.class_filter,
|
class_filter=payload.class_filter,
|
||||||
tile_manifest_path=payload.tile_manifest_path,
|
tile_manifest_path=payload.tile_manifest_path,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class Settings(BaseSettings):
|
|||||||
log_level: str = Field(default="INFO", validation_alias="GEOINTEL_LOG_LEVEL")
|
log_level: str = Field(default="INFO", validation_alias="GEOINTEL_LOG_LEVEL")
|
||||||
database_statement_timeout_ms: int = Field(default=5_000, validation_alias="DATABASE_STATEMENT_TIMEOUT_MS")
|
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_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_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_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_display_name: str = Field(default="Configured YOLO detector", validation_alias="YOLO_MODEL_DISPLAY_NAME")
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ from .detection import (
|
|||||||
DetectionRunRead,
|
DetectionRunRead,
|
||||||
DetectionRunRequest,
|
DetectionRunRequest,
|
||||||
DetectionRunResponse,
|
DetectionRunResponse,
|
||||||
|
ModelAssetListResponse,
|
||||||
|
ModelAssetRead,
|
||||||
)
|
)
|
||||||
from .segmentation import (
|
from .segmentation import (
|
||||||
SegmentationListResponse,
|
SegmentationListResponse,
|
||||||
@@ -105,6 +107,8 @@ __all__ = [
|
|||||||
"DetectionRunRead",
|
"DetectionRunRead",
|
||||||
"DetectionRunRequest",
|
"DetectionRunRequest",
|
||||||
"DetectionRunResponse",
|
"DetectionRunResponse",
|
||||||
|
"ModelAssetListResponse",
|
||||||
|
"ModelAssetRead",
|
||||||
"SegmentationListResponse",
|
"SegmentationListResponse",
|
||||||
"SegmentationModelCapability",
|
"SegmentationModelCapability",
|
||||||
"SegmentationModelsResponse",
|
"SegmentationModelsResponse",
|
||||||
|
|||||||
@@ -22,10 +22,33 @@ class DetectionModelsResponse(BaseModel):
|
|||||||
models: list[DetectionModelCapability]
|
models: list[DetectionModelCapability]
|
||||||
|
|
||||||
|
|
||||||
|
class ModelAssetRead(BaseModel):
|
||||||
|
model_asset_id: str
|
||||||
|
filename: str
|
||||||
|
display_name: str
|
||||||
|
model_path: str
|
||||||
|
suffix: str
|
||||||
|
framework: str
|
||||||
|
task_type: str
|
||||||
|
size_bytes: int
|
||||||
|
sha256: str
|
||||||
|
active: bool
|
||||||
|
status: str
|
||||||
|
limitation_message: str
|
||||||
|
will_download_models: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class ModelAssetListResponse(BaseModel):
|
||||||
|
items: list[ModelAssetRead]
|
||||||
|
total: int
|
||||||
|
model_directory: str
|
||||||
|
|
||||||
|
|
||||||
class DetectionRunRequest(BaseModel):
|
class DetectionRunRequest(BaseModel):
|
||||||
project_id: UUID
|
project_id: UUID
|
||||||
dataset_id: UUID
|
dataset_id: UUID
|
||||||
model_id: str
|
model_id: str
|
||||||
|
model_asset_id: str | None = None
|
||||||
confidence_threshold: float = Field(default=0.5, ge=0.0, le=1.0)
|
confidence_threshold: float = Field(default=0.5, ge=0.0, le=1.0)
|
||||||
class_filter: list[str] | None = None
|
class_filter: list[str] | None = None
|
||||||
tile_manifest_path: str | None = None
|
tile_manifest_path: str | None = None
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from app.core.errors import AppError
|
|||||||
from app.models import AnalysisRun, Dataset, Detection, Job, Project, VectorFeature
|
from app.models import AnalysisRun, Dataset, Detection, Job, Project, VectorFeature
|
||||||
from app.schemas.detection import DetectionListResponse, DetectionRead, DetectionRunListResponse, DetectionRunRead, DetectionRunResponse
|
from app.schemas.detection import DetectionListResponse, DetectionRead, DetectionRunListResponse, DetectionRunRead, DetectionRunResponse
|
||||||
from app.services.detection_georeferencing import pixel_bbox_to_epsg4326_polygon
|
from app.services.detection_georeferencing import pixel_bbox_to_epsg4326_polygon
|
||||||
|
from app.services.model_asset_catalog_service import ModelAssetCatalogService
|
||||||
from app.services.model_registry_service import ModelRegistryService
|
from app.services.model_registry_service import ModelRegistryService
|
||||||
from app.services.qa_service import QaService
|
from app.services.qa_service import QaService
|
||||||
from app.services.quality_service import QualityService
|
from app.services.quality_service import QualityService
|
||||||
@@ -33,6 +34,7 @@ class DetectionService:
|
|||||||
dataset_id: uuid.UUID,
|
dataset_id: uuid.UUID,
|
||||||
model_id: str,
|
model_id: str,
|
||||||
confidence_threshold: float,
|
confidence_threshold: float,
|
||||||
|
model_asset_id: str | None = None,
|
||||||
class_filter: list[str] | None = None,
|
class_filter: list[str] | None = None,
|
||||||
tile_manifest_path: str | None = None,
|
tile_manifest_path: str | None = None,
|
||||||
parameters_json: dict[str, Any] | None = None,
|
parameters_json: dict[str, Any] | None = None,
|
||||||
@@ -55,6 +57,11 @@ class DetectionService:
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
selected_model_asset = None
|
||||||
|
if model_id == resolved_settings.yolo_model_id and model_asset_id:
|
||||||
|
selected_model_asset = ModelAssetCatalogService.resolve_asset(model_asset_id, settings=resolved_settings)
|
||||||
|
resolved_settings = ModelAssetCatalogService.settings_for_asset(resolved_settings, selected_model_asset)
|
||||||
|
|
||||||
model = ModelRegistryService.get_model_capability(
|
model = ModelRegistryService.get_model_capability(
|
||||||
model_id,
|
model_id,
|
||||||
settings=resolved_settings,
|
settings=resolved_settings,
|
||||||
@@ -77,6 +84,9 @@ class DetectionService:
|
|||||||
|
|
||||||
run_parameters = {
|
run_parameters = {
|
||||||
"model_id": model.model_id,
|
"model_id": model.model_id,
|
||||||
|
"model_asset_id": selected_model_asset.model_asset_id if selected_model_asset else None,
|
||||||
|
"model_asset_path": selected_model_asset.model_path if selected_model_asset else None,
|
||||||
|
"model_asset_sha256": selected_model_asset.sha256 if selected_model_asset else None,
|
||||||
"confidence_threshold": confidence_threshold,
|
"confidence_threshold": confidence_threshold,
|
||||||
"class_filter": class_filter or [],
|
"class_filter": class_filter or [],
|
||||||
"tile_manifest_path": tile_manifest_path,
|
"tile_manifest_path": tile_manifest_path,
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from app.core.config import Settings, get_settings
|
||||||
|
from app.core.errors import AppError
|
||||||
|
from app.schemas.detection import ModelAssetListResponse, ModelAssetRead
|
||||||
|
|
||||||
|
|
||||||
|
class ModelAssetCatalogService:
|
||||||
|
SUPPORTED_SUFFIXES = {
|
||||||
|
".pt": "ultralytics/pytorch",
|
||||||
|
".onnx": "onnx",
|
||||||
|
".engine": "tensorrt",
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list_assets(settings: Settings | None = None) -> ModelAssetListResponse:
|
||||||
|
resolved_settings = settings or get_settings()
|
||||||
|
model_directory = ModelAssetCatalogService._model_directory(resolved_settings)
|
||||||
|
active_model_path = ModelAssetCatalogService._resolved_file_path(resolved_settings.yolo_model_path)
|
||||||
|
if not model_directory.exists() or not model_directory.is_dir():
|
||||||
|
return ModelAssetListResponse(items=[], total=0, model_directory=str(model_directory))
|
||||||
|
|
||||||
|
items = [
|
||||||
|
ModelAssetCatalogService._asset_from_file(path, active_model_path=active_model_path)
|
||||||
|
for path in sorted(model_directory.iterdir(), key=lambda item: item.name.lower())
|
||||||
|
if path.is_file() and path.suffix.lower() in ModelAssetCatalogService.SUPPORTED_SUFFIXES
|
||||||
|
]
|
||||||
|
return ModelAssetListResponse(items=items, total=len(items), model_directory=str(model_directory))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_asset(model_asset_id: str, settings: Settings | None = None) -> ModelAssetRead:
|
||||||
|
normalized = model_asset_id.strip()
|
||||||
|
for asset in ModelAssetCatalogService.list_assets(settings=settings).items:
|
||||||
|
if asset.model_asset_id == normalized:
|
||||||
|
return asset
|
||||||
|
raise AppError(
|
||||||
|
code="DETECTION_MODEL_ASSET_NOT_FOUND",
|
||||||
|
message="Selected local model asset was not found in the configured model directory",
|
||||||
|
details={"model_asset_id": normalized},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def settings_for_asset(settings: Settings, asset: ModelAssetRead) -> Settings:
|
||||||
|
return settings.model_copy(update={"yolo_model_path": asset.model_path})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _model_directory(settings: Settings) -> Path:
|
||||||
|
configured_directory = Path(settings.yolo_models_dir).expanduser()
|
||||||
|
if configured_directory.exists() and configured_directory.is_dir():
|
||||||
|
return configured_directory.resolve()
|
||||||
|
active_model_path = ModelAssetCatalogService._resolved_file_path(settings.yolo_model_path)
|
||||||
|
if active_model_path and active_model_path.parent.exists() and active_model_path.parent.is_dir():
|
||||||
|
return active_model_path.parent.resolve()
|
||||||
|
return configured_directory.resolve()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _asset_from_file(path: Path, *, active_model_path: Path | None) -> ModelAssetRead:
|
||||||
|
resolved_path = path.resolve()
|
||||||
|
return ModelAssetRead(
|
||||||
|
model_asset_id=ModelAssetCatalogService._asset_id(path),
|
||||||
|
filename=path.name,
|
||||||
|
display_name=path.stem,
|
||||||
|
model_path=str(resolved_path),
|
||||||
|
suffix=path.suffix.lower(),
|
||||||
|
framework=ModelAssetCatalogService.SUPPORTED_SUFFIXES[path.suffix.lower()],
|
||||||
|
task_type="object_detection",
|
||||||
|
size_bytes=path.stat().st_size,
|
||||||
|
sha256=ModelAssetCatalogService._sha256(path),
|
||||||
|
active=active_model_path == resolved_path,
|
||||||
|
status="available",
|
||||||
|
limitation_message="Local runtime model asset. GeoIntel will not download or mutate model weights.",
|
||||||
|
will_download_models=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _asset_id(path: Path) -> str:
|
||||||
|
raw = f"{path.stem}-{path.suffix.lower().lstrip('.')}"
|
||||||
|
normalized = re.sub(r"[^a-z0-9]+", "-", raw.lower()).strip("-")
|
||||||
|
return normalized or "model-asset"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sha256(path: Path) -> str:
|
||||||
|
digest = hashlib.sha256()
|
||||||
|
with path.open("rb") as handle:
|
||||||
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||||
|
digest.update(chunk)
|
||||||
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _resolved_file_path(raw_path: str | None) -> Path | None:
|
||||||
|
if not raw_path:
|
||||||
|
return None
|
||||||
|
path = Path(raw_path).expanduser()
|
||||||
|
if not path.exists() or not path.is_file():
|
||||||
|
return None
|
||||||
|
return path.resolve()
|
||||||
@@ -8,6 +8,7 @@ from typing import Any, Type
|
|||||||
from app.core.config import Settings, get_settings
|
from app.core.config import Settings, get_settings
|
||||||
from app.core.errors import AppError
|
from app.core.errors import AppError
|
||||||
from app.services.detection_service import DetectionService
|
from app.services.detection_service import DetectionService
|
||||||
|
from app.services.model_asset_catalog_service import ModelAssetCatalogService
|
||||||
from app.services.yolo_adapter import YoloDetectionAdapter
|
from app.services.yolo_adapter import YoloDetectionAdapter
|
||||||
|
|
||||||
|
|
||||||
@@ -20,10 +21,16 @@ class YoloPreflightService:
|
|||||||
yolo_adapter_class: Type[YoloDetectionAdapter] = YoloDetectionAdapter,
|
yolo_adapter_class: Type[YoloDetectionAdapter] = YoloDetectionAdapter,
|
||||||
assume_dependencies: bool = False,
|
assume_dependencies: bool = False,
|
||||||
check_model_load: bool = False,
|
check_model_load: bool = False,
|
||||||
|
model_asset_id: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
resolved_settings = settings or get_settings()
|
resolved_settings = settings or get_settings()
|
||||||
|
selected_asset = None
|
||||||
|
if model_asset_id:
|
||||||
|
selected_asset = ModelAssetCatalogService.resolve_asset(model_asset_id, settings=resolved_settings)
|
||||||
|
resolved_settings = ModelAssetCatalogService.settings_for_asset(resolved_settings, selected_asset)
|
||||||
result: dict[str, Any] = {
|
result: dict[str, Any] = {
|
||||||
"model_id": resolved_settings.yolo_model_id,
|
"model_id": resolved_settings.yolo_model_id,
|
||||||
|
"model_asset_id": selected_asset.model_asset_id if selected_asset else None,
|
||||||
"model_path": resolved_settings.yolo_model_path,
|
"model_path": resolved_settings.yolo_model_path,
|
||||||
"tile_manifest_path": tile_manifest_path,
|
"tile_manifest_path": tile_manifest_path,
|
||||||
"status": "not_configured",
|
"status": "not_configured",
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ def test_env_example_uses_runtime_env_names_read_by_backend_and_frontend() -> No
|
|||||||
|
|
||||||
assert "GEOINTEL_INSTALL_AI=false" in env_example
|
assert "GEOINTEL_INSTALL_AI=false" in env_example
|
||||||
assert "YOLO_ENABLED=false" in env_example
|
assert "YOLO_ENABLED=false" in env_example
|
||||||
|
assert "YOLO_MODELS_DIR=/app/models" in env_example
|
||||||
assert "YOLO_MODEL_PATH=" in env_example
|
assert "YOLO_MODEL_PATH=" in env_example
|
||||||
assert "YOLO_CONFIG_DIR=./storage/ultralytics" in env_example
|
assert "YOLO_CONFIG_DIR=./storage/ultralytics" in env_example
|
||||||
assert "YOLO_MAX_TILES=100" in env_example
|
assert "YOLO_MAX_TILES=100" in env_example
|
||||||
@@ -231,6 +232,8 @@ def test_unraid_deploy_passes_ai_build_arg_and_yolo_runtime_env() -> None:
|
|||||||
|
|
||||||
assert 'YOLO_ENABLED="${YOLO_ENABLED:-false}"' in run_script
|
assert 'YOLO_ENABLED="${YOLO_ENABLED:-false}"' in run_script
|
||||||
assert '-e YOLO_ENABLED="$YOLO_ENABLED"' in run_script
|
assert '-e YOLO_ENABLED="$YOLO_ENABLED"' in run_script
|
||||||
|
assert 'YOLO_MODELS_DIR="${YOLO_MODELS_DIR:-/app/models}"' in run_script
|
||||||
|
assert '-e YOLO_MODELS_DIR="$YOLO_MODELS_DIR"' in run_script
|
||||||
assert '-e YOLO_MODEL_PATH="$YOLO_MODEL_PATH"' in run_script
|
assert '-e YOLO_MODEL_PATH="$YOLO_MODEL_PATH"' in run_script
|
||||||
assert '-e YOLO_MAX_TILES="$YOLO_MAX_TILES"' in run_script
|
assert '-e YOLO_MAX_TILES="$YOLO_MAX_TILES"' in run_script
|
||||||
assert "-v \"${GEOINTEL_MODELS_PATH}:/app/models\"" in run_script
|
assert "-v \"${GEOINTEL_MODELS_PATH}:/app/models\"" in run_script
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.core.config import Settings
|
||||||
|
from app.core.errors import AppError
|
||||||
|
from app.main import app
|
||||||
|
from app.models import AnalysisRun, Dataset, Detection, Job, Project
|
||||||
|
from app.services.detection_service import DetectionService
|
||||||
|
from app.services.model_asset_catalog_service import ModelAssetCatalogService
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSession:
|
||||||
|
def __init__(self, objects=None) -> None:
|
||||||
|
self.objects = objects or {}
|
||||||
|
self.added = []
|
||||||
|
self.commits = 0
|
||||||
|
self.refreshes = []
|
||||||
|
|
||||||
|
def get(self, model, item_id):
|
||||||
|
return self.objects.get((model, item_id))
|
||||||
|
|
||||||
|
def add(self, item) -> None:
|
||||||
|
self.added.append(item)
|
||||||
|
if getattr(item, "id", None) is not None:
|
||||||
|
self.objects[(item.__class__, item.id)] = item
|
||||||
|
|
||||||
|
def commit(self) -> None:
|
||||||
|
self.commits += 1
|
||||||
|
|
||||||
|
def refresh(self, item) -> None:
|
||||||
|
self.refreshes.append(item)
|
||||||
|
|
||||||
|
|
||||||
|
class MockYoloAdapter:
|
||||||
|
def __init__(self, settings: Settings) -> None:
|
||||||
|
self.settings = settings
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def dependencies_available() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def load_model(self, model_path: Path):
|
||||||
|
return {"model_path": str(model_path)}
|
||||||
|
|
||||||
|
def predict_tile(self, model, tile_path: Path, confidence_threshold: float) -> list[dict]:
|
||||||
|
assert model["model_path"].endswith("building-detector.pt")
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"class_name": "building",
|
||||||
|
"confidence": 0.9,
|
||||||
|
"bbox": [10.0, 20.0, 30.0, 40.0],
|
||||||
|
"properties": {"adapter": "mock"},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _project_and_raster_dataset():
|
||||||
|
project_id = uuid4()
|
||||||
|
dataset_id = uuid4()
|
||||||
|
project = Project(id=project_id, name="Geel")
|
||||||
|
dataset = Dataset(
|
||||||
|
id=dataset_id,
|
||||||
|
project_id=project_id,
|
||||||
|
name="source.tif",
|
||||||
|
dataset_type="raster",
|
||||||
|
source="user_upload",
|
||||||
|
storage_path="storage/uploads/source.tif",
|
||||||
|
)
|
||||||
|
db = FakeSession(objects={(Project, project_id): project, (Dataset, dataset_id): dataset})
|
||||||
|
return db, project_id, dataset_id
|
||||||
|
|
||||||
|
|
||||||
|
def _manifest(tmp_path: Path) -> Path:
|
||||||
|
tile_path = tmp_path / "tile_0000.tif"
|
||||||
|
tile_path.write_bytes(b"tile")
|
||||||
|
manifest_path = tmp_path / "manifest.json"
|
||||||
|
manifest_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"tile_set_id": "tiles-fixture",
|
||||||
|
"count": 1,
|
||||||
|
"tiles": [
|
||||||
|
{
|
||||||
|
"path": str(tile_path),
|
||||||
|
"pixel_window": [0, 0, 100, 100],
|
||||||
|
"bounds": [4.0, 51.0, 5.0, 52.0],
|
||||||
|
"transform": [4.0, 0.01, 0.0, 52.0, 0.0, -0.01],
|
||||||
|
"index": 0,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return manifest_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_asset_catalog_lists_supported_local_model_files(tmp_path: Path) -> None:
|
||||||
|
model_file = tmp_path / "building-detector.pt"
|
||||||
|
model_file.write_bytes(b"local model")
|
||||||
|
ignored_file = tmp_path / "notes.txt"
|
||||||
|
ignored_file.write_text("ignore me", encoding="utf-8")
|
||||||
|
settings = Settings(yolo_models_dir=str(tmp_path), yolo_model_path=str(model_file), yolo_enabled=True)
|
||||||
|
|
||||||
|
response = ModelAssetCatalogService.list_assets(settings=settings)
|
||||||
|
|
||||||
|
assert response.total == 1
|
||||||
|
asset = response.items[0]
|
||||||
|
assert asset.model_asset_id == "building-detector-pt"
|
||||||
|
assert asset.filename == "building-detector.pt"
|
||||||
|
assert asset.display_name == "building-detector"
|
||||||
|
assert asset.model_path == str(model_file)
|
||||||
|
assert asset.size_bytes == len(b"local model")
|
||||||
|
assert len(asset.sha256) == 64
|
||||||
|
assert asset.active is True
|
||||||
|
assert asset.status == "available"
|
||||||
|
assert asset.will_download_models is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_asset_catalog_resolves_known_asset(tmp_path: Path) -> None:
|
||||||
|
model_file = tmp_path / "building-detector.pt"
|
||||||
|
model_file.write_bytes(b"local model")
|
||||||
|
settings = Settings(yolo_models_dir=str(tmp_path), yolo_enabled=True)
|
||||||
|
|
||||||
|
asset = ModelAssetCatalogService.resolve_asset("building-detector-pt", settings=settings)
|
||||||
|
|
||||||
|
assert asset.filename == "building-detector.pt"
|
||||||
|
assert asset.model_path == str(model_file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_asset_catalog_rejects_unknown_asset(tmp_path: Path) -> None:
|
||||||
|
settings = Settings(yolo_models_dir=str(tmp_path), yolo_enabled=True)
|
||||||
|
|
||||||
|
with pytest.raises(AppError) as exc_info:
|
||||||
|
ModelAssetCatalogService.resolve_asset("missing-model", settings=settings)
|
||||||
|
|
||||||
|
assert exc_info.value.code == "DETECTION_MODEL_ASSET_NOT_FOUND"
|
||||||
|
assert exc_info.value.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_model_assets_api_returns_canonical_envelope(monkeypatch, tmp_path: Path) -> None:
|
||||||
|
model_file = tmp_path / "building-detector.pt"
|
||||||
|
model_file.write_bytes(b"local model")
|
||||||
|
monkeypatch.setenv("YOLO_MODELS_DIR", str(tmp_path))
|
||||||
|
monkeypatch.setenv("YOLO_MODEL_PATH", str(model_file))
|
||||||
|
|
||||||
|
response = TestClient(app).get("/api/v1/detection/model-assets")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert set(payload) == {"data"}
|
||||||
|
assert payload["data"]["total"] == 1
|
||||||
|
assert payload["data"]["items"][0]["model_asset_id"] == "building-detector-pt"
|
||||||
|
assert payload["data"]["items"][0]["active"] is True
|
||||||
|
assert payload["data"]["items"][0]["will_download_models"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_detection_run_persists_selected_model_asset_parameters(tmp_path: Path) -> None:
|
||||||
|
model_file = tmp_path / "building-detector.pt"
|
||||||
|
model_file.write_bytes(b"local model")
|
||||||
|
db, project_id, dataset_id = _project_and_raster_dataset()
|
||||||
|
settings = Settings(
|
||||||
|
yolo_enabled=True,
|
||||||
|
yolo_model_path=str(tmp_path / "default.pt"),
|
||||||
|
yolo_models_dir=str(tmp_path),
|
||||||
|
yolo_max_tiles=4,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = DetectionService.run_detection(
|
||||||
|
db=db,
|
||||||
|
project_id=project_id,
|
||||||
|
dataset_id=dataset_id,
|
||||||
|
model_id="yolo-configured",
|
||||||
|
model_asset_id="building-detector-pt",
|
||||||
|
confidence_threshold=0.5,
|
||||||
|
tile_manifest_path=str(_manifest(tmp_path)),
|
||||||
|
settings=settings,
|
||||||
|
yolo_adapter_class=MockYoloAdapter,
|
||||||
|
)
|
||||||
|
|
||||||
|
jobs = [item for item in db.added if isinstance(item, Job)]
|
||||||
|
runs = [item for item in db.added if isinstance(item, AnalysisRun)]
|
||||||
|
detections = [item for item in db.added if isinstance(item, Detection)]
|
||||||
|
|
||||||
|
assert result.status == "success"
|
||||||
|
assert result.detection_count == 1
|
||||||
|
assert jobs[0].parameters_json["model_asset_id"] == "building-detector-pt"
|
||||||
|
assert jobs[0].parameters_json["model_asset_path"] == str(model_file)
|
||||||
|
assert len(jobs[0].parameters_json["model_asset_sha256"]) == 64
|
||||||
|
assert runs[0].parameters_json["model_asset_id"] == "building-detector-pt"
|
||||||
|
assert detections[0].model_name == "yolo-configured"
|
||||||
@@ -23,3 +23,29 @@ def test_detection_lab_surfaces_yolo_runtime_preflight() -> None:
|
|||||||
assert "/api/v1/detection/yolo/preflight" in api
|
assert "/api/v1/detection/yolo/preflight" in api
|
||||||
assert "interface YoloPreflightResponse" in types
|
assert "interface YoloPreflightResponse" in types
|
||||||
assert "yoloPreflight={yoloPreflight}" in app
|
assert "yoloPreflight={yoloPreflight}" in app
|
||||||
|
|
||||||
|
|
||||||
|
def test_detection_lab_surfaces_local_model_asset_selection() -> None:
|
||||||
|
lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
hook = (ROOT / "frontend" / "src" / "hooks" / "useDetectionWorkflow.ts").read_text(encoding="utf-8")
|
||||||
|
api = (ROOT / "frontend" / "src" / "services" / "api" / "detection.ts").read_text(encoding="utf-8")
|
||||||
|
types = (ROOT / "frontend" / "src" / "types.ts").read_text(encoding="utf-8")
|
||||||
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
|
provider_panel = (ROOT / "frontend" / "src" / "components" / "providers" / "ProviderPanel.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "interface ModelAssetRead" in types
|
||||||
|
assert "model_asset_id?: string | null" in types
|
||||||
|
assert "listModelAssets" in api
|
||||||
|
assert "/api/v1/detection/model-assets" in api
|
||||||
|
assert "modelAssets" in hook
|
||||||
|
assert "selectedModelAssetId" in hook
|
||||||
|
assert "model_asset_id: selectedModelAssetId || null" in hook
|
||||||
|
assert "Local model assets" in lab
|
||||||
|
assert "onSelectModelAsset" in lab
|
||||||
|
assert "modelAssets={modelAssets}" in app
|
||||||
|
assert "Official reference sources" in provider_panel
|
||||||
|
assert "not AI model choices" in provider_panel
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ def test_configure_yolo_model_dry_run_selects_single_model(tmp_path: Path) -> No
|
|||||||
assert payload["selected_container_model_path"] == "/app/models/nested/detector.pt"
|
assert payload["selected_container_model_path"] == "/app/models/nested/detector.pt"
|
||||||
assert payload["env_updates"]["GEOINTEL_INSTALL_AI"] == "true"
|
assert payload["env_updates"]["GEOINTEL_INSTALL_AI"] == "true"
|
||||||
assert payload["env_updates"]["YOLO_ENABLED"] == "true"
|
assert payload["env_updates"]["YOLO_ENABLED"] == "true"
|
||||||
|
assert payload["env_updates"]["YOLO_MODELS_DIR"] == "/app/models"
|
||||||
assert payload["env_updates"]["YOLO_MODEL_PATH"] == "/app/models/nested/detector.pt"
|
assert payload["env_updates"]["YOLO_MODEL_PATH"] == "/app/models/nested/detector.pt"
|
||||||
assert payload["will_download_models"] is False
|
assert payload["will_download_models"] is False
|
||||||
assert not (tmp_path / ".env").exists()
|
assert not (tmp_path / ".env").exists()
|
||||||
@@ -94,4 +95,5 @@ def test_configure_yolo_model_apply_updates_existing_env_file(tmp_path: Path) ->
|
|||||||
assert "GEOINTEL_FRONTEND_PORT=1202" in contents
|
assert "GEOINTEL_FRONTEND_PORT=1202" in contents
|
||||||
assert "GEOINTEL_INSTALL_AI=true" in contents
|
assert "GEOINTEL_INSTALL_AI=true" in contents
|
||||||
assert "YOLO_ENABLED=true" in contents
|
assert "YOLO_ENABLED=true" in contents
|
||||||
|
assert "YOLO_MODELS_DIR=/app/models" in contents
|
||||||
assert "YOLO_MODEL_PATH=/app/models/detector.engine" in contents
|
assert "YOLO_MODEL_PATH=/app/models/detector.engine" in contents
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ set it through `.env`, the Unraid template or `docker run -e`.
|
|||||||
AI dependencies are opt-in. Leave `GEOINTEL_INSTALL_AI=false` for the default
|
AI dependencies are opt-in. Leave `GEOINTEL_INSTALL_AI=false` for the default
|
||||||
GIS-only image. Set `GEOINTEL_INSTALL_AI=true`, mount models through
|
GIS-only image. Set `GEOINTEL_INSTALL_AI=true`, mount models through
|
||||||
`GEOINTEL_MODELS_PATH` and configure `YOLO_ENABLED=true` plus
|
`GEOINTEL_MODELS_PATH` and configure `YOLO_ENABLED=true` plus
|
||||||
`YOLO_MODEL_PATH=/app/models/<model>.pt` only when you have a local model file.
|
`YOLO_MODELS_DIR=/app/models` and `YOLO_MODEL_PATH=/app/models/<model>.pt` only
|
||||||
|
when you have a local model file.
|
||||||
The AI-enabled image installs PyTorch/Ultralytics plus the native OpenCV runtime
|
The AI-enabled image installs PyTorch/Ultralytics plus the native OpenCV runtime
|
||||||
libraries needed for Ultralytics imports; it still never downloads model weights.
|
libraries needed for Ultralytics imports; it still never downloads model weights.
|
||||||
`YOLO_CONFIG_DIR` defaults to `/app/storage/ultralytics`, a writable persistent
|
`YOLO_CONFIG_DIR` defaults to `/app/storage/ultralytics`, a writable persistent
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export STORAGE_ROOT="${STORAGE_ROOT:-${GEOINTEL_STORAGE_ROOT:-/app/storage}}"
|
|||||||
export DATABASE_URL="${DATABASE_URL:-postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:5432/${POSTGRES_DB}}"
|
export DATABASE_URL="${DATABASE_URL:-postgresql+psycopg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:5432/${POSTGRES_DB}}"
|
||||||
export CORS_ORIGINS="${GEOINTEL_CORS_ORIGINS:-${CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}}"
|
export CORS_ORIGINS="${GEOINTEL_CORS_ORIGINS:-${CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}}"
|
||||||
export MAX_UPLOAD_MB="${GEOINTEL_MAX_UPLOAD_MB:-${MAX_UPLOAD_MB:-500}}"
|
export MAX_UPLOAD_MB="${GEOINTEL_MAX_UPLOAD_MB:-${MAX_UPLOAD_MB:-500}}"
|
||||||
|
export YOLO_MODELS_DIR="${YOLO_MODELS_DIR:-/app/models}"
|
||||||
export YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-$STORAGE_ROOT/ultralytics}"
|
export YOLO_CONFIG_DIR="${YOLO_CONFIG_DIR:-$STORAGE_ROOT/ultralytics}"
|
||||||
|
|
||||||
mkdir -p "$PGDATA" "$STORAGE_ROOT" "$YOLO_CONFIG_DIR" /run/nginx /var/log/nginx
|
mkdir -p "$PGDATA" "$STORAGE_ROOT" "$YOLO_CONFIG_DIR" /run/nginx /var/log/nginx
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ GEOINTEL_MAX_UPLOAD_MB=500
|
|||||||
# Optional configured-YOLO runtime. Keep disabled unless a local model is mounted.
|
# Optional configured-YOLO runtime. Keep disabled unless a local model is mounted.
|
||||||
GEOINTEL_INSTALL_AI=false
|
GEOINTEL_INSTALL_AI=false
|
||||||
YOLO_ENABLED=false
|
YOLO_ENABLED=false
|
||||||
|
YOLO_MODELS_DIR=/app/models
|
||||||
YOLO_MODEL_PATH=
|
YOLO_MODEL_PATH=
|
||||||
YOLO_MODEL_ID=yolo-configured
|
YOLO_MODEL_ID=yolo-configured
|
||||||
YOLO_MODEL_DISPLAY_NAME=Configured YOLO detector
|
YOLO_MODEL_DISPLAY_NAME=Configured YOLO detector
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ GEOINTEL_POSTGRES_PASSWORD="${GEOINTEL_POSTGRES_PASSWORD:-geointel}"
|
|||||||
GEOINTEL_CORS_ORIGINS="${GEOINTEL_CORS_ORIGINS:-http://localhost:${GEOINTEL_FRONTEND_PORT},http://127.0.0.1:${GEOINTEL_FRONTEND_PORT},http://192.168.10.150:${GEOINTEL_FRONTEND_PORT}}"
|
GEOINTEL_CORS_ORIGINS="${GEOINTEL_CORS_ORIGINS:-http://localhost:${GEOINTEL_FRONTEND_PORT},http://127.0.0.1:${GEOINTEL_FRONTEND_PORT},http://192.168.10.150:${GEOINTEL_FRONTEND_PORT}}"
|
||||||
GEOINTEL_MAX_UPLOAD_MB="${GEOINTEL_MAX_UPLOAD_MB:-500}"
|
GEOINTEL_MAX_UPLOAD_MB="${GEOINTEL_MAX_UPLOAD_MB:-500}"
|
||||||
YOLO_ENABLED="${YOLO_ENABLED:-false}"
|
YOLO_ENABLED="${YOLO_ENABLED:-false}"
|
||||||
|
YOLO_MODELS_DIR="${YOLO_MODELS_DIR:-/app/models}"
|
||||||
YOLO_MODEL_PATH="${YOLO_MODEL_PATH:-}"
|
YOLO_MODEL_PATH="${YOLO_MODEL_PATH:-}"
|
||||||
YOLO_MODEL_ID="${YOLO_MODEL_ID:-yolo-configured}"
|
YOLO_MODEL_ID="${YOLO_MODEL_ID:-yolo-configured}"
|
||||||
YOLO_MODEL_DISPLAY_NAME="${YOLO_MODEL_DISPLAY_NAME:-Configured YOLO detector}"
|
YOLO_MODEL_DISPLAY_NAME="${YOLO_MODEL_DISPLAY_NAME:-Configured YOLO detector}"
|
||||||
@@ -80,6 +81,7 @@ docker run -d \
|
|||||||
-e GEOINTEL_CORS_ORIGINS="$GEOINTEL_CORS_ORIGINS" \
|
-e GEOINTEL_CORS_ORIGINS="$GEOINTEL_CORS_ORIGINS" \
|
||||||
-e GEOINTEL_MAX_UPLOAD_MB="$GEOINTEL_MAX_UPLOAD_MB" \
|
-e GEOINTEL_MAX_UPLOAD_MB="$GEOINTEL_MAX_UPLOAD_MB" \
|
||||||
-e YOLO_ENABLED="$YOLO_ENABLED" \
|
-e YOLO_ENABLED="$YOLO_ENABLED" \
|
||||||
|
-e YOLO_MODELS_DIR="$YOLO_MODELS_DIR" \
|
||||||
-e YOLO_MODEL_PATH="$YOLO_MODEL_PATH" \
|
-e YOLO_MODEL_PATH="$YOLO_MODEL_PATH" \
|
||||||
-e YOLO_MODEL_ID="$YOLO_MODEL_ID" \
|
-e YOLO_MODEL_ID="$YOLO_MODEL_ID" \
|
||||||
-e YOLO_MODEL_DISPLAY_NAME="$YOLO_MODEL_DISPLAY_NAME" \
|
-e YOLO_MODEL_DISPLAY_NAME="$YOLO_MODEL_DISPLAY_NAME" \
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ services:
|
|||||||
GEOINTEL_CORS_ORIGINS: ${GEOINTEL_CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}
|
GEOINTEL_CORS_ORIGINS: ${GEOINTEL_CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}
|
||||||
GEOINTEL_MAX_UPLOAD_MB: ${GEOINTEL_MAX_UPLOAD_MB:-500}
|
GEOINTEL_MAX_UPLOAD_MB: ${GEOINTEL_MAX_UPLOAD_MB:-500}
|
||||||
YOLO_ENABLED: ${YOLO_ENABLED:-false}
|
YOLO_ENABLED: ${YOLO_ENABLED:-false}
|
||||||
|
YOLO_MODELS_DIR: ${YOLO_MODELS_DIR:-/app/models}
|
||||||
YOLO_MODEL_PATH: ${YOLO_MODEL_PATH:-}
|
YOLO_MODEL_PATH: ${YOLO_MODEL_PATH:-}
|
||||||
YOLO_MODEL_ID: ${YOLO_MODEL_ID:-yolo-configured}
|
YOLO_MODEL_ID: ${YOLO_MODEL_ID:-yolo-configured}
|
||||||
YOLO_MODEL_DISPLAY_NAME: ${YOLO_MODEL_DISPLAY_NAME:-Configured YOLO detector}
|
YOLO_MODEL_DISPLAY_NAME: ${YOLO_MODEL_DISPLAY_NAME:-Configured YOLO detector}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ services:
|
|||||||
CORS_ORIGINS: ${GEOINTEL_CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}
|
CORS_ORIGINS: ${GEOINTEL_CORS_ORIGINS:-http://localhost:1202,http://127.0.0.1:1202}
|
||||||
MAX_UPLOAD_MB: ${GEOINTEL_MAX_UPLOAD_MB:-500}
|
MAX_UPLOAD_MB: ${GEOINTEL_MAX_UPLOAD_MB:-500}
|
||||||
YOLO_ENABLED: ${YOLO_ENABLED:-false}
|
YOLO_ENABLED: ${YOLO_ENABLED:-false}
|
||||||
|
YOLO_MODELS_DIR: ${YOLO_MODELS_DIR:-/app/models}
|
||||||
YOLO_MODEL_PATH: ${YOLO_MODEL_PATH:-}
|
YOLO_MODEL_PATH: ${YOLO_MODEL_PATH:-}
|
||||||
YOLO_MODEL_ID: ${YOLO_MODEL_ID:-yolo-configured}
|
YOLO_MODEL_ID: ${YOLO_MODEL_ID:-yolo-configured}
|
||||||
YOLO_MODEL_DISPLAY_NAME: ${YOLO_MODEL_DISPLAY_NAME:-Configured YOLO detector}
|
YOLO_MODEL_DISPLAY_NAME: ${YOLO_MODEL_DISPLAY_NAME:-Configured YOLO detector}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ Environment variables:
|
|||||||
|
|
||||||
- `GEOINTEL_INSTALL_AI`
|
- `GEOINTEL_INSTALL_AI`
|
||||||
- `YOLO_ENABLED`
|
- `YOLO_ENABLED`
|
||||||
|
- `YOLO_MODELS_DIR`
|
||||||
- `YOLO_MODEL_PATH`
|
- `YOLO_MODEL_PATH`
|
||||||
- `YOLO_MODEL_ID`
|
- `YOLO_MODEL_ID`
|
||||||
- `YOLO_MODEL_DISPLAY_NAME`
|
- `YOLO_MODEL_DISPLAY_NAME`
|
||||||
@@ -109,6 +110,19 @@ Environment variables:
|
|||||||
- `YOLO_MAX_TILES`
|
- `YOLO_MAX_TILES`
|
||||||
- `YOLO_BATCH_SIZE`
|
- `YOLO_BATCH_SIZE`
|
||||||
|
|
||||||
|
### Local model asset catalog
|
||||||
|
|
||||||
|
GeoIntel can list local runtime model files mounted into the backend model
|
||||||
|
directory through `GET /api/v1/detection/model-assets`. The catalog is
|
||||||
|
filesystem-backed and read-only: it reports existing `.pt`, `.onnx` and
|
||||||
|
`.engine` files, size, checksum and whether the file matches `YOLO_MODEL_PATH`.
|
||||||
|
|
||||||
|
Detection runs still use `model_id="yolo-configured"` for the configured YOLO
|
||||||
|
execution path. A selected `model_asset_id` can be supplied to use one specific
|
||||||
|
cataloged file for that run. The backend resolves the ID to a local path and
|
||||||
|
persists the selected asset metadata in Job/AnalysisRun parameters. GeoIntel
|
||||||
|
does not download weights or accept arbitrary model paths from the browser.
|
||||||
|
|
||||||
### Sprint 8C detection visualization and QA status
|
### Sprint 8C detection visualization and QA status
|
||||||
|
|
||||||
Sprint 8C makes persisted detections reviewable:
|
Sprint 8C makes persisted detections reviewable:
|
||||||
|
|||||||
@@ -636,12 +636,51 @@ Returns object-detection model capability descriptors.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### GET `/api/v1/detection/model-assets`
|
||||||
|
|
||||||
|
Returns local runtime model files discovered in the configured model directory.
|
||||||
|
This is a read-only catalog. GeoIntel never downloads, creates, mutates or
|
||||||
|
deletes model weights from this endpoint.
|
||||||
|
|
||||||
|
The backend scans `YOLO_MODELS_DIR` (default `/app/models`) and reports
|
||||||
|
supported local model files such as `.pt`, `.onnx` and `.engine`. The active
|
||||||
|
model is the file matching `YOLO_MODEL_PATH`.
|
||||||
|
|
||||||
|
Response data:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"model_asset_id": "building-detector-pt",
|
||||||
|
"filename": "building-detector.pt",
|
||||||
|
"display_name": "building-detector",
|
||||||
|
"model_path": "/app/models/building-detector.pt",
|
||||||
|
"suffix": ".pt",
|
||||||
|
"framework": "ultralytics/pytorch",
|
||||||
|
"task_type": "object_detection",
|
||||||
|
"size_bytes": 123456,
|
||||||
|
"sha256": "sha256hex",
|
||||||
|
"active": true,
|
||||||
|
"status": "available",
|
||||||
|
"limitation_message": "Local runtime model asset. GeoIntel will not download or mutate model weights.",
|
||||||
|
"will_download_models": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1,
|
||||||
|
"model_directory": "/app/models"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### GET `/api/v1/detection/yolo/preflight`
|
### GET `/api/v1/detection/yolo/preflight`
|
||||||
|
|
||||||
Returns a canonical envelope with read-only configured-YOLO runtime preflight
|
Returns a canonical envelope with read-only configured-YOLO runtime preflight
|
||||||
state. Optional query parameters:
|
state. Optional query parameters:
|
||||||
|
|
||||||
- `tile_manifest_path`: existing raster tile manifest path to validate.
|
- `tile_manifest_path`: existing raster tile manifest path to validate.
|
||||||
|
- `model_asset_id`: optional local model asset ID from
|
||||||
|
`GET /api/v1/detection/model-assets`; when supplied, preflight validates that
|
||||||
|
asset path instead of the default `YOLO_MODEL_PATH`.
|
||||||
- `check_model_load`: default `false`; when `true`, explicitly loads only the
|
- `check_model_load`: default `false`; when `true`, explicitly loads only the
|
||||||
configured local model file for compatibility smoke. It never downloads
|
configured local model file for compatibility smoke. It never downloads
|
||||||
weights and never runs inference.
|
weights and never runs inference.
|
||||||
@@ -651,6 +690,7 @@ Response data:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"model_id": "yolo-configured",
|
"model_id": "yolo-configured",
|
||||||
|
"model_asset_id": null,
|
||||||
"model_path": null,
|
"model_path": null,
|
||||||
"tile_manifest_path": null,
|
"tile_manifest_path": null,
|
||||||
"status": "not_configured",
|
"status": "not_configured",
|
||||||
@@ -693,6 +733,7 @@ Request:
|
|||||||
"project_id": "uuid",
|
"project_id": "uuid",
|
||||||
"dataset_id": "uuid",
|
"dataset_id": "uuid",
|
||||||
"model_id": "yolo-placeholder",
|
"model_id": "yolo-placeholder",
|
||||||
|
"model_asset_id": null,
|
||||||
"confidence_threshold": 0.5,
|
"confidence_threshold": 0.5,
|
||||||
"class_filter": ["building"],
|
"class_filter": ["building"],
|
||||||
"tile_manifest_path": null,
|
"tile_manifest_path": null,
|
||||||
@@ -707,6 +748,12 @@ Sprint 8B configured YOLO mode uses `model_id: "yolo-configured"`. It requires:
|
|||||||
- backend optional AI dependencies installed with `geointel-backend[ai]`
|
- backend optional AI dependencies installed with `geointel-backend[ai]`
|
||||||
- `tile_manifest_path` pointing to an existing raster tile manifest generated by the raster tile operation
|
- `tile_manifest_path` pointing to an existing raster tile manifest generated by the raster tile operation
|
||||||
|
|
||||||
|
`model_asset_id` may be supplied with `model_id: "yolo-configured"` to select a
|
||||||
|
specific local model file from the read-only model asset catalog. The backend
|
||||||
|
resolves the ID to a file inside the configured model directory and persists the
|
||||||
|
asset ID, path and SHA-256 in the job and analysis-run parameters for
|
||||||
|
reproducibility. Clients must not submit arbitrary model paths.
|
||||||
|
|
||||||
GeoIntel does not download model weights automatically. Configured YOLO runs read existing tile files from the manifest, convert YOLO pixel-space boxes to EPSG:4326 detection polygons and persist detections as first-class records.
|
GeoIntel does not download model weights automatically. Configured YOLO runs read existing tile files from the manifest, convert YOLO pixel-space boxes to EPSG:4326 detection polygons and persist detections as first-class records.
|
||||||
|
|
||||||
Unavailable model response:
|
Unavailable model response:
|
||||||
@@ -729,6 +776,7 @@ Validation errors:
|
|||||||
|
|
||||||
- `INVALID_DATASET_TYPE` when the dataset is not raster.
|
- `INVALID_DATASET_TYPE` when the dataset is not raster.
|
||||||
- `DETECTION_MODEL_NOT_FOUND` when the model id is unknown.
|
- `DETECTION_MODEL_NOT_FOUND` when the model id is unknown.
|
||||||
|
- `DETECTION_MODEL_ASSET_NOT_FOUND` when `model_asset_id` is not present in the configured model directory.
|
||||||
- `FIXTURE_MODE_REQUIRED` when `manual-fixture-detector` is requested without `parameters_json.fixture_mode=true`.
|
- `FIXTURE_MODE_REQUIRED` when `manual-fixture-detector` is requested without `parameters_json.fixture_mode=true`.
|
||||||
- `DETECTION_TILE_MANIFEST_REQUIRED` when `yolo-configured` is requested without `tile_manifest_path`.
|
- `DETECTION_TILE_MANIFEST_REQUIRED` when `yolo-configured` is requested without `tile_manifest_path`.
|
||||||
- `DETECTION_TILE_MANIFEST_NOT_FOUND` when the provided manifest path does not exist.
|
- `DETECTION_TILE_MANIFEST_NOT_FOUND` when the provided manifest path does not exist.
|
||||||
|
|||||||
@@ -1,3 +1,39 @@
|
|||||||
|
## Sprint 118 Local model and reference catalog clarity (2026-07-06)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Added a read-only backend model asset catalog through `GET /api/v1/detection/model-assets`.
|
||||||
|
- Added `YOLO_MODELS_DIR` to backend settings, Compose, Unraid env examples and all-in-one runtime startup so `/app/models` is the explicit model catalog directory.
|
||||||
|
- Extended configured YOLO preflight and detection runs with optional `model_asset_id`, resolved server-side against the model asset catalog.
|
||||||
|
- Detection jobs and analysis runs now persist selected model asset ID, path and SHA-256 in parameters for reproducibility.
|
||||||
|
- Detection Lab now loads local model assets, selects the active model by default and lets operators choose a cataloged local model file for `yolo-configured`.
|
||||||
|
- Provider Capabilities now distinguishes GRB/OSM/manual/fixture reference-data sources from AI model choices.
|
||||||
|
- Updated `docs/API_CONTRACTS.md`, `docs/AI_PIPELINES.md`, `backend/README.md`, `frontend/README.md`, `deploy/unraid/README.md`, `scripts/README.md`, `docs/TODO.md` and `CHANGELOG.md`.
|
||||||
|
- Added design/plan documents under `docs/superpowers/`.
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- RED: `python -m pytest backend/tests/test_model_asset_catalog.py -q` failed before implementation because `app.services.model_asset_catalog_service` did not exist.
|
||||||
|
- `python -m pytest backend/tests/test_model_asset_catalog.py -q` passed: 5 tests.
|
||||||
|
- RED: `python -m pytest backend/tests/test_sprint118_yolo_preflight_ui.py -q` failed before frontend wiring because the model asset types/API/hook/UI were absent.
|
||||||
|
- `python -m pytest backend/tests/test_sprint118_yolo_preflight_ui.py -q` passed: 2 tests.
|
||||||
|
- RED: runtime config tests failed before `YOLO_MODELS_DIR` was added to env examples, Unraid runtime and `scripts/configure_yolo_model.py`.
|
||||||
|
- `python -m pytest backend/tests/test_docker_runtime_config.py::test_env_example_uses_runtime_env_names_read_by_backend_and_frontend backend/tests/test_docker_runtime_config.py::test_unraid_deploy_passes_ai_build_arg_and_yolo_runtime_env backend/tests/test_sprint119_yolo_model_configuration.py -q` passed: 6 tests.
|
||||||
|
- `python -m compileall backend/app` passed.
|
||||||
|
- `cd backend && python -m pytest -q` passed: 383 tests.
|
||||||
|
- `cd frontend && npm run typecheck` passed.
|
||||||
|
- `cd frontend && npm run build` passed.
|
||||||
|
- `python scripts/audit_api_contracts.py` passed: 81 implemented routes match docs; 2 explicit non-envelope endpoints tracked.
|
||||||
|
- `bash scripts/run_readiness_check.sh` passed: 383 backend tests plus frontend typecheck/build, API contract audit, Alembic head and shell syntax checks.
|
||||||
|
- `cd backend && python -m alembic upgrade head --sql` passed.
|
||||||
|
- `bash -n scripts/live_migration_smoke.sh; bash -n deploy/unraid/run-dockerman-container.sh; bash -n deploy/unraid/all-in-one-start.sh; bash -n scripts/deploy_tower.sh` passed.
|
||||||
|
- Local `docker compose config` could not run because Docker is not installed in this Windows Codex environment; Tower deploy validation remains required.
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- The catalog is intentionally filesystem-backed and read-only. It does not download, validate semantic class metadata, train models or manage model lifecycle records in the database.
|
||||||
|
- GRB/OSM remain provider capabilities only; no live external fetching was added.
|
||||||
|
|
||||||
|
Next recommended pass:
|
||||||
|
- Redeploy Tower, verify `/api/v1/detection/model-assets`, confirm Detection Lab shows the local model picker, then continue with real raster/model workflow validation on non-synthetic imagery.
|
||||||
|
|
||||||
## Sprint 117 Reusable GIS run and AI runtime opt-in (2026-07-05)
|
## Sprint 117 Reusable GIS run and AI runtime opt-in (2026-07-05)
|
||||||
|
|
||||||
Changed:
|
Changed:
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Add reusable latest-result mode for repeated Map QA/QC runs without duplicate derived artifacts.
|
- [x] Add reusable latest-result mode for repeated Map QA/QC runs without duplicate derived artifacts.
|
||||||
- [x] Add opt-in Docker/Unraid AI build/runtime path for local PyTorch/Ultralytics YOLO operation.
|
- [x] Add opt-in Docker/Unraid AI build/runtime path for local PyTorch/Ultralytics YOLO operation.
|
||||||
- [x] Surface configured-YOLO runtime preflight status through the API and Detection Lab UI.
|
- [x] Surface configured-YOLO runtime preflight status through the API and Detection Lab UI.
|
||||||
|
- [x] Add read-only local model asset catalog and Detection Lab model-file selection.
|
||||||
- [x] Add one-click full GIS workflow action for query, derived dataset, QA/QC and export handoff.
|
- [x] Add one-click full GIS workflow action for query, derived dataset, QA/QC and export handoff.
|
||||||
- [x] Add QA/QC workspace result hierarchy and filter density polish.
|
- [x] Add QA/QC workspace result hierarchy and filter density polish.
|
||||||
- [x] Add Change Detection panel hierarchy and analysis workspace density polish.
|
- [x] Add Change Detection panel hierarchy and analysis workspace density polish.
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst
|
|||||||
## Sprint 7B additions
|
## Sprint 7B additions
|
||||||
- Added a lightweight Provider Capabilities panel.
|
- Added a lightweight Provider Capabilities panel.
|
||||||
- The panel lists GRB, OSM, manual and fixture provider status, configured state, authority level, supported layers, supported geometry types, query modes and limitation messages.
|
- The panel lists GRB, OSM, manual and fixture provider status, configured state, authority level, supported layers, supported geometry types, query modes and limitation messages.
|
||||||
|
- Provider Capabilities now labels GRB/OSM/manual/fixture as reference data source capabilities, not AI model choices.
|
||||||
- GRB and OSM are shown as `not_configured`; the UI does not expose a live import/download action for them.
|
- GRB and OSM are shown as `not_configured`; the UI does not expose a live import/download action for them.
|
||||||
- Existing dataset, reference and QA/QC UI remains unchanged.
|
- Existing dataset, reference and QA/QC UI remains unchanged.
|
||||||
|
|
||||||
@@ -120,6 +121,7 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst
|
|||||||
## Sprint 8B additions
|
## Sprint 8B additions
|
||||||
- Detection Lab now exposes the `yolo-configured` capability reported by the backend.
|
- Detection Lab now exposes the `yolo-configured` capability reported by the backend.
|
||||||
- When `yolo-configured` is selected, users can provide an existing raster tile manifest path.
|
- When `yolo-configured` is selected, users can provide an existing raster tile manifest path.
|
||||||
|
- Detection Lab lists local model assets from `GET /api/v1/detection/model-assets` so operators can choose an existing mounted model file instead of editing only one hidden `YOLO_MODEL_PATH` slot.
|
||||||
- Detection Lab includes a read-only YOLO runtime preflight panel with backend status, dependency visibility, local model configuration, `torch`/`ultralytics` versions, CUDA state and `YOLO_CONFIG_DIR`.
|
- Detection Lab includes a read-only YOLO runtime preflight panel with backend status, dependency visibility, local model configuration, `torch`/`ultralytics` versions, CUDA state and `YOLO_CONFIG_DIR`.
|
||||||
- The UI still does not download models or create fake detections; backend status and error codes remain the source of truth.
|
- The UI still does not download models or create fake detections; backend status and error codes remain the source of truth.
|
||||||
|
|
||||||
|
|||||||
@@ -222,10 +222,13 @@ function App(): JSX.Element {
|
|||||||
})
|
})
|
||||||
const {
|
const {
|
||||||
detectionModels,
|
detectionModels,
|
||||||
|
modelAssets,
|
||||||
loadingDetectionModels,
|
loadingDetectionModels,
|
||||||
detectionModelError,
|
detectionModelError,
|
||||||
|
modelAssetError,
|
||||||
selectedDetectionDatasetId,
|
selectedDetectionDatasetId,
|
||||||
selectedDetectionModelId,
|
selectedDetectionModelId,
|
||||||
|
selectedModelAssetId,
|
||||||
detectionTileManifestPath,
|
detectionTileManifestPath,
|
||||||
detectionConfidenceThreshold,
|
detectionConfidenceThreshold,
|
||||||
runningDetection,
|
runningDetection,
|
||||||
@@ -254,6 +257,7 @@ function App(): JSX.Element {
|
|||||||
resetDetectionForProject,
|
resetDetectionForProject,
|
||||||
setSelectedDetectionDatasetId,
|
setSelectedDetectionDatasetId,
|
||||||
setSelectedDetectionModelId,
|
setSelectedDetectionModelId,
|
||||||
|
setSelectedModelAssetId,
|
||||||
setDetectionTileManifestPath,
|
setDetectionTileManifestPath,
|
||||||
setDetectionConfidenceThreshold,
|
setDetectionConfidenceThreshold,
|
||||||
setSelectedDetectionRunId,
|
setSelectedDetectionRunId,
|
||||||
@@ -930,10 +934,13 @@ function App(): JSX.Element {
|
|||||||
<div className="workspace-grid workspace-grid-ai">
|
<div className="workspace-grid workspace-grid-ai">
|
||||||
<DetectionLab
|
<DetectionLab
|
||||||
detectionModels={detectionModels}
|
detectionModels={detectionModels}
|
||||||
|
modelAssets={modelAssets}
|
||||||
loadingDetectionModels={loadingDetectionModels}
|
loadingDetectionModels={loadingDetectionModels}
|
||||||
detectionModelError={detectionModelError}
|
detectionModelError={detectionModelError}
|
||||||
|
modelAssetError={modelAssetError}
|
||||||
selectedDetectionDatasetId={selectedDetectionDatasetId}
|
selectedDetectionDatasetId={selectedDetectionDatasetId}
|
||||||
selectedDetectionModelId={selectedDetectionModelId}
|
selectedDetectionModelId={selectedDetectionModelId}
|
||||||
|
selectedModelAssetId={selectedModelAssetId}
|
||||||
detectionTileManifestPath={detectionTileManifestPath}
|
detectionTileManifestPath={detectionTileManifestPath}
|
||||||
detectionConfidenceThreshold={detectionConfidenceThreshold}
|
detectionConfidenceThreshold={detectionConfidenceThreshold}
|
||||||
runningDetection={runningDetection}
|
runningDetection={runningDetection}
|
||||||
@@ -959,6 +966,7 @@ function App(): JSX.Element {
|
|||||||
onRefreshYoloPreflight={() => loadYoloPreflight()}
|
onRefreshYoloPreflight={() => loadYoloPreflight()}
|
||||||
onSelectDataset={setSelectedDetectionDatasetId}
|
onSelectDataset={setSelectedDetectionDatasetId}
|
||||||
onSelectModel={setSelectedDetectionModelId}
|
onSelectModel={setSelectedDetectionModelId}
|
||||||
|
onSelectModelAsset={setSelectedModelAssetId}
|
||||||
onSetConfidenceThreshold={setDetectionConfidenceThreshold}
|
onSetConfidenceThreshold={setDetectionConfidenceThreshold}
|
||||||
onSetTileManifestPath={setDetectionTileManifestPath}
|
onSetTileManifestPath={setDetectionTileManifestPath}
|
||||||
onRunDetection={runDetection}
|
onRunDetection={runDetection}
|
||||||
|
|||||||
@@ -5,15 +5,19 @@ import type {
|
|||||||
DetectionRead,
|
DetectionRead,
|
||||||
DetectionRunRead,
|
DetectionRunRead,
|
||||||
DetectionRunResponse,
|
DetectionRunResponse,
|
||||||
|
ModelAssetRead,
|
||||||
YoloPreflightResponse,
|
YoloPreflightResponse,
|
||||||
} from '../../types'
|
} from '../../types'
|
||||||
|
|
||||||
interface DetectionLabProps {
|
interface DetectionLabProps {
|
||||||
detectionModels: DetectionModelCapability[]
|
detectionModels: DetectionModelCapability[]
|
||||||
|
modelAssets: ModelAssetRead[]
|
||||||
loadingDetectionModels: boolean
|
loadingDetectionModels: boolean
|
||||||
detectionModelError: string | null
|
detectionModelError: string | null
|
||||||
|
modelAssetError: string | null
|
||||||
selectedDetectionDatasetId: string
|
selectedDetectionDatasetId: string
|
||||||
selectedDetectionModelId: string
|
selectedDetectionModelId: string
|
||||||
|
selectedModelAssetId: string
|
||||||
detectionTileManifestPath: string
|
detectionTileManifestPath: string
|
||||||
detectionConfidenceThreshold: number
|
detectionConfidenceThreshold: number
|
||||||
runningDetection: boolean
|
runningDetection: boolean
|
||||||
@@ -39,6 +43,7 @@ interface DetectionLabProps {
|
|||||||
onRefreshYoloPreflight: () => void
|
onRefreshYoloPreflight: () => void
|
||||||
onSelectDataset: (datasetId: string) => void
|
onSelectDataset: (datasetId: string) => void
|
||||||
onSelectModel: (modelId: string) => void
|
onSelectModel: (modelId: string) => void
|
||||||
|
onSelectModelAsset: (modelAssetId: string) => void
|
||||||
onSetConfidenceThreshold: (value: number) => void
|
onSetConfidenceThreshold: (value: number) => void
|
||||||
onSetTileManifestPath: (value: string) => void
|
onSetTileManifestPath: (value: string) => void
|
||||||
onRunDetection: () => void
|
onRunDetection: () => void
|
||||||
@@ -53,10 +58,13 @@ interface DetectionLabProps {
|
|||||||
|
|
||||||
export function DetectionLab({
|
export function DetectionLab({
|
||||||
detectionModels,
|
detectionModels,
|
||||||
|
modelAssets,
|
||||||
loadingDetectionModels,
|
loadingDetectionModels,
|
||||||
detectionModelError,
|
detectionModelError,
|
||||||
|
modelAssetError,
|
||||||
selectedDetectionDatasetId,
|
selectedDetectionDatasetId,
|
||||||
selectedDetectionModelId,
|
selectedDetectionModelId,
|
||||||
|
selectedModelAssetId,
|
||||||
detectionTileManifestPath,
|
detectionTileManifestPath,
|
||||||
detectionConfidenceThreshold,
|
detectionConfidenceThreshold,
|
||||||
runningDetection,
|
runningDetection,
|
||||||
@@ -82,6 +90,7 @@ export function DetectionLab({
|
|||||||
onRefreshYoloPreflight,
|
onRefreshYoloPreflight,
|
||||||
onSelectDataset,
|
onSelectDataset,
|
||||||
onSelectModel,
|
onSelectModel,
|
||||||
|
onSelectModelAsset,
|
||||||
onSetConfidenceThreshold,
|
onSetConfidenceThreshold,
|
||||||
onSetTileManifestPath,
|
onSetTileManifestPath,
|
||||||
onRunDetection,
|
onRunDetection,
|
||||||
@@ -94,6 +103,7 @@ export function DetectionLab({
|
|||||||
onRunQa,
|
onRunQa,
|
||||||
}: DetectionLabProps): JSX.Element {
|
}: DetectionLabProps): JSX.Element {
|
||||||
const selectedDetectionModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId) ?? null
|
const selectedDetectionModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId) ?? null
|
||||||
|
const selectedModelAsset = modelAssets.find((asset) => asset.model_asset_id === selectedModelAssetId) ?? null
|
||||||
const detectionRequiresTileManifest = selectedDetectionModelId === 'yolo-configured'
|
const detectionRequiresTileManifest = selectedDetectionModelId === 'yolo-configured'
|
||||||
const detectionHasDataset = selectedDetectionDatasetId.length > 0
|
const detectionHasDataset = selectedDetectionDatasetId.length > 0
|
||||||
const detectionHasModel = selectedDetectionModel !== null
|
const detectionHasModel = selectedDetectionModel !== null
|
||||||
@@ -149,6 +159,12 @@ export function DetectionLab({
|
|||||||
<p>{detectionModelError}</p>
|
<p>{detectionModelError}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{modelAssetError ? (
|
||||||
|
<div className="result-state result-state-error">
|
||||||
|
<strong>Local model assets unavailable.</strong>
|
||||||
|
<p>{modelAssetError}</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{detectionModels.length === 0 && !loadingDetectionModels ? (
|
{detectionModels.length === 0 && !loadingDetectionModels ? (
|
||||||
<div className="result-state result-state-empty">
|
<div className="result-state result-state-empty">
|
||||||
<strong>No detection models reported by backend.</strong>
|
<strong>No detection models reported by backend.</strong>
|
||||||
@@ -173,6 +189,47 @@ export function DetectionLab({
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{selectedDetectionModelId === 'yolo-configured' ? (
|
||||||
|
<div className="ai-lab-model-surface" aria-label="Local model asset selection">
|
||||||
|
<div className="ai-lab-section-header">
|
||||||
|
<div>
|
||||||
|
<h3>Local model assets</h3>
|
||||||
|
<p>Select an existing model file mounted into the backend runtime. GeoIntel does not download model weights.</p>
|
||||||
|
</div>
|
||||||
|
<span className={selectedModelAsset ? 'status-badge status-badge-ready' : 'status-badge'}>
|
||||||
|
{selectedModelAsset ? 'asset selected' : 'using configured path'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
Local model file
|
||||||
|
<select value={selectedModelAssetId} onChange={(event) => onSelectModelAsset(event.target.value)}>
|
||||||
|
<option value="">Use configured YOLO_MODEL_PATH</option>
|
||||||
|
{modelAssets.map((asset) => (
|
||||||
|
<option key={asset.model_asset_id} value={asset.model_asset_id}>
|
||||||
|
{asset.display_name} {asset.active ? '(active)' : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{modelAssets.length === 0 && !loadingDetectionModels ? (
|
||||||
|
<div className="result-state result-state-empty">
|
||||||
|
<strong>No local model assets found.</strong>
|
||||||
|
<p>Mount model files into the backend model directory or continue with the configured YOLO_MODEL_PATH.</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{selectedModelAsset ? (
|
||||||
|
<div className="result-summary-card">
|
||||||
|
<p>File: {selectedModelAsset.filename}</p>
|
||||||
|
<p>Status: {selectedModelAsset.status}</p>
|
||||||
|
<p>Size: {formatModelAssetSize(selectedModelAsset.size_bytes)}</p>
|
||||||
|
<p>SHA-256: {selectedModelAsset.sha256.slice(0, 12)}</p>
|
||||||
|
<p>Path: {selectedModelAsset.model_path}</p>
|
||||||
|
<p>{selectedModelAsset.limitation_message}</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="ai-lab-model-surface" aria-label="YOLO runtime preflight">
|
<div className="ai-lab-model-surface" aria-label="YOLO runtime preflight">
|
||||||
<div className="ai-lab-section-header">
|
<div className="ai-lab-section-header">
|
||||||
<div>
|
<div>
|
||||||
@@ -238,6 +295,7 @@ export function DetectionLab({
|
|||||||
<span>cuda_available: {String(yoloPreflight.runtime.cuda_available ?? 'unknown')}</span>
|
<span>cuda_available: {String(yoloPreflight.runtime.cuda_available ?? 'unknown')}</span>
|
||||||
<span>YOLO_CONFIG_DIR: {yoloPreflight.runtime.yolo_config_dir ?? 'n/a'}</span>
|
<span>YOLO_CONFIG_DIR: {yoloPreflight.runtime.yolo_config_dir ?? 'n/a'}</span>
|
||||||
<span>model directory: {yoloPreflight.runtime.model_directory ?? 'n/a'}</span>
|
<span>model directory: {yoloPreflight.runtime.model_directory ?? 'n/a'}</span>
|
||||||
|
<span>model_asset_id: {yoloPreflight.model_asset_id ?? 'n/a'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -486,3 +544,13 @@ export function DetectionLab({
|
|||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatModelAssetSize(sizeBytes: number): string {
|
||||||
|
if (sizeBytes >= 1024 * 1024) {
|
||||||
|
return `${(sizeBytes / (1024 * 1024)).toFixed(1)} MB`
|
||||||
|
}
|
||||||
|
if (sizeBytes >= 1024) {
|
||||||
|
return `${(sizeBytes / 1024).toFixed(1)} KB`
|
||||||
|
}
|
||||||
|
return `${sizeBytes} B`
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ export function ProviderPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="system-provider-capability-surface" aria-label="Provider capability registry">
|
<div className="system-provider-capability-surface" aria-label="Provider capability registry">
|
||||||
|
<div className="provider-detail-stack">
|
||||||
|
<strong>Official reference sources</strong>
|
||||||
|
<div>
|
||||||
|
GRB and OSM are reference-data provider capabilities, not AI model choices. Manual upload is the configured path for real reference datasets today; fixtures remain test/demo only.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<ul className="system-provider-list">
|
<ul className="system-provider-list">
|
||||||
{providers.map((provider) => (
|
{providers.map((provider) => (
|
||||||
<li className="system-provider-card" key={provider.provider_name}>
|
<li className="system-provider-card" key={provider.provider_name}>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
DetectionRead,
|
DetectionRead,
|
||||||
DetectionRunRead,
|
DetectionRunRead,
|
||||||
DetectionRunResponse,
|
DetectionRunResponse,
|
||||||
|
ModelAssetRead,
|
||||||
QualityCheckRead,
|
QualityCheckRead,
|
||||||
YoloPreflightResponse,
|
YoloPreflightResponse,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
@@ -28,10 +29,13 @@ export function useDetectionWorkflow({
|
|||||||
loadQualityChecks,
|
loadQualityChecks,
|
||||||
}: DetectionWorkflowOptions) {
|
}: DetectionWorkflowOptions) {
|
||||||
const [detectionModels, setDetectionModels] = useState<DetectionModelCapability[]>([])
|
const [detectionModels, setDetectionModels] = useState<DetectionModelCapability[]>([])
|
||||||
|
const [modelAssets, setModelAssets] = useState<ModelAssetRead[]>([])
|
||||||
const [loadingDetectionModels, setLoadingDetectionModels] = useState(false)
|
const [loadingDetectionModels, setLoadingDetectionModels] = useState(false)
|
||||||
const [detectionModelError, setDetectionModelError] = useState<string | null>(null)
|
const [detectionModelError, setDetectionModelError] = useState<string | null>(null)
|
||||||
|
const [modelAssetError, setModelAssetError] = useState<string | null>(null)
|
||||||
const [selectedDetectionDatasetId, setSelectedDetectionDatasetId] = useState('')
|
const [selectedDetectionDatasetId, setSelectedDetectionDatasetId] = useState('')
|
||||||
const [selectedDetectionModelId, setSelectedDetectionModelId] = useState('yolo-placeholder')
|
const [selectedDetectionModelId, setSelectedDetectionModelId] = useState('yolo-placeholder')
|
||||||
|
const [selectedModelAssetId, setSelectedModelAssetId] = useState('')
|
||||||
const [detectionTileManifestPath, setDetectionTileManifestPath] = useState('')
|
const [detectionTileManifestPath, setDetectionTileManifestPath] = useState('')
|
||||||
const [detectionConfidenceThreshold, setDetectionConfidenceThreshold] = useState(0.5)
|
const [detectionConfidenceThreshold, setDetectionConfidenceThreshold] = useState(0.5)
|
||||||
const [runningDetection, setRunningDetection] = useState(false)
|
const [runningDetection, setRunningDetection] = useState(false)
|
||||||
@@ -55,6 +59,7 @@ export function useDetectionWorkflow({
|
|||||||
const loadDetectionModels = async () => {
|
const loadDetectionModels = async () => {
|
||||||
setLoadingDetectionModels(true)
|
setLoadingDetectionModels(true)
|
||||||
setDetectionModelError(null)
|
setDetectionModelError(null)
|
||||||
|
setModelAssetError(null)
|
||||||
try {
|
try {
|
||||||
const response = await detectionApi.listModels()
|
const response = await detectionApi.listModels()
|
||||||
setDetectionModels(response.models)
|
setDetectionModels(response.models)
|
||||||
@@ -63,6 +68,17 @@ export function useDetectionWorkflow({
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setDetectionModelError(formatError(error, 'Failed to load detection models'))
|
setDetectionModelError(formatError(error, 'Failed to load detection models'))
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const assetResponse = await detectionApi.listModelAssets()
|
||||||
|
setModelAssets(assetResponse.items)
|
||||||
|
const activeAsset = assetResponse.items.find((asset) => asset.active) ?? assetResponse.items[0] ?? null
|
||||||
|
if (!assetResponse.items.some((asset) => asset.model_asset_id === selectedModelAssetId)) {
|
||||||
|
setSelectedModelAssetId(activeAsset?.model_asset_id ?? '')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setModelAssets([])
|
||||||
|
setModelAssetError(formatError(error, 'Failed to load local model assets'))
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingDetectionModels(false)
|
setLoadingDetectionModels(false)
|
||||||
}
|
}
|
||||||
@@ -74,6 +90,7 @@ export function useDetectionWorkflow({
|
|||||||
try {
|
try {
|
||||||
const response = await detectionApi.getYoloPreflight({
|
const response = await detectionApi.getYoloPreflight({
|
||||||
tile_manifest_path: tileManifestPath.trim() || null,
|
tile_manifest_path: tileManifestPath.trim() || null,
|
||||||
|
model_asset_id: selectedModelAssetId || null,
|
||||||
})
|
})
|
||||||
setYoloPreflight(response)
|
setYoloPreflight(response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -143,6 +160,7 @@ export function useDetectionWorkflow({
|
|||||||
project_id: selectedProjectId,
|
project_id: selectedProjectId,
|
||||||
dataset_id: datasetId,
|
dataset_id: datasetId,
|
||||||
model_id: selectedDetectionModelId,
|
model_id: selectedDetectionModelId,
|
||||||
|
model_asset_id: selectedModelAssetId || null,
|
||||||
confidence_threshold: detectionConfidenceThreshold,
|
confidence_threshold: detectionConfidenceThreshold,
|
||||||
tile_manifest_path: detectionTileManifestPath.trim() || null,
|
tile_manifest_path: detectionTileManifestPath.trim() || null,
|
||||||
parameters_json: {},
|
parameters_json: {},
|
||||||
@@ -198,10 +216,13 @@ export function useDetectionWorkflow({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
detectionModels,
|
detectionModels,
|
||||||
|
modelAssets,
|
||||||
loadingDetectionModels,
|
loadingDetectionModels,
|
||||||
detectionModelError,
|
detectionModelError,
|
||||||
|
modelAssetError,
|
||||||
selectedDetectionDatasetId,
|
selectedDetectionDatasetId,
|
||||||
selectedDetectionModelId,
|
selectedDetectionModelId,
|
||||||
|
selectedModelAssetId,
|
||||||
detectionTileManifestPath,
|
detectionTileManifestPath,
|
||||||
detectionConfidenceThreshold,
|
detectionConfidenceThreshold,
|
||||||
runningDetection,
|
runningDetection,
|
||||||
@@ -230,6 +251,7 @@ export function useDetectionWorkflow({
|
|||||||
resetDetectionForProject,
|
resetDetectionForProject,
|
||||||
setSelectedDetectionDatasetId,
|
setSelectedDetectionDatasetId,
|
||||||
setSelectedDetectionModelId,
|
setSelectedDetectionModelId,
|
||||||
|
setSelectedModelAssetId,
|
||||||
setDetectionTileManifestPath,
|
setDetectionTileManifestPath,
|
||||||
setDetectionConfidenceThreshold,
|
setDetectionConfidenceThreshold,
|
||||||
setSelectedDetectionRunId,
|
setSelectedDetectionRunId,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
DetectionRunRead,
|
DetectionRunRead,
|
||||||
DetectionRunRequest,
|
DetectionRunRequest,
|
||||||
DetectionRunResponse,
|
DetectionRunResponse,
|
||||||
|
ModelAssetListResponse,
|
||||||
YoloPreflightResponse,
|
YoloPreflightResponse,
|
||||||
} from '../../types'
|
} from '../../types'
|
||||||
|
|
||||||
@@ -24,7 +25,8 @@ function queryString(params: Record<string, string | number | boolean | null | u
|
|||||||
|
|
||||||
export const detectionApi = {
|
export const detectionApi = {
|
||||||
listModels: (): Promise<DetectionModelsResponse> => apiGet<DetectionModelsResponse>('/api/v1/detection/models'),
|
listModels: (): Promise<DetectionModelsResponse> => apiGet<DetectionModelsResponse>('/api/v1/detection/models'),
|
||||||
getYoloPreflight: (params: { tile_manifest_path?: string | null; check_model_load?: boolean | null } = {}): Promise<YoloPreflightResponse> =>
|
listModelAssets: (): Promise<ModelAssetListResponse> => apiGet<ModelAssetListResponse>('/api/v1/detection/model-assets'),
|
||||||
|
getYoloPreflight: (params: { tile_manifest_path?: string | null; check_model_load?: boolean | null; model_asset_id?: string | null } = {}): Promise<YoloPreflightResponse> =>
|
||||||
apiGet<YoloPreflightResponse>(`/api/v1/detection/yolo/preflight${queryString(params)}`),
|
apiGet<YoloPreflightResponse>(`/api/v1/detection/yolo/preflight${queryString(params)}`),
|
||||||
run: (payload: DetectionRunRequest): Promise<DetectionRunResponse> =>
|
run: (payload: DetectionRunRequest): Promise<DetectionRunResponse> =>
|
||||||
apiPost<DetectionRunResponse>('/api/v1/detection/run', payload),
|
apiPost<DetectionRunResponse>('/api/v1/detection/run', payload),
|
||||||
|
|||||||
@@ -404,6 +404,28 @@ export interface DetectionModelsResponse {
|
|||||||
models: DetectionModelCapability[]
|
models: DetectionModelCapability[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ModelAssetRead {
|
||||||
|
model_asset_id: string
|
||||||
|
filename: string
|
||||||
|
display_name: string
|
||||||
|
model_path: string
|
||||||
|
suffix: string
|
||||||
|
framework: string
|
||||||
|
task_type: string
|
||||||
|
size_bytes: number
|
||||||
|
sha256: string
|
||||||
|
active: boolean
|
||||||
|
status: string
|
||||||
|
limitation_message: string
|
||||||
|
will_download_models: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelAssetListResponse {
|
||||||
|
items: ModelAssetRead[]
|
||||||
|
total: number
|
||||||
|
model_directory: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface YoloPreflightChecks {
|
export interface YoloPreflightChecks {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
dependencies_available?: boolean | null
|
dependencies_available?: boolean | null
|
||||||
@@ -428,6 +450,7 @@ export interface YoloPreflightRuntime {
|
|||||||
|
|
||||||
export interface YoloPreflightResponse {
|
export interface YoloPreflightResponse {
|
||||||
model_id: string
|
model_id: string
|
||||||
|
model_asset_id?: string | null
|
||||||
model_path?: string | null
|
model_path?: string | null
|
||||||
tile_manifest_path?: string | null
|
tile_manifest_path?: string | null
|
||||||
status: string
|
status: string
|
||||||
@@ -445,6 +468,7 @@ export interface DetectionRunRequest {
|
|||||||
project_id: string
|
project_id: string
|
||||||
dataset_id: string
|
dataset_id: string
|
||||||
model_id: string
|
model_id: string
|
||||||
|
model_asset_id?: string | null
|
||||||
confidence_threshold: number
|
confidence_threshold: number
|
||||||
class_filter?: string[] | null
|
class_filter?: string[] | null
|
||||||
tile_manifest_path?: string | null
|
tile_manifest_path?: string | null
|
||||||
|
|||||||
+4
-3
@@ -145,7 +145,8 @@ GEOINTEL_INSTALL_AI=true
|
|||||||
|
|
||||||
For Unraid/all-in-one deployments, place model files under
|
For Unraid/all-in-one deployments, place model files under
|
||||||
`GEOINTEL_MODELS_PATH` so they appear in the container under `/app/models`, then
|
`GEOINTEL_MODELS_PATH` so they appear in the container under `/app/models`, then
|
||||||
set `YOLO_ENABLED=true` and `YOLO_MODEL_PATH=/app/models/<model>.pt`.
|
set `YOLO_ENABLED=true`, `YOLO_MODELS_DIR=/app/models` and
|
||||||
|
`YOLO_MODEL_PATH=/app/models/<model>.pt`.
|
||||||
|
|
||||||
Configure the Unraid/Tower env file from an existing local model without
|
Configure the Unraid/Tower env file from an existing local model without
|
||||||
downloading weights or running inference:
|
downloading weights or running inference:
|
||||||
@@ -168,8 +169,8 @@ python scripts/configure_yolo_model.py \
|
|||||||
|
|
||||||
The configurator refuses to proceed when no model exists or when multiple model
|
The configurator refuses to proceed when no model exists or when multiple model
|
||||||
files are present without `--model-file`. It writes only
|
files are present without `--model-file`. It writes only
|
||||||
`GEOINTEL_INSTALL_AI=true`, `YOLO_ENABLED=true` and the mounted
|
`GEOINTEL_INSTALL_AI=true`, `YOLO_ENABLED=true`, `YOLO_MODELS_DIR=/app/models`
|
||||||
`YOLO_MODEL_PATH`.
|
and the mounted `YOLO_MODEL_PATH`.
|
||||||
|
|
||||||
Clean old offline demo export artifacts without touching uploaded source data:
|
Clean old offline demo export artifacts without touching uploaded source data:
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing import Iterable
|
|||||||
|
|
||||||
|
|
||||||
SUPPORTED_MODEL_SUFFIXES = {".pt", ".onnx", ".engine"}
|
SUPPORTED_MODEL_SUFFIXES = {".pt", ".onnx", ".engine"}
|
||||||
ENV_KEYS = ("GEOINTEL_INSTALL_AI", "YOLO_ENABLED", "YOLO_MODEL_PATH")
|
ENV_KEYS = ("GEOINTEL_INSTALL_AI", "YOLO_ENABLED", "YOLO_MODELS_DIR", "YOLO_MODEL_PATH")
|
||||||
|
|
||||||
|
|
||||||
def _candidate_paths(models_dir: Path) -> list[Path]:
|
def _candidate_paths(models_dir: Path) -> list[Path]:
|
||||||
@@ -149,6 +149,7 @@ def configure(args: argparse.Namespace) -> tuple[int, dict[str, object]]:
|
|||||||
updates = {
|
updates = {
|
||||||
"GEOINTEL_INSTALL_AI": "true",
|
"GEOINTEL_INSTALL_AI": "true",
|
||||||
"YOLO_ENABLED": "true",
|
"YOLO_ENABLED": "true",
|
||||||
|
"YOLO_MODELS_DIR": args.container_model_dir.rstrip("/"),
|
||||||
"YOLO_MODEL_PATH": selected_container_path,
|
"YOLO_MODEL_PATH": selected_container_path,
|
||||||
}
|
}
|
||||||
payload.update(
|
payload.update(
|
||||||
|
|||||||
Reference in New Issue
Block a user