Add YOLO preflight runtime diagnostics
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
- Added an opt-in Docker/Unraid AI build path (`GEOINTEL_INSTALL_AI=true`) for installing optional PyTorch/Ultralytics dependencies while keeping the default GIS runtime lightweight and import-safe.
|
||||
- Hardened the AI Docker runtime with OpenCV native libraries required by Ultralytics and made YOLO dependency detection use real imports instead of optimistic module discovery.
|
||||
- Added a writable `YOLO_CONFIG_DIR` default under application storage so Ultralytics does not fall back to root user config paths in Docker/Unraid.
|
||||
- Added YOLO preflight runtime diagnostics for dependency assumption state, model directory, `YOLO_CONFIG_DIR`, installed `torch`/`ultralytics` versions and CUDA availability without running inference or downloading weights.
|
||||
- Added static regression coverage for the road basemap, attribution, basemap policy notice, database layer selector and persisted operational GIS workflow wiring.
|
||||
|
||||
## Sprint 115 QA/QC and Exports usability layout pass (2026-07-04)
|
||||
|
||||
+1
-1
@@ -299,7 +299,7 @@ To validate only local model/manifest paths on a machine without optional AI dep
|
||||
python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt --tile-manifest-path /absolute/path/to/manifest.json --assume-dependencies --json
|
||||
```
|
||||
|
||||
The preflight checks configuration, dependency availability, local model file existence, tile manifest validity, tile count and referenced tile paths. It does not load a YOLO model, run inference or download weights.
|
||||
The preflight checks configuration, dependency availability, local model file existence, tile manifest validity, tile count and referenced tile paths. JSON output also includes runtime diagnostics for the model directory, `YOLO_CONFIG_DIR`, installed `torch`/`ultralytics` versions and CUDA availability when dependency checks pass. It does not load a YOLO model, run inference or download weights.
|
||||
|
||||
### Run backend
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from importlib import metadata
|
||||
from pathlib import Path
|
||||
from typing import Any, Type
|
||||
|
||||
@@ -42,6 +44,10 @@ class YoloPreflightService:
|
||||
"max_tiles": resolved_settings.yolo_max_tiles,
|
||||
"will_download_models": False,
|
||||
"will_run_inference": False,
|
||||
"runtime": YoloPreflightService._runtime_details(
|
||||
settings=resolved_settings,
|
||||
assume_dependencies=assume_dependencies,
|
||||
),
|
||||
}
|
||||
|
||||
if not resolved_settings.yolo_enabled:
|
||||
@@ -50,6 +56,8 @@ class YoloPreflightService:
|
||||
|
||||
dependencies_available = True if assume_dependencies else yolo_adapter_class.dependencies_available()
|
||||
result["checks"]["dependencies_available"] = dependencies_available
|
||||
if dependencies_available and not assume_dependencies:
|
||||
result["runtime"]["cuda_available"] = YoloPreflightService._torch_cuda_available()
|
||||
if not dependencies_available:
|
||||
result["status"] = "dependency_unavailable"
|
||||
result["message"] = "YOLO dependencies are not installed. Install backend optional extras with geointel-backend[ai]."
|
||||
@@ -115,3 +123,35 @@ class YoloPreflightService:
|
||||
else:
|
||||
result["message"] = "Configured YOLO preflight passed. No model was loaded and no inference was run."
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _runtime_details(*, settings: Settings, assume_dependencies: bool) -> dict[str, Any]:
|
||||
model_directory = None
|
||||
if settings.yolo_model_path:
|
||||
model_directory = str(Path(settings.yolo_model_path).expanduser().parent)
|
||||
return {
|
||||
"dependencies_assumed": assume_dependencies,
|
||||
"model_directory": model_directory,
|
||||
"yolo_config_dir": os.environ.get("YOLO_CONFIG_DIR"),
|
||||
"torch_version": YoloPreflightService._package_version("torch"),
|
||||
"ultralytics_version": YoloPreflightService._package_version("ultralytics"),
|
||||
"cuda_available": None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _package_version(package_name: str) -> str | None:
|
||||
try:
|
||||
return metadata.version(package_name)
|
||||
except metadata.PackageNotFoundError:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _torch_cuda_available() -> bool | None:
|
||||
try:
|
||||
import torch
|
||||
except Exception:
|
||||
return None
|
||||
try:
|
||||
return bool(torch.cuda.is_available())
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -54,7 +54,9 @@ def _manifest(tmp_path: Path, tile_count: int = 1) -> Path:
|
||||
return manifest_path
|
||||
|
||||
|
||||
def test_yolo_preflight_reports_disabled_without_loading_model(tmp_path: Path) -> None:
|
||||
def test_yolo_preflight_reports_disabled_without_loading_model(tmp_path: Path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("YOLO_CONFIG_DIR", str(tmp_path / "ultralytics"))
|
||||
|
||||
result = YoloPreflightService.run(
|
||||
settings=Settings(yolo_enabled=False, yolo_model_path=str(tmp_path / "missing.pt")),
|
||||
tile_manifest_path=str(tmp_path / "missing-manifest.json"),
|
||||
@@ -66,6 +68,12 @@ def test_yolo_preflight_reports_disabled_without_loading_model(tmp_path: Path) -
|
||||
assert result["checks"]["dependencies_available"] is None
|
||||
assert result["checks"]["model_file_exists"] is None
|
||||
assert result["checks"]["manifest_valid"] is None
|
||||
assert result["runtime"]["dependencies_assumed"] is False
|
||||
assert result["runtime"]["model_directory"] == str(tmp_path)
|
||||
assert result["runtime"]["yolo_config_dir"] == str(tmp_path / "ultralytics")
|
||||
assert "torch_version" in result["runtime"]
|
||||
assert "ultralytics_version" in result["runtime"]
|
||||
assert "cuda_available" in result["runtime"]
|
||||
|
||||
|
||||
def test_yolo_preflight_distinguishes_missing_dependencies_from_missing_model(tmp_path: Path) -> None:
|
||||
@@ -102,6 +110,25 @@ def test_yolo_preflight_validates_model_and_manifest_without_importing_yolo(tmp_
|
||||
assert result["tile_count"] == 2
|
||||
assert result["will_download_models"] is False
|
||||
assert result["will_run_inference"] is False
|
||||
assert result["runtime"]["dependencies_assumed"] is False
|
||||
|
||||
|
||||
def test_yolo_preflight_marks_assumed_dependencies_in_runtime_details(tmp_path: Path) -> None:
|
||||
model_path = tmp_path / "model.pt"
|
||||
model_path.write_bytes(b"weights")
|
||||
manifest_path = _manifest(tmp_path, tile_count=1)
|
||||
|
||||
result = YoloPreflightService.run(
|
||||
settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4),
|
||||
tile_manifest_path=str(manifest_path),
|
||||
yolo_adapter_class=MissingDependencyAdapter,
|
||||
assume_dependencies=True,
|
||||
)
|
||||
|
||||
assert result["status"] == "ready"
|
||||
assert result["checks"]["dependencies_available"] is True
|
||||
assert result["runtime"]["dependencies_assumed"] is True
|
||||
assert result["runtime"]["model_directory"] == str(tmp_path)
|
||||
|
||||
|
||||
def test_yolo_preflight_can_explicitly_smoke_load_local_model(tmp_path: Path) -> None:
|
||||
|
||||
@@ -70,6 +70,10 @@ The preflight checks:
|
||||
- tile count against `YOLO_MAX_TILES`;
|
||||
- referenced tile file existence.
|
||||
|
||||
JSON output also reports runtime diagnostics: whether dependencies were assumed,
|
||||
the configured model directory, `YOLO_CONFIG_DIR`, installed `torch` and
|
||||
`ultralytics` versions, and CUDA availability when dependency checks pass.
|
||||
|
||||
The preflight does not load the model, does not import Ultralytics unless dependency discovery requires package metadata, does not run inference and never downloads model weights.
|
||||
|
||||
Sprint 25 adds an explicit local model compatibility smoke:
|
||||
|
||||
@@ -7,6 +7,7 @@ Changed:
|
||||
- Passed YOLO runtime environment variables and a `/app/models` volume into the all-in-one Unraid container so local PyTorch/Ultralytics models can be mounted explicitly.
|
||||
- Hardened the AI image path after Tower validation showed `torch` imported but `ultralytics` failed on a missing OpenCV native library. The Dockerfiles now include the required OpenCV runtime shared libraries and YOLO dependency detection performs real imports instead of `find_spec` checks.
|
||||
- Added a writable `YOLO_CONFIG_DIR` default under application storage after Tower validation showed Ultralytics otherwise falls back to `/tmp` because root config is not writable in the container.
|
||||
- Added YOLO preflight runtime diagnostics so operators can see dependency assumption state, model directory, `YOLO_CONFIG_DIR`, installed `torch`/`ultralytics` package versions and CUDA availability without loading a model, running inference or downloading weights.
|
||||
- Updated `.env.example`, `backend/README.md`, `frontend/README.md`, `scripts/README.md`, `docs/AI_PIPELINES.md`, `docs/TODO.md` and `CHANGELOG.md`.
|
||||
- Added regression coverage in `backend/tests/test_sprint116_operational_gis_map_workflow.py`, `backend/tests/test_sprint8b_yolo_foundation.py` and `backend/tests/test_docker_runtime_config.py`.
|
||||
|
||||
@@ -37,6 +38,18 @@ Validation:
|
||||
- Tower container check passed: `torch` imported as `2.12.1+cu130`, `torch.cuda.is_available()` returned `False`, `ultralytics` imported as `8.4.87`, and `scripts/yolo_preflight.py --enabled --json` returned `dependencies_available=true`, `status=not_configured`, `will_download_models=false`, `will_run_inference=false` because no local model path is configured yet.
|
||||
- Tower runtime `YOLO_CONFIG_DIR` is `/app/storage/ultralytics`; the directory exists, is writable and Ultralytics writes settings there instead of root config.
|
||||
- Internal browser validation passed against `http://192.168.10.150:1202`: the live shell and Map workspace rendered without console errors, with database layer selection, Operational GIS controls, and both full-run modes visible.
|
||||
- RED: `python -m pytest backend\tests\test_sprint13_yolo_preflight.py -q` failed before runtime diagnostics were implemented because `runtime` was absent from preflight output.
|
||||
- `python -m pytest backend\tests\test_sprint13_yolo_preflight.py -q` passed: 8 tests.
|
||||
- `python -m pytest backend\tests\test_sprint13_yolo_preflight.py backend\tests\test_sprint8b_yolo_foundation.py backend\tests\test_docker_runtime_config.py -q` passed: 39 tests.
|
||||
- `python -m compileall backend/app` passed.
|
||||
- `cd backend && python -m pytest -q` passed: 369 tests.
|
||||
- `cd frontend && npm run typecheck` passed.
|
||||
- `cd frontend && npm run build` passed.
|
||||
- `bash scripts/run_readiness_check.sh` passed: 369 backend tests plus frontend typecheck/build and Alembic head.
|
||||
- `cd backend && python -m alembic heads` passed: `202606120900 (head)`.
|
||||
- `cd backend && python -m alembic upgrade head --sql` passed.
|
||||
- `bash -n scripts/live_migration_smoke.sh`, `bash -n scripts/deploy_tower.sh`, `bash -n deploy/unraid/run-dockerman-container.sh` and `bash -n deploy/unraid/all-in-one-start.sh` passed.
|
||||
- Local Codex host still cannot run `docker compose config` because Docker is not installed in this Windows environment; Tower Docker validation is required after push/deploy.
|
||||
|
||||
Limitations:
|
||||
- `GEOINTEL_INSTALL_AI=true` installs optional PyTorch/Ultralytics dependencies but still requires a user-provided local model file; GeoIntel does not download weights.
|
||||
|
||||
Reference in New Issue
Block a user