Add guarded promoted YOLO activation
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-11 10:45:06 +02:00
parent 340be960e9
commit b1a4074cc8
13 changed files with 550 additions and 23 deletions
+17 -2
View File
@@ -275,8 +275,23 @@ python scripts/configure_yolo_model.py \
--apply
```
The smoke loads only the supplied local model file, does not run inference and
does not download weights.
When a promotion report recommends an exact model/tile/threshold candidate,
prefer the guarded activation helper. It validates the report, checks the local
model asset and writes `.env` only when `--apply` is supplied:
```bash
python scripts/activate_promoted_yolo_candidate.py \
--promotion-report /mnt/user/appdata/geointel/artifacts/detection-model-promotion/split-aware/aoi1024bg512r3e50-high-threshold-split-20260710T222934Z/detection_model_promotion_report.json \
--candidate-key 'geointel-building-yolov8s-aoi1024bg512r3e50-pt|512|64|0.35' \
--models-dir /mnt/user/appdata/geointel/models \
--env-file /mnt/user/appdata/geointel/.env \
--json
```
Add `--apply` only after reviewing the emitted env updates. The smoke and
activation helpers load no model by default, run no inference and do not
download weights. Restart or rebuild the runtime after applying because the
active model is read from `YOLO_MODEL_PATH`.
Operator-only local training preparation is available when real public model
candidates are too weak for the target imagery. It is not a browser feature and
@@ -4,7 +4,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_detection_operator_profiles_define_explicit_non_default_yolo_candidates() -> None:
def test_detection_operator_profiles_define_explicit_yolo_candidates_and_promoted_profile() -> None:
profiles = ROOT / "frontend" / "src" / "components" / "detection" / "detectionProfiles.ts"
source = profiles.read_text(encoding="utf-8")
@@ -15,7 +15,10 @@ def test_detection_operator_profiles_define_explicit_non_default_yolo_candidates
assert "confidenceThreshold: 0.15" in source
assert "confidenceThreshold: 0.35" in source
assert "defaultApproved: false" in source
assert "defaultApproved: true" in source
assert "promotionRecommendation: 'none'" in source
assert "promotionRecommendation: 'promote_candidate'" in source
assert "pure-empty gate passed" in source
assert "false-positive pressure" in source
@@ -29,6 +32,7 @@ def test_detection_lab_surfaces_profiles_as_deliberate_operator_actions() -> Non
assert "profile.displayName" in lab
assert "profile.confidenceThreshold" in lab
assert "Candidate only - not default-approved" in lab
assert "default-approved" in lab
assert "Apply profile" in lab
assert "onApplyOperatorProfile(profile)" in lab
assert "Recommended starting threshold: 0.25" not in lab
@@ -0,0 +1,151 @@
from __future__ import annotations
import json
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "scripts" / "activate_promoted_yolo_candidate.py"
CANDIDATE_KEY = "geointel-building-yolov8s-aoi1024bg512r3e50-pt|512|64|0.35"
def _write_model(models_dir: Path) -> Path:
model_file = models_dir / "geointel-building-yolov8s-aoi1024bg512r3e50.pt"
model_file.parent.mkdir(parents=True)
model_file.write_bytes(b"local promoted model")
return model_file
def _write_report(path: Path, *, promotion_status: str = "promote_candidate") -> None:
rejection_reasons = [] if promotion_status == "promote_candidate" else ["background_false_positive_pressure"]
path.write_text(
json.dumps(
{
"gates": {
"max_background_detections_per_sample": 0,
"min_background_samples": 2,
"min_mean_f1": 0.25,
"min_positive_samples": 7,
},
"recommended_candidate": {
"background_sample_count": 3,
"background_samples": [["arendonk_heide", 0], ["lommel_heide", 0], ["postel_bos", 0]],
"candidate_key": CANDIDATE_KEY,
"max_background_detections": 0,
"mean_f1": 0.32086574003576274,
"mean_precision": 0.8400057773951873,
"mean_recall": 0.20213514285308795,
"model_asset_id": "geointel-building-yolov8s-aoi1024bg512r3e50-pt",
"positive_sample_count": 7,
"promotion_status": promotion_status,
"rejection_reasons": rejection_reasons,
"threshold": 0.35,
"tile_overlap": 64,
"tile_size": 512,
"total_background_detections": 0,
},
}
),
encoding="utf-8",
)
def _run_activation(tmp_path: Path, *extra_args: str) -> subprocess.CompletedProcess[str]:
models_dir = tmp_path / "models"
_write_model(models_dir)
report_path = tmp_path / "promotion_report.json"
_write_report(report_path)
env_file = tmp_path / ".env"
env_file.write_text("GEOINTEL_ENV=production\nYOLO_ENABLED=false\n", encoding="utf-8")
return subprocess.run(
[
"python",
str(SCRIPT),
"--promotion-report",
str(report_path),
"--candidate-key",
CANDIDATE_KEY,
"--models-dir",
str(models_dir),
"--container-model-dir",
"/app/models",
"--env-file",
str(env_file),
"--json",
*extra_args,
],
cwd=ROOT,
capture_output=True,
text=True,
timeout=30,
check=False,
)
def test_promoted_yolo_activation_dry_run_validates_report_and_model(tmp_path: Path) -> None:
result = _run_activation(tmp_path)
assert result.returncode == 0, result.stderr
payload = json.loads(result.stdout)
assert payload["status"] == "ready_to_apply"
assert payload["applied"] is False
assert payload["will_download_models"] is False
assert payload["candidate"]["candidate_key"] == CANDIDATE_KEY
assert payload["candidate"]["threshold"] == 0.35
assert payload["env_updates"]["YOLO_ENABLED"] == "true"
assert payload["env_updates"]["YOLO_MODEL_PATH"] == "/app/models/geointel-building-yolov8s-aoi1024bg512r3e50.pt"
def test_promoted_yolo_activation_apply_updates_env_file(tmp_path: Path) -> None:
result = _run_activation(tmp_path, "--apply")
assert result.returncode == 0, result.stderr
payload = json.loads(result.stdout)
assert payload["status"] == "applied"
env_text = (tmp_path / ".env").read_text(encoding="utf-8")
assert "GEOINTEL_ENV=production" in env_text
assert "GEOINTEL_INSTALL_AI=true" in env_text
assert "YOLO_ENABLED=true" in env_text
assert "YOLO_MODELS_DIR=/app/models" in env_text
assert "YOLO_MODEL_PATH=/app/models/geointel-building-yolov8s-aoi1024bg512r3e50.pt" in env_text
def test_promoted_yolo_activation_rejects_non_promoted_report(tmp_path: Path) -> None:
models_dir = tmp_path / "models"
_write_model(models_dir)
report_path = tmp_path / "promotion_report.json"
_write_report(report_path, promotion_status="reject")
result = subprocess.run(
[
"python",
str(SCRIPT),
"--promotion-report",
str(report_path),
"--candidate-key",
CANDIDATE_KEY,
"--models-dir",
str(models_dir),
"--env-file",
str(tmp_path / ".env"),
"--json",
],
cwd=ROOT,
capture_output=True,
text=True,
timeout=30,
check=False,
)
assert result.returncode == 3
payload = json.loads(result.stdout)
assert payload["status"] == "candidate_not_promoted"
assert "rejection_reasons" in payload
def test_readiness_gate_compiles_promoted_activation_script() -> None:
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
assert "-m py_compile scripts/activate_promoted_yolo_candidate.py" in readiness