From ab362bc79cd31095048e928544fd0e14402cb15a Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 5 Jul 2026 19:51:55 -0400 Subject: [PATCH] =?UTF-8?q?feat(ml):=20Settings=20=E2=86=92=20Tagging=20'C?= =?UTF-8?q?rop=20proposers'=20card=20(#134=20step=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exposes the detector config (per-proposer enable + weights + confidence, caps, dedupe IoU) in Settings → Tagging, backed by MLSettings via /api/ml/settings. ml_admin adds the detector fields to _EDITABLE + GET payload + validation (conf 0..1, caps >=1, IoU 0..1). New CropProposersCard.vue (mirrors HeadsCard) with working defaults pre-filled, per-field live-save (no restart — the agent picks changes up on its next lease), weights-format help, switch-revert on error. Closes milestone #134: all three proposers are on out-of-the-box and tunable in the UI. Test: detector defaults GET + patch round-trip + range validation. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM --- backend/app/api/ml_admin.py | 35 ++++ .../components/settings/CropProposersCard.vue | 151 ++++++++++++++++++ .../components/settings/MaintenancePanel.vue | 2 + tests/test_api_ml_admin.py | 29 ++++ 4 files changed, 217 insertions(+) create mode 100644 frontend/src/components/settings/CropProposersCard.vue diff --git a/backend/app/api/ml_admin.py b/backend/app/api/ml_admin.py index 62fbc26..c146b70 100644 --- a/backend/app/api/ml_admin.py +++ b/backend/app/api/ml_admin.py @@ -8,6 +8,26 @@ from ..models import MLSettings ml_admin_bp = Blueprint("ml_admin", __name__, url_prefix="/api/ml") +# Crop-proposer / detector config (#134). Announced to the GPU agent in the lease +# → tunable here with no restart. weights = ultralytics name | URL | hf_repo::file +# (empty, or enabled off, skips that proposer). +_DETECTOR_FIELDS = ( + "detector_person_enabled", + "detector_person_weights", + "detector_person_conf", + "detector_anatomy_enabled", + "detector_anatomy_weights", + "detector_anatomy_conf", + "detector_panel_enabled", + "detector_panel_weights", + "detector_panel_conf", + "detector_max_figures", + "detector_max_components", + "detector_max_panels", + "detector_max_regions", + "detector_dedupe_iou", +) + _EDITABLE = ( "cpu_embed_enabled", "video_frame_interval_seconds", @@ -21,6 +41,7 @@ _EDITABLE = ( "ccip_auto_apply_threshold", "embedder_model_name", "embedder_model_version", + *_DETECTOR_FIELDS, ) @@ -76,6 +97,7 @@ async def get_settings(): "ccip_auto_apply_enabled": s.ccip_auto_apply_enabled, "ccip_auto_apply_threshold": s.ccip_auto_apply_threshold, "embedder_model_name": s.embedder_model_name, + **{f: getattr(s, f) for f in _DETECTOR_FIELDS}, } ) @@ -133,6 +155,19 @@ def _validate(p: dict) -> str | None: for key in ("embedder_model_name", "embedder_model_version"): if not str(p[key]).strip(): return f"{key} must not be empty" + # Crop proposers (#134). Weights may be empty (that proposer is just off); + # confidences are probabilities; caps are positive counts; IoU is [0,1]. + for key in ("detector_person_conf", "detector_anatomy_conf", "detector_panel_conf"): + if not (0.0 <= float(p[key]) <= 1.0): + return f"{key} must be between 0 and 1" + for key in ( + "detector_max_figures", "detector_max_components", + "detector_max_panels", "detector_max_regions", + ): + if int(p[key]) < 1: + return f"{key} must be >= 1" + if not (0.0 <= float(p["detector_dedupe_iou"]) <= 1.0): + return "detector_dedupe_iou must be between 0 and 1" return None diff --git a/frontend/src/components/settings/CropProposersCard.vue b/frontend/src/components/settings/CropProposersCard.vue new file mode 100644 index 0000000..46edd54 --- /dev/null +++ b/frontend/src/components/settings/CropProposersCard.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/frontend/src/components/settings/MaintenancePanel.vue b/frontend/src/components/settings/MaintenancePanel.vue index 26cae1f..dfc5693 100644 --- a/frontend/src/components/settings/MaintenancePanel.vue +++ b/frontend/src/components/settings/MaintenancePanel.vue @@ -36,6 +36,7 @@

+
@@ -75,6 +76,7 @@ import MissingFileRepairCard from './MissingFileRepairCard.vue' import GpuTriageCard from './GpuTriageCard.vue' import DbMaintenanceCard from './DbMaintenanceCard.vue' import MLThresholdSliders from './MLThresholdSliders.vue' +import CropProposersCard from './CropProposersCard.vue' import HeadsCard from './HeadsCard.vue' import GpuAgentCard from './GpuAgentCard.vue' import AliasTable from './AliasTable.vue' diff --git a/tests/test_api_ml_admin.py b/tests/test_api_ml_admin.py index 5602f8c..33faf09 100644 --- a/tests/test_api_ml_admin.py +++ b/tests/test_api_ml_admin.py @@ -52,6 +52,35 @@ async def test_embedder_model_default_settable_and_empty_rejected(client): assert bad.status_code == 400 +@pytest.mark.asyncio +async def test_detector_settings_defaults_patch_and_validation(client): + # #134: crop-proposer config is exposed + editable here (announced to the + # agent via the lease). Defaults are all-on with the pinned weights. + body = await (await client.get("/api/ml/settings")).get_json() + assert body["detector_person_enabled"] is True + assert body["detector_anatomy_enabled"] is True + assert "yolov11m_aa22" in body["detector_anatomy_weights"] + assert body["detector_panel_weights"].endswith("::best.pt") + assert body["detector_max_regions"] == 128 + + ok = await client.patch("/api/ml/settings", json={ + "detector_anatomy_enabled": False, + "detector_person_conf": 0.5, + "detector_max_panels": 4, + }) + assert ok.status_code == 200 + out = await ok.get_json() + assert out["detector_anatomy_enabled"] is False + assert out["detector_person_conf"] == 0.5 + assert out["detector_max_panels"] == 4 + + # Out-of-range confidence + non-positive cap are rejected. + assert (await client.patch( + "/api/ml/settings", json={"detector_panel_conf": 1.5})).status_code == 400 + assert (await client.patch( + "/api/ml/settings", json={"detector_max_regions": 0})).status_code == 400 + + @pytest.mark.asyncio async def test_embedder_models_list(client): # #1203: the dropdown reads the supported-model list from the server.