259 lines
9.7 KiB
Bash
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}"
|