diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bfc968b..e70b2f73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ## Sprint 25 YOLO compatibility smoke hardening (2026-06-17) - Added an explicit `--check-model-load` mode to `scripts/yolo_preflight.py`. +- Added the same YOLO preflight entrypoint under `backend/scripts/` so it can run inside the backend Docker container. - 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. diff --git a/backend/README.md b/backend/README.md index 37c12543..44555962 100644 --- a/backend/README.md +++ b/backend/README.md @@ -224,6 +224,12 @@ Optional local model compatibility smoke: 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 ``` +In Docker, run the same smoke through the backend container: + +```bash +docker compose exec -T backend 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. diff --git a/backend/scripts/yolo_preflight.py b/backend/scripts/yolo_preflight.py new file mode 100644 index 00000000..ebf08fa2 --- /dev/null +++ b/backend/scripts/yolo_preflight.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + + +BACKEND_ROOT = Path(__file__).resolve().parents[1] +if str(BACKEND_ROOT) not in sys.path: + sys.path.insert(0, str(BACKEND_ROOT)) + +from app.core.config import Settings # noqa: E402 +from app.services.yolo_preflight_service import YoloPreflightService # noqa: E402 + + +def main() -> int: + parser = argparse.ArgumentParser(description="Run local YOLO configuration preflight without running inference.") + parser.add_argument("--model-path", help="Existing local YOLO model path.") + parser.add_argument("--tile-manifest-path", help="Existing raster tile manifest path.") + parser.add_argument("--enabled", action="store_true", help="Treat YOLO as enabled for this preflight.") + parser.add_argument("--max-tiles", type=int, default=100, help="Maximum tile count allowed by preflight.") + parser.add_argument( + "--assume-dependencies", + 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), + yolo_model_path=args.model_path, + yolo_max_tiles=args.max_tiles, + ) + payload = YoloPreflightService.run( + settings=settings, + tile_manifest_path=args.tile_manifest_path, + assume_dependencies=args.assume_dependencies, + check_model_load=args.check_model_load, + ) + + if args.json: + print(json.dumps(payload, indent=2, sort_keys=True)) + else: + print("GeoIntel YOLO preflight") + print(json.dumps(payload, indent=2, sort_keys=True)) + return 0 if payload["status"] in {"ready", "not_configured", "dependency_unavailable"} else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/backend/tests/test_readiness_gate.py b/backend/tests/test_readiness_gate.py index d49331e4..f56a0542 100644 --- a/backend/tests/test_readiness_gate.py +++ b/backend/tests/test_readiness_gate.py @@ -35,6 +35,7 @@ def test_readiness_gate_compiles_yolo_preflight_script() -> None: content = script.read_text(encoding="utf-8") assert "-m py_compile scripts/yolo_preflight.py" in content + assert "-m py_compile backend/scripts/yolo_preflight.py" in content def test_demo_export_workflow_script_verifies_export_endpoints() -> None: diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 71050c52..0d565728 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -1,12 +1,12 @@ ## Sprint 25 YOLO compatibility smoke hardening (2026-06-17) Changed: -- Added explicit `--check-model-load` support to `scripts/yolo_preflight.py` and `YoloPreflightService`. +- Added explicit `--check-model-load` support to `scripts/yolo_preflight.py`, `backend/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. +- Added Python compile validation for both YOLO preflight entrypoints to the readiness gate. +- Updated AI pipeline, scripts, backend, TODO and changelog docs, including Docker runtime usage. Validation: - `python -m py_compile scripts/yolo_preflight.py` passed. diff --git a/scripts/README.md b/scripts/README.md index 4d5c38b4..cb31813f 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -29,6 +29,12 @@ Verify a configured local YOLO model without running inference: python scripts/yolo_preflight.py --model-path /absolute/path/to/model.pt --tile-manifest-path /absolute/path/to/manifest.json --check-model-load --json ``` +Against the Docker runtime: + +```bash +docker compose exec -T backend 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. diff --git a/scripts/run_readiness_check.sh b/scripts/run_readiness_check.sh index bf26a7f6..f16a2923 100755 --- a/scripts/run_readiness_check.sh +++ b/scripts/run_readiness_check.sh @@ -38,6 +38,7 @@ ${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 backend/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 diff --git a/scripts/yolo_preflight.py b/scripts/yolo_preflight.py index 69f019ac..780846be 100644 --- a/scripts/yolo_preflight.py +++ b/scripts/yolo_preflight.py @@ -1,59 +1,22 @@ from __future__ import annotations -import argparse -import json -import sys +import importlib.util from pathlib import Path +from types import ModuleType -ROOT = Path(__file__).resolve().parents[1] -BACKEND_ROOT = ROOT / "backend" -if str(BACKEND_ROOT) not in sys.path: - sys.path.insert(0, str(BACKEND_ROOT)) - -from app.core.config import Settings # noqa: E402 -from app.services.yolo_preflight_service import YoloPreflightService # noqa: E402 +def load_backend_script() -> ModuleType: + script = Path(__file__).resolve().parents[1] / "backend" / "scripts" / "yolo_preflight.py" + spec = importlib.util.spec_from_file_location("backend_yolo_preflight", script) + if spec is None or spec.loader is None: + raise RuntimeError(f"Could not load YOLO preflight implementation from {script}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module -def main() -> int: - parser = argparse.ArgumentParser(description="Run local YOLO configuration preflight without loading a model.") - parser.add_argument("--model-path", help="Existing local YOLO model path.") - parser.add_argument("--tile-manifest-path", help="Existing raster tile manifest path.") - parser.add_argument("--enabled", action="store_true", help="Treat YOLO as enabled for this preflight.") - parser.add_argument("--max-tiles", type=int, default=100, help="Maximum tile count allowed by preflight.") - parser.add_argument( - "--assume-dependencies", - 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), - yolo_model_path=args.model_path, - yolo_max_tiles=args.max_tiles, - ) - payload = YoloPreflightService.run( - settings=settings, - tile_manifest_path=args.tile_manifest_path, - assume_dependencies=args.assume_dependencies, - check_model_load=args.check_model_load, - ) - - if args.json: - print(json.dumps(payload, indent=2, sort_keys=True)) - else: - print("GeoIntel YOLO preflight") - print(json.dumps(payload, indent=2, sort_keys=True)) - return 0 if payload["status"] in {"ready", "not_configured", "dependency_unavailable"} else 1 +_impl = load_backend_script() +main = _impl.main if __name__ == "__main__":