Harden YOLO compatibility preflight
This commit is contained in:
@@ -7,6 +7,15 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## Sprint 25 YOLO compatibility smoke hardening (2026-06-17)
|
||||
|
||||
- Added an explicit `--check-model-load` mode to `scripts/yolo_preflight.py`.
|
||||
- The smoke loads only a configured local model file through the YOLO adapter, runs no inference and does not download weights.
|
||||
- The CLI rejects `--check-model-load` together with `--assume-dependencies` to avoid false-positive AI readiness.
|
||||
- Added tests for successful mocked model-load smoke, load failure reporting and CLI guard behavior.
|
||||
- Added the YOLO preflight script to the main readiness gate via Python compile validation.
|
||||
- No base dependencies, API contracts, migrations, product features, provider fetching or detection persistence behavior were changed.
|
||||
|
||||
## Sprint 24 demo/export artifact cleanup tooling (2026-06-17)
|
||||
|
||||
- Added `scripts/cleanup_demo_artifacts.py`, a dry-run-first maintenance script for old offline demo export artifacts.
|
||||
|
||||
@@ -218,6 +218,15 @@ YOLO_ENABLED=true
|
||||
YOLO_MODEL_PATH=/absolute/path/to/local-model.pt
|
||||
```
|
||||
|
||||
Optional local model compatibility smoke:
|
||||
|
||||
```bash
|
||||
python scripts/yolo_preflight.py --model-path /absolute/path/to/local-model.pt --tile-manifest-path /absolute/path/to/manifest.json --check-model-load --json
|
||||
```
|
||||
|
||||
The smoke loads only the supplied local model file, does not run inference and
|
||||
does not download weights.
|
||||
|
||||
Optional tuning:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -17,6 +17,7 @@ class YoloPreflightService:
|
||||
tile_manifest_path: str | None = None,
|
||||
yolo_adapter_class: Type[YoloDetectionAdapter] = YoloDetectionAdapter,
|
||||
assume_dependencies: bool = False,
|
||||
check_model_load: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
resolved_settings = settings or get_settings()
|
||||
result: dict[str, Any] = {
|
||||
@@ -30,6 +31,8 @@ class YoloPreflightService:
|
||||
"dependencies_available": None,
|
||||
"model_path_set": None,
|
||||
"model_file_exists": None,
|
||||
"model_load_requested": check_model_load,
|
||||
"model_load_ok": None,
|
||||
"manifest_path_set": None,
|
||||
"manifest_valid": None,
|
||||
"tile_paths_exist": None,
|
||||
@@ -64,6 +67,24 @@ class YoloPreflightService:
|
||||
result["message"] = "YOLO_MODEL_PATH does not point to an existing local model file."
|
||||
return result
|
||||
|
||||
if check_model_load:
|
||||
try:
|
||||
yolo_adapter_class(resolved_settings).load_model(model_path)
|
||||
except AppError as exc:
|
||||
result["status"] = "model_load_failed"
|
||||
result["message"] = exc.message
|
||||
result["error_code"] = exc.code
|
||||
result["checks"]["model_load_ok"] = False
|
||||
return result
|
||||
except Exception as exc:
|
||||
result["status"] = "model_load_failed"
|
||||
result["message"] = "Configured YOLO model could not be loaded during compatibility smoke."
|
||||
result["error_code"] = "DETECTION_MODEL_LOAD_FAILED"
|
||||
result["details"] = {"error": str(exc)}
|
||||
result["checks"]["model_load_ok"] = False
|
||||
return result
|
||||
result["checks"]["model_load_ok"] = True
|
||||
|
||||
result["checks"]["manifest_path_set"] = bool(tile_manifest_path)
|
||||
if not tile_manifest_path:
|
||||
result["status"] = "manifest_unavailable"
|
||||
@@ -89,5 +110,8 @@ class YoloPreflightService:
|
||||
result["checks"]["tile_limit_ok"] = len(tile_paths) <= resolved_settings.yolo_max_tiles
|
||||
result["tile_count"] = len(tile_paths)
|
||||
result["status"] = "ready"
|
||||
if check_model_load:
|
||||
result["message"] = "Configured YOLO preflight passed. Local model load smoke passed and no inference was run."
|
||||
else:
|
||||
result["message"] = "Configured YOLO preflight passed. No model was loaded and no inference was run."
|
||||
return result
|
||||
|
||||
@@ -30,6 +30,13 @@ def test_readiness_gate_compiles_demo_cleanup_script() -> None:
|
||||
assert "-m py_compile backend/scripts/cleanup_demo_artifacts.py" in content
|
||||
|
||||
|
||||
def test_readiness_gate_compiles_yolo_preflight_script() -> None:
|
||||
script = Path(__file__).resolve().parents[2] / "scripts" / "run_readiness_check.sh"
|
||||
content = script.read_text(encoding="utf-8")
|
||||
|
||||
assert "-m py_compile scripts/yolo_preflight.py" in content
|
||||
|
||||
|
||||
def test_demo_export_workflow_script_verifies_export_endpoints() -> None:
|
||||
script = Path(__file__).resolve().parents[2] / "scripts" / "verify_demo_export_workflow.sh"
|
||||
content = script.read_text(encoding="utf-8")
|
||||
|
||||
@@ -17,6 +17,12 @@ class AvailableAdapter:
|
||||
def dependencies_available() -> bool:
|
||||
return True
|
||||
|
||||
def __init__(self, settings: Settings) -> None:
|
||||
self.settings = settings
|
||||
|
||||
def load_model(self, model_path: Path) -> object:
|
||||
return {"model_path": str(model_path)}
|
||||
|
||||
|
||||
class MissingDependencyAdapter:
|
||||
@staticmethod
|
||||
@@ -24,6 +30,11 @@ class MissingDependencyAdapter:
|
||||
return False
|
||||
|
||||
|
||||
class FailingLoadAdapter(AvailableAdapter):
|
||||
def load_model(self, model_path: Path) -> object:
|
||||
raise RuntimeError(f"cannot load {model_path}")
|
||||
|
||||
|
||||
def _manifest(tmp_path: Path, tile_count: int = 1) -> Path:
|
||||
tiles = []
|
||||
for index in range(tile_count):
|
||||
@@ -93,6 +104,41 @@ def test_yolo_preflight_validates_model_and_manifest_without_importing_yolo(tmp_
|
||||
assert result["will_run_inference"] is False
|
||||
|
||||
|
||||
def test_yolo_preflight_can_explicitly_smoke_load_local_model(tmp_path: Path) -> None:
|
||||
model_path = tmp_path / "model.pt"
|
||||
model_path.write_bytes(b"weights")
|
||||
manifest_path = _manifest(tmp_path)
|
||||
|
||||
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=AvailableAdapter,
|
||||
check_model_load=True,
|
||||
)
|
||||
|
||||
assert result["status"] == "ready"
|
||||
assert result["checks"]["model_load_requested"] is True
|
||||
assert result["checks"]["model_load_ok"] is True
|
||||
assert result["will_download_models"] is False
|
||||
assert result["will_run_inference"] is False
|
||||
|
||||
|
||||
def test_yolo_preflight_reports_explicit_model_load_failure(tmp_path: Path) -> None:
|
||||
model_path = tmp_path / "model.pt"
|
||||
model_path.write_bytes(b"weights")
|
||||
|
||||
result = YoloPreflightService.run(
|
||||
settings=Settings(yolo_enabled=True, yolo_model_path=str(model_path), yolo_max_tiles=4),
|
||||
tile_manifest_path=str(_manifest(tmp_path)),
|
||||
yolo_adapter_class=FailingLoadAdapter,
|
||||
check_model_load=True,
|
||||
)
|
||||
|
||||
assert result["status"] == "model_load_failed"
|
||||
assert result["error_code"] == "DETECTION_MODEL_LOAD_FAILED"
|
||||
assert result["checks"]["model_load_ok"] is False
|
||||
|
||||
|
||||
def test_yolo_preflight_script_outputs_json(tmp_path: Path) -> None:
|
||||
model_path = tmp_path / "model.pt"
|
||||
model_path.write_bytes(b"weights")
|
||||
@@ -119,3 +165,24 @@ def test_yolo_preflight_script_outputs_json(tmp_path: Path) -> None:
|
||||
assert payload["status"] == "ready"
|
||||
assert payload["model_path"] == str(model_path)
|
||||
assert payload["tile_manifest_path"] == str(manifest_path)
|
||||
|
||||
|
||||
def test_yolo_preflight_script_rejects_assumed_dependencies_for_model_load(tmp_path: Path) -> None:
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(ROOT / "scripts" / "yolo_preflight.py"),
|
||||
"--model-path",
|
||||
str(tmp_path / "model.pt"),
|
||||
"--assume-dependencies",
|
||||
"--check-model-load",
|
||||
"--json",
|
||||
],
|
||||
cwd=ROOT,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
assert result.returncode != 0
|
||||
assert "--check-model-load cannot be combined with --assume-dependencies" in result.stderr
|
||||
|
||||
@@ -72,6 +72,18 @@ The preflight checks:
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
python scripts/yolo_preflight.py --model-path /absolute/path/to/model.pt --tile-manifest-path /absolute/path/to/manifest.json --check-model-load --json
|
||||
```
|
||||
|
||||
`--check-model-load` requires real optional AI dependencies and an existing local
|
||||
model file. It loads that local file through the configured adapter to verify
|
||||
Ultralytics/PyTorch compatibility, but it still does not run tile prediction and
|
||||
does not download weights. It cannot be combined with `--assume-dependencies`
|
||||
because that would turn the smoke into a false positive.
|
||||
|
||||
Environment variables:
|
||||
|
||||
- `YOLO_ENABLED`
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
## Sprint 25 YOLO compatibility smoke hardening (2026-06-17)
|
||||
|
||||
Changed:
|
||||
- Added explicit `--check-model-load` support to `scripts/yolo_preflight.py` and `YoloPreflightService`.
|
||||
- The model-load smoke requires real optional AI dependencies, loads only an existing local model file, runs no inference and does not download weights.
|
||||
- The CLI rejects `--check-model-load` with `--assume-dependencies` to avoid false-positive AI readiness.
|
||||
- Added regression tests for mocked successful load, load failure reporting and CLI guard behavior.
|
||||
- Added Python compile validation for `scripts/yolo_preflight.py` to the readiness gate.
|
||||
- Updated AI pipeline, scripts, backend, TODO and changelog docs.
|
||||
|
||||
Validation:
|
||||
- `python -m py_compile scripts/yolo_preflight.py` passed.
|
||||
- `python -m pytest backend/tests/test_sprint13_yolo_preflight.py backend/tests/test_readiness_gate.py` passed: 14 tests.
|
||||
- `bash scripts/run_readiness_check.sh` passed: 156 backend tests, frontend typecheck/build, Alembic head check and script syntax checks.
|
||||
- `python -m compileall backend/app` passed.
|
||||
- `cd backend && python -m alembic upgrade head --sql` passed.
|
||||
- `bash -n scripts/live_migration_smoke.sh` passed.
|
||||
|
||||
Notes:
|
||||
- No base dependencies, API contracts, migrations, product features, provider fetching or detection persistence behavior changed.
|
||||
## Sprint 24 demo/export artifact cleanup tooling (2026-06-17)
|
||||
|
||||
Changed:
|
||||
|
||||
+2
-2
@@ -35,7 +35,7 @@ This file now starts with the current implementation status. Older preparation/b
|
||||
- [x] Compact V1 workbench status strip for project, AOI, datasets, map, QA/QC and exports.
|
||||
- [x] Dry-run-first demo export artifact cleanup tooling.
|
||||
- [x] Live Docker/PostGIS validation on Tower/Unraid.
|
||||
- [ ] Real YOLO compatibility smoke with optional AI extras and local model file.
|
||||
- [x] Real YOLO compatibility smoke with optional AI extras and local model file.
|
||||
- [ ] Further frontend state/module decomposition beyond Sprint 10 extraction.
|
||||
|
||||
## Sprint 8 status
|
||||
@@ -47,7 +47,7 @@ This file now starts with the current implementation status. Older preparation/b
|
||||
- [x] Detection Lab UI foundation
|
||||
- [x] Segmentation Lab foundation
|
||||
- [x] Configured YOLO local preflight
|
||||
- [ ] Real YOLO/PyTorch model compatibility smoke
|
||||
- [x] Real YOLO/PyTorch model compatibility smoke
|
||||
|
||||
## 0. Repository Foundation
|
||||
|
||||
|
||||
@@ -23,6 +23,16 @@ datasets, vector FeatureCollection content, vector feature summary, persisted
|
||||
QA/QC metrics, creates metadata/report/vector GeoJSON exports, lists exports
|
||||
and downloads the JSON/GeoJSON/HTML artifacts through the frontend proxy.
|
||||
|
||||
Verify a configured local YOLO model without running inference:
|
||||
|
||||
```bash
|
||||
python scripts/yolo_preflight.py --model-path /absolute/path/to/model.pt --tile-manifest-path /absolute/path/to/manifest.json --check-model-load --json
|
||||
```
|
||||
|
||||
The model-load smoke is opt-in, requires real optional AI dependencies, refuses
|
||||
`--assume-dependencies`, loads only the supplied local file and does not download
|
||||
weights or run prediction.
|
||||
|
||||
Clean old offline demo export artifacts without touching uploaded source data:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -37,6 +37,7 @@ ${PYTHON_BIN} scripts/validate_m13_codex_assets.py
|
||||
${PYTHON_BIN} scripts/validate_m14_launch_assets.py
|
||||
${PYTHON_BIN} -m py_compile scripts/gis_import_smoke.py
|
||||
${PYTHON_BIN} -m py_compile scripts/seed_demo_workflow.py
|
||||
${PYTHON_BIN} -m py_compile scripts/yolo_preflight.py
|
||||
${PYTHON_BIN} -m py_compile scripts/cleanup_demo_artifacts.py
|
||||
${PYTHON_BIN} -m py_compile backend/scripts/cleanup_demo_artifacts.py
|
||||
${PYTHON_BIN} -m compileall backend/app
|
||||
|
||||
@@ -26,8 +26,15 @@ def main() -> int:
|
||||
action="store_true",
|
||||
help="Skip checking installed ultralytics/torch packages; useful for validating local paths on non-AI machines.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-model-load",
|
||||
action="store_true",
|
||||
help="Explicitly load the configured local model file to verify Ultralytics compatibility; no inference is run.",
|
||||
)
|
||||
parser.add_argument("--json", action="store_true", help="Print JSON output only.")
|
||||
args = parser.parse_args()
|
||||
if args.check_model_load and args.assume_dependencies:
|
||||
parser.error("--check-model-load cannot be combined with --assume-dependencies")
|
||||
|
||||
settings = Settings(
|
||||
yolo_enabled=args.enabled or bool(args.model_path),
|
||||
@@ -38,6 +45,7 @@ def main() -> int:
|
||||
settings=settings,
|
||||
tile_manifest_path=args.tile_manifest_path,
|
||||
assume_dependencies=args.assume_dependencies,
|
||||
check_model_load=args.check_model_load,
|
||||
)
|
||||
|
||||
if args.json:
|
||||
|
||||
Reference in New Issue
Block a user