feat(suggestions): heads are the suggestion source — Camie + centroid removed (#114 C)
The rail's Suggestions now come from the trained per-concept heads. SuggestionService.for_image scores the image's frozen SigLIP embedding against every head (heads.score_image) and surfaces concepts above each head's own suggest threshold; the typed-dropdown's min=0 "show everything" mode maps to a flat floor so any head-scored concept can still be picked. Already-applied tags drop; rejected tags stay flagged + reversible (unchanged). REMOVED from the suggestion path (rule 22, no fallback): the Camie ImagePrediction candidate/alias/merge pipeline and the per-tag centroid augmentation, plus the now-dead SuggestionService internals (_load_predictions, _threshold_for, _settings, self.aliases, self.centroids). Head suggestions are always canonical tags, so raw_name/via_alias are null/false and the rail's alias kebab is inert by data (its removal + the Camie ingest-tagger rip are the flagged follow-up). for_selection (bulk consensus) now aggregates head suggestions unchanged. Tests rewritten to the head path: test_ml_suggestions (surfaces/applied/ rejected-reversible/override/no-embedding/no-heads), test_suggestions_bulk (consensus), test_api_suggestions (get + dropped the Camie-alias roundtrip), and test_ml_artist_retired (artist not head-eligible via _HEAD_KINDS). DEPLOY NOTE: after this lands, the rail is empty until you run Train heads (Settings → Tagging → Concept heads) — deploy, train, then the rail populates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
@@ -287,10 +287,14 @@ async def _current_heads(session: AsyncSession, embedding_version: str):
|
||||
return loaded
|
||||
|
||||
|
||||
async def score_image(session: AsyncSession, image_id: int) -> list[dict]:
|
||||
async def score_image(
|
||||
session: AsyncSession, image_id: int, threshold_override: float | None = None,
|
||||
) -> list[dict]:
|
||||
"""Suggestions for one image from the trained heads: [{tag_id, name,
|
||||
category, score}], score >= each head's suggest_threshold, ranked. Empty if
|
||||
the image has no embedding or no heads exist yet."""
|
||||
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."""
|
||||
import numpy as np
|
||||
|
||||
img = await session.get(ImageRecord, image_id)
|
||||
@@ -307,7 +311,8 @@ async def score_image(session: AsyncSession, image_id: int) -> list[dict]:
|
||||
probs = 1.0 / (1.0 + np.exp(-z))
|
||||
out = []
|
||||
for i, p in enumerate(probs):
|
||||
if p >= heads["thr"][i]:
|
||||
cut = threshold_override if threshold_override is not None else heads["thr"][i]
|
||||
if p >= cut:
|
||||
m = heads["meta"][i]
|
||||
out.append({
|
||||
"tag_id": m["tag_id"],
|
||||
|
||||
Reference in New Issue
Block a user