Files
geointel/scripts/verify_model_asset_detection_workflow.sh
T
Codex b2fe7fa8bb
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled
Add model asset detection workflow smoke
2026-07-06 23:55:58 +02:00

259 lines
9.7 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${1:-${GE_INTEL_BASE_URL:-http://localhost:1202}}"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "${TMP_DIR}"' EXIT
if ! command -v curl >/dev/null 2>&1; then
echo "curl is required for model asset detection workflow verification" >&2
exit 1
fi
if [ -n "${PYTHON_BIN:-}" ]; then
PYTHON_BIN="${PYTHON_BIN}"
else
PYTHON_BIN=""
for candidate in python3 python.exe python; do
if command -v "${candidate}" >/dev/null 2>&1 && "${candidate}" -c "import json, sys" >/dev/null 2>&1; then
PYTHON_BIN="${candidate}"
break
fi
done
fi
if [ -z "${PYTHON_BIN}" ]; then
echo "A Python interpreter is required for JSON parsing" >&2
exit 1
fi
json_field() {
local file_path="$1"
local expression="$2"
"${PYTHON_BIN}" - "$file_path" "$expression" <<'PY'
import json
import sys
path, expression = sys.argv[1], sys.argv[2]
with open(path, "r", encoding="utf-8") as handle:
payload = json.load(handle)
value = payload
for part in expression.split("."):
if part:
value = value[part]
print(value)
PY
}
require_json_data() {
local file_path="$1"
"${PYTHON_BIN}" - "$file_path" <<'PY'
import json
import sys
with open(sys.argv[1], "r", encoding="utf-8") as handle:
payload = json.load(handle)
if "data" not in payload:
raise SystemExit("Response is not a canonical GeoIntel data envelope")
PY
}
echo "== GeoIntel model asset detection workflow verification =="
echo "Base URL: ${BASE_URL}"
curl -fsS -X POST "${BASE_URL%/}/api/v1/demo/workflow" > "${TMP_DIR}/demo.json"
require_json_data "${TMP_DIR}/demo.json"
project_id="$(json_field "${TMP_DIR}/demo.json" "data.project_id")"
raster_dataset_id="$(json_field "${TMP_DIR}/demo.json" "data.raster_dataset_id")"
if [ -z "${project_id}" ] || [ "${project_id}" = "None" ] || [ "${project_id}" = "null" ]; then
echo "Demo workflow did not return a project_id" >&2
exit 1
fi
if [ -z "${raster_dataset_id}" ] || [ "${raster_dataset_id}" = "None" ] || [ "${raster_dataset_id}" = "null" ]; then
echo "Demo workflow did not return a raster_dataset_id" >&2
exit 1
fi
curl -fsS -X POST "${BASE_URL%/}/api/v1/projects/${project_id}/datasets/${raster_dataset_id}/raster/tile" \
-H "Content-Type: application/json" \
-d '{"tile_size":64,"overlap":0,"output_name":"model_asset_detection_smoke_tiles"}' > "${TMP_DIR}/tile.json"
require_json_data "${TMP_DIR}/tile.json"
manifest_path="$(json_field "${TMP_DIR}/tile.json" "data.result_json.manifest_path")"
if [ -z "${manifest_path}" ] || [ "${manifest_path}" = "None" ] || [ "${manifest_path}" = "null" ]; then
echo "Raster tile response did not include a manifest_path" >&2
exit 1
fi
curl -fsS "${BASE_URL%/}/api/v1/detection/model-assets" > "${TMP_DIR}/model_assets.json"
require_json_data "${TMP_DIR}/model_assets.json"
model_asset_id="$("${PYTHON_BIN}" - "${TMP_DIR}/model_assets.json" <<'PY'
import json
import sys
with open(sys.argv[1], "r", encoding="utf-8") as handle:
data = json.load(handle)["data"]
items = data.get("items") or []
if not items:
raise SystemExit("No local model assets are available. Mount a local .pt/.onnx/.engine file before running this smoke.")
for item in items:
if item.get("will_download_models") is not False:
raise SystemExit("Model asset catalog must never report automatic model downloads")
selected = next((item for item in items if item.get("active")), items[0])
print(selected["model_asset_id"])
PY
)"
curl -fsS -G "${BASE_URL%/}/api/v1/detection/yolo/preflight" \
--data-urlencode "tile_manifest_path=${manifest_path}" \
--data-urlencode "model_asset_id=${model_asset_id}" > "${TMP_DIR}/preflight.json"
require_json_data "${TMP_DIR}/preflight.json"
"${PYTHON_BIN}" - "${TMP_DIR}/preflight.json" "${model_asset_id}" <<'PY'
import json
import sys
path, expected_model_asset_id = sys.argv[1], sys.argv[2]
with open(path, "r", encoding="utf-8") as handle:
data = json.load(handle)["data"]
if data.get("model_asset_id") != expected_model_asset_id:
raise SystemExit("YOLO preflight did not use the selected model_asset_id")
if data.get("will_download_models") is not False:
raise SystemExit("YOLO preflight must never download model weights")
if data.get("will_run_inference") is not False:
raise SystemExit("YOLO preflight must remain read-only")
if data.get("status") != "ready":
raise SystemExit(f"YOLO preflight is not ready: {data.get('status')} {data.get('message')}")
checks = data.get("checks") or {}
if checks.get("manifest_valid") is not True:
raise SystemExit("YOLO preflight did not validate the raster tile manifest")
if checks.get("model_file_exists") is not True:
raise SystemExit("YOLO preflight did not confirm the local model file")
PY
"${PYTHON_BIN}" - "${TMP_DIR}/run_request.json" "${project_id}" "${raster_dataset_id}" "${model_asset_id}" "${manifest_path}" <<'PY'
import json
import sys
path, project_id, dataset_id, model_asset_id, tile_manifest_path = sys.argv[1:6]
payload = {
"project_id": project_id,
"dataset_id": dataset_id,
"model_id": "yolo-configured",
"model_asset_id": model_asset_id,
"confidence_threshold": 0.5,
"tile_manifest_path": tile_manifest_path,
"parameters_json": {},
}
with open(path, "w", encoding="utf-8") as handle:
json.dump(payload, handle)
PY
curl -fsS -X POST "${BASE_URL%/}/api/v1/detection/run" \
-H "Content-Type: application/json" \
--data-binary "@${TMP_DIR}/run_request.json" > "${TMP_DIR}/detection_run.json"
require_json_data "${TMP_DIR}/detection_run.json"
"${PYTHON_BIN}" - "${TMP_DIR}/detection_run.json" "${model_asset_id}" <<'PY'
import json
import sys
path, expected_model_asset_id = sys.argv[1], sys.argv[2]
with open(path, "r", encoding="utf-8") as handle:
data = json.load(handle)["data"]
if data.get("model_id") != "yolo-configured":
raise SystemExit("Detection run did not use yolo-configured")
if not data.get("analysis_run_id") or not data.get("job_id"):
raise SystemExit("Detection run did not return persisted run/job ids")
if data.get("status") != "success":
raise SystemExit(f"Detection run failed: {data.get('error_code')} {data.get('message')}")
if int(data.get("detection_count") or 0) < 0:
raise SystemExit("Detection count cannot be negative")
PY
analysis_run_id="$(json_field "${TMP_DIR}/detection_run.json" "data.analysis_run_id")"
detection_count="$(json_field "${TMP_DIR}/detection_run.json" "data.detection_count")"
curl -fsS "${BASE_URL%/}/api/v1/detection/runs/${analysis_run_id}" > "${TMP_DIR}/run_detail.json"
require_json_data "${TMP_DIR}/run_detail.json"
"${PYTHON_BIN}" - "${TMP_DIR}/run_detail.json" "${analysis_run_id}" "${model_asset_id}" "${manifest_path}" <<'PY'
import json
import sys
path, analysis_run_id, model_asset_id, tile_manifest_path = sys.argv[1:5]
with open(path, "r", encoding="utf-8") as handle:
data = json.load(handle)["data"]
if data.get("id") != analysis_run_id:
raise SystemExit("Detection run detail returned the wrong run id")
if data.get("status") != "success":
raise SystemExit(f"Detection run detail is not successful: {data.get('status')}")
parameters = data.get("parameters_json") or {}
if parameters.get("model_asset_id") != model_asset_id:
raise SystemExit("Persisted run parameters lost model_asset_id provenance")
if parameters.get("tile_manifest_path") != tile_manifest_path:
raise SystemExit("Persisted run parameters lost tile_manifest_path provenance")
PY
curl -fsS "${BASE_URL%/}/api/v1/detection/runs/${analysis_run_id}/detections" > "${TMP_DIR}/detections.json"
require_json_data "${TMP_DIR}/detections.json"
"${PYTHON_BIN}" - "${TMP_DIR}/detections.json" "${detection_count}" <<'PY'
import json
import sys
path, expected_count = sys.argv[1], int(sys.argv[2])
with open(path, "r", encoding="utf-8") as handle:
data = json.load(handle)["data"]
items = data.get("items") or []
if int(data.get("total") or 0) != expected_count:
raise SystemExit("Detection list total does not match run detection_count")
if len(items) != expected_count:
raise SystemExit("Detection list item count does not match run detection_count")
for item in items:
if not item.get("geometry"):
raise SystemExit("Persisted detection is missing geometry")
if not item.get("source_tile_path"):
raise SystemExit("Persisted YOLO detection is missing source_tile_path provenance")
PY
curl -fsS "${BASE_URL%/}/api/v1/detection/runs/${analysis_run_id}/geojson" > "${TMP_DIR}/geojson.json"
require_json_data "${TMP_DIR}/geojson.json"
"${PYTHON_BIN}" - "${TMP_DIR}/geojson.json" "${detection_count}" <<'PY'
import json
import sys
path, expected_count = sys.argv[1], int(sys.argv[2])
with open(path, "r", encoding="utf-8") as handle:
data = json.load(handle)["data"]
if data.get("type") != "FeatureCollection":
raise SystemExit("Detection GeoJSON response is not a FeatureCollection")
features = data.get("features") or []
if len(features) != expected_count:
raise SystemExit("Detection GeoJSON feature count does not match run detection_count")
required_properties = {
"detection_id",
"class_name",
"confidence",
"model_name",
"model_version",
"analysis_run_id",
"dataset_id",
"job_id",
"source_tile_path",
"bbox_json",
}
for feature in features:
properties = feature.get("properties") or {}
missing = sorted(required_properties - set(properties))
if missing:
raise SystemExit(f"Detection GeoJSON feature is missing properties: {missing}")
if not feature.get("geometry"):
raise SystemExit("Detection GeoJSON feature is missing geometry")
PY
echo "Model asset detection workflow verification passed"
echo "Project: ${project_id}"
echo "Raster dataset: ${raster_dataset_id}"
echo "Model asset: ${model_asset_id}"
echo "Manifest: ${manifest_path}"
echo "Analysis run: ${analysis_run_id}"
echo "Detections: ${detection_count}"