Add YOLO preflight runtime diagnostics
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-05 21:17:20 +02:00
parent ad4fa45584
commit 7a29e7867d
6 changed files with 87 additions and 2 deletions
+1
View File
@@ -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
View File
@@ -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
+28 -1
View File
@@ -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:
+4
View File
@@ -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:
+13
View File
@@ -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.