From 6c6e8bdb6d85972447e6be72a761047f619b10bb Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 3 Jul 2026 14:49:40 -0400 Subject: [PATCH] feat(heads): surface system-tag suggestions at a flat 0.65 confidence floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit System tags (wip/banner/editor) already get heads (kind=general) and aren't filtered from suggestions, but they surfaced only at each head's precision-tuned suggest_threshold — high enough to hide the borderline/false-positive guesses the operator wants to SEE and REJECT (hard-negative mining: 'negatively reinforce what isn't a system tag'). score_image now uses a flat _SYSTEM_TAG_SUGGEST_FLOOR (0.65, operator-set) for system-tag heads instead of their auto threshold; content-tag heads keep their own, and the typed-dropdown threshold_override still overrides everything. _current_heads carries Tag.is_system into the head meta to drive it. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM --- backend/app/services/ml/heads.py | 28 ++++++++++++++++++--- tests/test_ml_suggestions.py | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/backend/app/services/ml/heads.py b/backend/app/services/ml/heads.py index d7d8480..7143663 100644 --- a/backend/app/services/ml/heads.py +++ b/backend/app/services/ml/heads.py @@ -63,6 +63,17 @@ _HEAD_KINDS = (TagKind.general, TagKind.character) # tag.kind -> the suggestion category the rail groups under. _CATEGORY = {TagKind.general: "general", TagKind.character: "character"} +# System-tag (wip/banner/editor screenshot) heads surface as suggestions at +# this FLAT confidence floor instead of their auto-derived (precision-tuned) +# suggest threshold. The auto threshold is high, so it hides the borderline / +# false-positive guesses — which are exactly the cases the operator wants to +# SEE and REJECT to sharpen these heads (hard-negative mining: "negatively +# reinforce what isn't a system tag"). Operator-set 0.65 (2026-07-03): high +# enough not to spam near-zero scores, low enough to surface real mistakes. +# Content-tag heads keep their own thresholds; the typed-dropdown's +# threshold_override still overrides everything (show-all mode). +_SYSTEM_TAG_SUGGEST_FLOOR = 0.65 + class HeadTrainingAlreadyRunning(Exception): """Raised by start_head_training_run when a run is already in flight.""" @@ -297,7 +308,7 @@ async def _current_heads(session: AsyncSession, embedding_version: str): rows = ( await session.execute( select( - TagHead.tag_id, Tag.name, Tag.kind, + TagHead.tag_id, Tag.name, Tag.kind, Tag.is_system, TagHead.weights, TagHead.bias, TagHead.suggest_threshold, TagHead.auto_apply_threshold, ) @@ -317,6 +328,7 @@ async def _current_heads(session: AsyncSession, embedding_version: str): "name": r.name, "category": _CATEGORY.get(r.kind, "general"), "auto_apply_threshold": r.auto_apply_threshold, + "is_system": bool(r.is_system), } for r in rows ] @@ -333,7 +345,10 @@ async def score_image( category, score}], ranked. A concept surfaces when its score clears the head's own suggest_threshold — or, when threshold_override is given (the typed-dropdown "show everything" mode), that flat floor instead (0 → every - head). Empty if the image has no embedding or no heads exist yet. + head). System-tag heads (wip/banner/editor) instead use a flat + _SYSTEM_TAG_SUGGEST_FLOOR so their false positives surface for rejection + (still overridden by threshold_override). Empty if the image has no + embedding or no heads exist yet. MAX-OVER-BAG: the image is scored as a BAG of embeddings — the whole-image vector PLUS every concept-region crop the agent embedded (same model @@ -383,7 +398,14 @@ async def score_image( probs = (1.0 / (1.0 + np.exp(-Z))).max(axis=0) # (H,) best over the bag out = [] for i, p in enumerate(probs): - cut = threshold_override if threshold_override is not None else heads["thr"][i] + if threshold_override is not None: + cut = threshold_override + elif heads["meta"][i]["is_system"]: + # System tags surface at the flat floor (see _SYSTEM_TAG_SUGGEST_FLOOR) + # so their false positives show up for the operator to reject. + cut = _SYSTEM_TAG_SUGGEST_FLOOR + else: + cut = heads["thr"][i] if p >= cut: m = heads["meta"][i] out.append({ diff --git a/tests/test_ml_suggestions.py b/tests/test_ml_suggestions.py index a797013..e0cc143 100644 --- a/tests/test_ml_suggestions.py +++ b/tests/test_ml_suggestions.py @@ -111,6 +111,49 @@ async def test_threshold_override_surfaces_below_cut(db): assert any(s.canonical_tag_id == tag.id for s in flooded.by_category["general"]) +@pytest.mark.asyncio +async def test_system_tag_surfaces_at_flat_floor_despite_high_threshold(db): + # A system tag's head uses the flat _SYSTEM_TAG_SUGGEST_FLOOR (0.65), not its + # precision-tuned suggest_threshold — so a matching image (~0.73) surfaces it + # for rejection even though its 0.9 auto threshold would hide it. + from backend.app.services.ml.heads import _SYSTEM_TAG_SUGGEST_FLOOR + assert _SYSTEM_TAG_SUGGEST_FLOOR == 0.65 + tag = await TagService(db).find_or_create("wip sketchy", TagKind.general) + tag.is_system = True + img = await _img(db, "1" * 64, _emb(0)) # matches head → ~0.73 + await _head(db, tag.id, slot=0, suggest_threshold=0.9) + await db.commit() + sl = await SuggestionService(db).for_image(img.id) + assert any( + s.canonical_tag_id == tag.id for s in sl.by_category.get("general", []) + ) + + +@pytest.mark.asyncio +async def test_system_tag_below_floor_stays_hidden(db): + # Below the 0.65 floor (~0.5 on an orthogonal image) the system tag is not + # surfaced — the floor still gates, it isn't "always show". + tag = await TagService(db).find_or_create("wip faint", TagKind.general) + tag.is_system = True + img = await _img(db, "2" * 64, _emb(1)) # orthogonal → 0.5 + await _head(db, tag.id, slot=0, suggest_threshold=0.9) + await db.commit() + sl = await SuggestionService(db).for_image(img.id) + assert not sl.by_category.get("general") + + +@pytest.mark.asyncio +async def test_content_tag_ignores_system_floor(db): + # A NON-system head with the same high threshold does NOT get the floor: the + # ~0.73 match stays hidden below its 0.9 auto threshold (floor is system-only). + tag = await TagService(db).find_or_create("glasses hi", TagKind.general) + img = await _img(db, "3" * 64, _emb(0)) # matches head → ~0.73 + await _head(db, tag.id, slot=0, suggest_threshold=0.9) + await db.commit() + sl = await SuggestionService(db).for_image(img.id) + assert not sl.by_category.get("general") + + @pytest.mark.asyncio async def test_concept_region_surfaces_via_max_over_bag(db): # Max-over-bag: the whole-image vector is orthogonal to the head (scores the