197 lines
6.7 KiB
Bash
197 lines
6.7 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat >&2 <<'EOF'
|
|
Usage:
|
|
PROMOTION_POSITIVE_PORTFOLIO_PATH=artifacts/detection-quality-matrix/multi-sample/aoi1024-positive/multi_sample_quality_summary.json \
|
|
OPERATOR_SAMPLE_MANIFEST_PATH=storage/operator-data/operator_samples_manifest.json \
|
|
QUALITY_MODEL_ASSET_IDS="geointel-building-yolov8s-aoi1024bg512r3e50-pt" \
|
|
QUALITY_TILE_SIZES="512" \
|
|
QUALITY_TILE_OVERLAPS="64" \
|
|
QUALITY_THRESHOLDS="0.35 0.15" \
|
|
bash scripts/run_split_background_promotion_workflow.sh [--preflight-only] [base_url]
|
|
|
|
Optional environment:
|
|
BACKGROUND_SPLIT_OUTPUT_DIR Split matrix output directory.
|
|
PROMOTION_OUTPUT_DIR Promotion report output directory.
|
|
PROMOTION_POSITIVE_PORTFOLIO_PATH Positive AOI portfolio or multi-sample summary.
|
|
PROMOTION_MIN_POSITIVE_SAMPLES Default: 7.
|
|
PROMOTION_MIN_BACKGROUND_SAMPLES Default: 2.
|
|
PROMOTION_MIN_MEAN_F1 Default: 0.25.
|
|
PROMOTION_MAX_BACKGROUND_DETECTIONS_PER_SAMPLE Default: 0.
|
|
PROMOTION_DEFAULT_POSITIVE_TILE_SIZE Optional fallback for legacy positive portfolios.
|
|
PROMOTION_DEFAULT_POSITIVE_TILE_OVERLAP Optional fallback for legacy positive portfolios.
|
|
GE_INTEL_BASE_URL Default base URL when no argument is supplied.
|
|
|
|
Runs the split background matrix, then builds a split-aware promotion report.
|
|
The pure-empty background summary is the strict default gate. Sparse-context
|
|
background evidence is included for operator review only.
|
|
|
|
The script uses existing runtime/API flows and report tooling only. It does not
|
|
fetch providers, fetch weights, mutate model settings or activate a default.
|
|
EOF
|
|
}
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$ROOT"
|
|
|
|
BASE_URL="${GE_INTEL_BASE_URL:-http://localhost:1202}"
|
|
PREFLIGHT_ONLY=0
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--preflight-only)
|
|
PREFLIGHT_ONLY=1
|
|
;;
|
|
*)
|
|
BASE_URL="$1"
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [ -z "${PROMOTION_POSITIVE_PORTFOLIO_PATH:-}" ]; then
|
|
echo "PROMOTION_POSITIVE_PORTFOLIO_PATH is required" >&2
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -s "${PROMOTION_POSITIVE_PORTFOLIO_PATH}" ]; then
|
|
echo "Positive portfolio is not readable: ${PROMOTION_POSITIVE_PORTFOLIO_PATH}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "${OPERATOR_SAMPLE_MANIFEST_PATH:-}" ]; then
|
|
echo "OPERATOR_SAMPLE_MANIFEST_PATH is required" >&2
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
if [ ! -s "${OPERATOR_SAMPLE_MANIFEST_PATH}" ]; then
|
|
echo "Operator sample manifest is not readable: ${OPERATOR_SAMPLE_MANIFEST_PATH}" >&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 split-background promotion reporting" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v curl >/dev/null 2>&1; then
|
|
echo "curl is required for split-background promotion preflight" >&2
|
|
exit 1
|
|
fi
|
|
|
|
"${PYTHON_BIN}" - "${OPERATOR_SAMPLE_MANIFEST_PATH}" <<'PY'
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
manifest_path = Path(sys.argv[1])
|
|
payload = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
|
|
samples = payload.get("samples") or payload.get("items") or []
|
|
if not isinstance(samples, list) or not samples:
|
|
raise SystemExit(f"Operator sample manifest has no samples: {manifest_path}")
|
|
|
|
background_categories = set()
|
|
for sample in samples:
|
|
if str(sample.get("sample_role") or "") != "background_candidate":
|
|
continue
|
|
|
|
category = str(sample.get("background_category") or "").strip()
|
|
if not category:
|
|
try:
|
|
reference_feature_count = int(sample.get("reference_feature_count") or 0)
|
|
except (TypeError, ValueError):
|
|
reference_feature_count = 0
|
|
category = (
|
|
"pure_empty_negative"
|
|
if reference_feature_count == 0
|
|
else "sparse_building_context"
|
|
)
|
|
|
|
background_categories.add(category)
|
|
missing = {"pure_empty_negative", "sparse_building_context"} - background_categories
|
|
if missing:
|
|
raise SystemExit(
|
|
"Operator sample manifest is missing background categories "
|
|
f"{sorted(missing)}; found {sorted(background_categories)}"
|
|
)
|
|
PY
|
|
|
|
api_response="$(curl -fsS "${BASE_URL%/}/api/v1/projects" 2>/dev/null || true)"
|
|
if ! printf '%s' "${api_response}" | grep -q '"data"'; then
|
|
echo "Runtime API proxy is not reachable or did not return the canonical envelope: ${BASE_URL%/}/api/v1/projects" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "${PREFLIGHT_ONLY}" = "1" ]; then
|
|
echo "Split-background promotion preflight passed"
|
|
echo "Base URL: ${BASE_URL}"
|
|
echo "Positive portfolio: ${PROMOTION_POSITIVE_PORTFOLIO_PATH}"
|
|
echo "Operator sample manifest: ${OPERATOR_SAMPLE_MANIFEST_PATH}"
|
|
exit 0
|
|
fi
|
|
|
|
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
BACKGROUND_SPLIT_OUTPUT_DIR="${BACKGROUND_SPLIT_OUTPUT_DIR:-artifacts/detection-hard-negatives/background-split/${stamp}}"
|
|
PROMOTION_OUTPUT_DIR="${PROMOTION_OUTPUT_DIR:-artifacts/detection-model-promotion/split-aware/${stamp}}"
|
|
|
|
echo "== GeoIntel split-background promotion workflow =="
|
|
echo "Base URL: ${BASE_URL}"
|
|
echo "Positive portfolio: ${PROMOTION_POSITIVE_PORTFOLIO_PATH}"
|
|
echo "Background split output: ${BACKGROUND_SPLIT_OUTPUT_DIR}"
|
|
echo "Promotion output: ${PROMOTION_OUTPUT_DIR}"
|
|
|
|
BACKGROUND_SPLIT_OUTPUT_DIR="${BACKGROUND_SPLIT_OUTPUT_DIR}" \
|
|
bash scripts/run_background_corpus_split_matrix.sh "${BASE_URL}"
|
|
|
|
split_summary="${BACKGROUND_SPLIT_OUTPUT_DIR}/background_corpus_split_summary.json"
|
|
if [ ! -s "${split_summary}" ]; then
|
|
echo "Split summary was not produced: ${split_summary}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "${PROMOTION_OUTPUT_DIR}"
|
|
|
|
promotion_args=(
|
|
scripts/build_detection_model_promotion_report.py
|
|
--positive-portfolio "${PROMOTION_POSITIVE_PORTFOLIO_PATH}"
|
|
--background-split-summary "${split_summary}"
|
|
--output-dir "${PROMOTION_OUTPUT_DIR}"
|
|
--min-positive-samples "${PROMOTION_MIN_POSITIVE_SAMPLES:-7}"
|
|
--min-background-samples "${PROMOTION_MIN_BACKGROUND_SAMPLES:-2}"
|
|
--min-mean-f1 "${PROMOTION_MIN_MEAN_F1:-0.25}"
|
|
--max-background-detections-per-sample "${PROMOTION_MAX_BACKGROUND_DETECTIONS_PER_SAMPLE:-0}"
|
|
)
|
|
|
|
if [ -n "${PROMOTION_DEFAULT_POSITIVE_TILE_SIZE:-}" ]; then
|
|
promotion_args+=(--default-positive-tile-size "${PROMOTION_DEFAULT_POSITIVE_TILE_SIZE}")
|
|
fi
|
|
|
|
if [ -n "${PROMOTION_DEFAULT_POSITIVE_TILE_OVERLAP:-}" ]; then
|
|
promotion_args+=(--default-positive-tile-overlap "${PROMOTION_DEFAULT_POSITIVE_TILE_OVERLAP}")
|
|
fi
|
|
|
|
"${PYTHON_BIN}" "${promotion_args[@]}"
|
|
|
|
echo "Split summary: ${split_summary}"
|
|
echo "Promotion report: ${PROMOTION_OUTPUT_DIR}/detection_model_promotion_report.json"
|