feat(heads): surface system-tag suggestions at a flat 0.65 confidence floor
CI / lint (push) Successful in 5s
CI / frontend-build (push) Successful in 38s
CI / backend-lint-and-test (push) Successful in 3m23s
CI / integration (push) Successful in 4m36s

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
This commit is contained in:
2026-07-03 14:49:40 -04:00
parent c9696a2faf
commit 6c6e8bdb6d
2 changed files with 68 additions and 3 deletions
+25 -3
View File
@@ -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({
+43
View File
@@ -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