feat(ml): soft auto-apply — retract auto-tags now below threshold (milestone 139)
Daily scheduled_retract_auto_tags re-scores standing auto-applied tags and drops the ones the model no longer supports: - retract_auto_applied_heads: per graduated head, re-score its source='head_auto' images (bounded — only the images already carrying the auto-tag, not the whole library) and remove ones now < auto_apply_threshold. - retract_auto_applied_ccip: per source='ccip_auto' character tag, max-cosine the image's figure vectors vs that character's prototypes; remove ones now below the ccip auto-apply threshold. Both SKIP operator-confirmed tags (TagPositiveConfirmation) and are SILENT — a low score isn't proof the tag was wrong, so no hard negative is recorded (that's reserved for an operator removal). No-op unless the relevant auto-apply switch is on. New daily beat. sklearn-free tests for both paths + the disabled no-op. 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:
@@ -35,6 +35,7 @@ from ...models import (
|
||||
Tag,
|
||||
TagHead,
|
||||
TagKind,
|
||||
TagPositiveConfirmation,
|
||||
TagSuggestionRejection,
|
||||
)
|
||||
from ...models.tag import image_tag
|
||||
@@ -723,3 +724,64 @@ def auto_apply_sweep(
|
||||
for h in range(len(rows))
|
||||
]
|
||||
return {"n_applied": sum(applied), "concepts": concepts}
|
||||
|
||||
|
||||
def retract_auto_applied_heads(session: Session) -> int:
|
||||
"""Soft auto-apply (milestone 139): re-score every standing source='head_auto'
|
||||
tag against its CURRENT head and REMOVE the ones now BELOW the head's
|
||||
auto_apply_threshold — i.e. the head sharpened (or the operator raised the bar)
|
||||
and no longer supports them. Skips operator-confirmed tags
|
||||
(TagPositiveConfirmation). SILENT: a low score isn't proof the tag was wrong,
|
||||
so no hard negative is recorded — that's reserved for an operator removal.
|
||||
No-op unless head_auto_apply_enabled. Only re-scores the images that ALREADY
|
||||
carry the auto-tag (bounded), never the whole library. Returns n_retracted."""
|
||||
import numpy as np
|
||||
|
||||
settings = _settings(session)
|
||||
if not settings.head_auto_apply_enabled:
|
||||
return 0
|
||||
heads = session.execute(
|
||||
select(
|
||||
TagHead.tag_id, TagHead.weights, TagHead.bias,
|
||||
TagHead.auto_apply_threshold,
|
||||
)
|
||||
.where(TagHead.embedding_version == settings.embedder_model_version)
|
||||
.where(TagHead.auto_apply_threshold.is_not(None))
|
||||
).all()
|
||||
retracted = 0
|
||||
for tag_id, weights, bias, thr in heads:
|
||||
auto_ids = [
|
||||
iid for (iid,) in session.execute(
|
||||
select(image_tag.c.image_record_id)
|
||||
.where(image_tag.c.tag_id == tag_id)
|
||||
.where(image_tag.c.source == "head_auto")
|
||||
)
|
||||
]
|
||||
if not auto_ids:
|
||||
continue
|
||||
confirmed = {
|
||||
iid for (iid,) in session.execute(
|
||||
select(TagPositiveConfirmation.image_record_id)
|
||||
.where(TagPositiveConfirmation.tag_id == tag_id)
|
||||
.where(TagPositiveConfirmation.image_record_id.in_(auto_ids))
|
||||
)
|
||||
}
|
||||
candidates = [i for i in auto_ids if i not in confirmed]
|
||||
emb = _load_embeddings(session, candidates)
|
||||
cids = [i for i in candidates if i in emb]
|
||||
if not cids:
|
||||
continue
|
||||
Xn = _l2norm(np.vstack([emb[i] for i in cids]).astype(np.float32), np)
|
||||
w = np.asarray(weights, dtype=np.float32)
|
||||
probs = 1.0 / (1.0 + np.exp(-(Xn @ w + float(bias))))
|
||||
below = [cids[k] for k in np.where(probs < float(thr))[0]]
|
||||
for iid in below:
|
||||
session.execute(
|
||||
image_tag.delete()
|
||||
.where(image_tag.c.image_record_id == iid)
|
||||
.where(image_tag.c.tag_id == tag_id)
|
||||
.where(image_tag.c.source == "head_auto")
|
||||
)
|
||||
retracted += 1
|
||||
session.commit()
|
||||
return retracted
|
||||
|
||||
Reference in New Issue
Block a user