Add guarded promoted YOLO activation
This commit is contained in:
+17
-2
@@ -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
|
||||
Reference in New Issue
Block a user