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 @@
+
+
+ Each proposer finds regions that the crop → SigLIP bag scores alongside the
+ whole image; the best-scoring region is what grounds a tag. Weights accept
+ an ultralytics name ( {{ p.help }}
+ Bound how many crops each image can produce — protects GPU time and
+ storage. Dedupe IoU drops near-duplicate crops before embedding.
+ yolo11n.pt), a
+ URL, or hf_repo::file. Edits reach the GPU
+ agent on its next lease — no restart. After turning a proposer on, re-process
+ the library so existing images gain its regions.
+