feat(ml): DB-backed tagger_store_floor (default 0.70), the ingest confidence floor
Promotes the prediction store-floor from the TAGGER_STORE_FLOOR env (default 0.05) to a DB-backed, Settings-UI-tunable ml_settings column (default 0.70). Storing every tag down to 0.05 from a ~10k-tag tagger is what grew image_record's TOAST to ~100 GB; the suggestion path already filters at 0.70 and the centroid/learned path covers lower-confidence preferred tags, so the sub-0.70 tail is redundant. Foundation for plan-task #764 (backfill + reclaim land next; this only changes the write gate for NEW imports). - ml_settings.tagger_store_floor (migration 0044, default 0.70) - tagger.Tagger.infer(store_floor=...); ml task passes settings.tagger_store_floor - ML admin GET/PATCH expose it; PATCH rejects a category suggestion threshold below the floor (nothing below the floor is stored, so the gap surfaces nothing) — server backstop for the UI slider clamp - Settings → ML: store-floor slider + caption; category sliders min-bound to it Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ _EDITABLE = (
|
||||
"suggestion_threshold_general",
|
||||
"centroid_similarity_threshold",
|
||||
"min_reference_images",
|
||||
"tagger_store_floor",
|
||||
)
|
||||
|
||||
|
||||
@@ -30,6 +31,7 @@ async def get_settings():
|
||||
"suggestion_threshold_general": s.suggestion_threshold_general,
|
||||
"centroid_similarity_threshold": s.centroid_similarity_threshold,
|
||||
"min_reference_images": s.min_reference_images,
|
||||
"tagger_store_floor": s.tagger_store_floor,
|
||||
"tagger_model_version": s.tagger_model_version,
|
||||
"embedder_model_version": s.embedder_model_version,
|
||||
}
|
||||
@@ -47,13 +49,45 @@ async def patch_settings():
|
||||
s = (
|
||||
await session.execute(select(MLSettings).where(MLSettings.id == 1))
|
||||
).scalar_one()
|
||||
|
||||
# Merge the patch over current values, then validate the result as a
|
||||
# whole — the store-floor invariant couples three fields, so they
|
||||
# can't be checked one at a time.
|
||||
proposed = {f: getattr(s, f) for f in _EDITABLE}
|
||||
for field in _EDITABLE:
|
||||
if field in body:
|
||||
setattr(s, field, body[field])
|
||||
proposed[field] = body[field]
|
||||
|
||||
err = _validate(proposed)
|
||||
if err is not None:
|
||||
return jsonify({"error": err}), 400
|
||||
|
||||
for field in _EDITABLE:
|
||||
setattr(s, field, proposed[field])
|
||||
await session.commit()
|
||||
return await get_settings()
|
||||
|
||||
|
||||
def _validate(p: dict) -> str | None:
|
||||
"""Returns an error string if the proposed settings are invalid, else None.
|
||||
|
||||
Invariant (plan-task #764): the per-category suggestion thresholds can't
|
||||
drop below tagger_store_floor — nothing below the floor is stored, so a
|
||||
lower threshold would silently surface nothing in that gap. The UI clamps
|
||||
the sliders to the floor; this is the server-side backstop.
|
||||
"""
|
||||
floor = p["tagger_store_floor"]
|
||||
if not (0.0 <= floor <= 1.0):
|
||||
return "tagger_store_floor must be between 0 and 1"
|
||||
for cat in ("character", "general"):
|
||||
if p[f"suggestion_threshold_{cat}"] < floor:
|
||||
return (
|
||||
f"suggestion_threshold_{cat} cannot be below tagger_store_floor "
|
||||
f"({floor}) — predictions below the floor are not stored"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@ml_admin_bp.route("/backfill", methods=["POST"])
|
||||
async def trigger_backfill():
|
||||
from ..tasks.ml import backfill
|
||||
|
||||
Reference in New Issue
Block a user