Harden YOLO compatibility preflight
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-17 01:59:00 +02:00
parent 794f1cd51e
commit 3f1a686bef
11 changed files with 170 additions and 3 deletions
+9
View File
@@ -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
+25 -1
View File
@@ -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"
result["message"] = "Configured YOLO preflight passed. No model was loaded and no inference was run."
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
+7
View File
@@ -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