feat(suggestions): tag-input dropdown searches the full prediction set
The typed dropdown sourced the threshold-filtered panel list (>= 0.70 general), so low-confidence actions/features the model DID predict never appeared — forcing hand-typed custom tags instead of accepting the model's canonical formatting. Add a threshold override: SuggestionService.for_image(threshold_override=) and GET /images/<id>/suggestions?min=<f> surface EVERY stored prediction (down to the 0.05 store floor), alias-resolved and normalized, still excluding applied/rejected and unsurfaced categories. The suggestions store gains allByCategory + loadAll (min=0); the dropdown searches that full set (cap 20), while the Suggestions panel stays curated at the configured threshold. Accept/dismiss drop from both lists. Operator-asked 2026-06-09. Test: a 0.30 general prediction is hidden by default but surfaced with threshold_override=0.0; unsurfaced categories still excluded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,8 +11,21 @@ suggestions_bp = Blueprint("suggestions", __name__, url_prefix="/api")
|
||||
|
||||
@suggestions_bp.route("/images/<int:image_id>/suggestions", methods=["GET"])
|
||||
async def get_suggestions(image_id: int):
|
||||
# ?min=<float> overrides the configured per-category thresholds so the typed
|
||||
# tag-input dropdown can surface EVERY stored prediction (min=0), including
|
||||
# low-confidence actions/features, in canonical formatting. Omitted → the
|
||||
# curated above-threshold list the Suggestions panel uses.
|
||||
override = None
|
||||
raw_min = request.args.get("min")
|
||||
if raw_min is not None:
|
||||
try:
|
||||
override = min(1.0, max(0.0, float(raw_min)))
|
||||
except ValueError:
|
||||
return jsonify({"error": "min must be a float in [0,1]"}), 400
|
||||
async with get_session() as session:
|
||||
sl = await SuggestionService(session).for_image(image_id)
|
||||
sl = await SuggestionService(session).for_image(
|
||||
image_id, threshold_override=override
|
||||
)
|
||||
return jsonify(
|
||||
{
|
||||
"by_category": {
|
||||
|
||||
@@ -48,16 +48,33 @@ class SuggestionService:
|
||||
await self.session.execute(select(MLSettings).where(MLSettings.id == 1))
|
||||
).scalar_one()
|
||||
|
||||
def _threshold_for(self, s: MLSettings, category: str) -> float:
|
||||
def _threshold_for(
|
||||
self, s: MLSettings, category: str, override: float | None = None,
|
||||
) -> float:
|
||||
# 'artist' (FC-2d-vii-c) and 'copyright' (2026-06-01) retired;
|
||||
# both fall through to the 1.01 "never surfaces" default like any
|
||||
# unsurfaced category.
|
||||
# override (the typed-dropdown "show everything the model saw" mode)
|
||||
# applies to the surfaced categories only — unsurfaced ones are already
|
||||
# skipped before the threshold check, so they can't leak in.
|
||||
if override is not None:
|
||||
return override
|
||||
return {
|
||||
"character": s.suggestion_threshold_character,
|
||||
"general": s.suggestion_threshold_general,
|
||||
}.get(category, 1.01)
|
||||
|
||||
async def for_image(self, image_id: int) -> SuggestionList:
|
||||
async def for_image(
|
||||
self, image_id: int, *, threshold_override: float | None = None,
|
||||
) -> SuggestionList:
|
||||
"""Ranked suggestions for one image.
|
||||
|
||||
threshold_override surfaces EVERY stored tagger prediction (down to the
|
||||
ingest STORE_FLOOR) regardless of the configured per-category suggestion
|
||||
thresholds — backs the tag-input dropdown's "search all of the model's
|
||||
predictions, including low-confidence ones, in the canonical formatting"
|
||||
mode (operator-asked 2026-06-09). The Suggestions panel still calls with
|
||||
no override so it stays the curated above-threshold list."""
|
||||
img = await self.session.get(ImageRecord, image_id)
|
||||
if img is None:
|
||||
return SuggestionList()
|
||||
@@ -96,7 +113,7 @@ class SuggestionService:
|
||||
if category not in SURFACED_CATEGORIES:
|
||||
continue
|
||||
conf = float(p.get("confidence", 0.0))
|
||||
if conf < self._threshold_for(settings, category):
|
||||
if conf < self._threshold_for(settings, category, threshold_override):
|
||||
continue
|
||||
display = normalize_tag_name(name)
|
||||
if display is None:
|
||||
|
||||
Reference in New Issue
Block a user