feat(system-tags): process vs chrome groups + WIP provisional auto-apply (#1464)
Backend for the system-tag behavior refactor (milestone #157). editor screenshot moves from chrome (hidden) to the PROCESS group (shown, like wip); wip+editor gain provisional auto-apply so they stop needing endless manual identification — without a runaway loop. - tag.py: split PRESENTATION_SYSTEM_TAGS → CHROME_SYSTEM_TAGS (banner) + PROCESS_SYSTEM_TAGS (wip, editor screenshot). - heads.py: generalize presentation_auto_apply_sweep → system_tag_auto_apply_sweep (mode chrome|process). Same Guard 1 (skip human/confirmed) + Guard 2 (ring-loud conflict → PresentationReview). process mode uses source 'process_auto' and does NOT hide (hide is a gallery-query effect of group membership). - training_data._AUTO_SOURCES += 'process_auto' → the head never trains on its own auto-applied output; only wip_title/manual train it (the runaway break). - ml_settings: process_auto_apply_enabled (OFF, opt-in) + threshold + conflict threshold. presentation_review.mode ('chrome'|'process'). Migration 0086. - gallery_service: default-hide reads CHROME only (editor now shows); Explore neighbors exclude the whole PROCESS group. - tasks/ml + celery beat: scheduled_process_auto_apply (daily, opt-in); prune covers both modes. - api: ml_admin process_* CRUD+validation; hidden-review returns mode. - tests: rename chrome sweep calls; new test_process_auto_apply (apply, guards, mode flag, no-self-train); gallery test asserts editor now visible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ from ...models import (
|
||||
TagPositiveConfirmation,
|
||||
TagSuggestionRejection,
|
||||
)
|
||||
from ...models.tag import PRESENTATION_SYSTEM_TAGS, image_tag
|
||||
from ...models.tag import CHROME_SYSTEM_TAGS, PROCESS_SYSTEM_TAGS, image_tag
|
||||
from .training_data import (
|
||||
_AUTO_SOURCES,
|
||||
_auto_apply_point,
|
||||
@@ -759,18 +759,42 @@ def auto_apply_sweep(
|
||||
|
||||
|
||||
_PRESENTATION_SOURCE = "presentation_auto"
|
||||
_PROCESS_SOURCE = "process_auto"
|
||||
|
||||
# System-tag auto-apply modes (#1464). Both modes run the identical sweep — apply
|
||||
# a system tag at a flat threshold with a PROVISIONAL source + a ring-loud review
|
||||
# guard — and differ ONLY in which tags, which settings knobs, and which
|
||||
# source/review-mode. 'chrome' (banner) is HIDDEN from the gallery; 'process'
|
||||
# (wip / editor screenshot) stays VISIBLE (the hide is a gallery-query effect of
|
||||
# the tag's group membership, not of this sweep).
|
||||
_SWEEP_MODES = {
|
||||
"chrome": {
|
||||
"names": CHROME_SYSTEM_TAGS,
|
||||
"enabled": "presentation_auto_apply_enabled",
|
||||
"threshold": "presentation_auto_apply_threshold",
|
||||
"conflict": "presentation_conflict_threshold",
|
||||
"source": _PRESENTATION_SOURCE,
|
||||
},
|
||||
"process": {
|
||||
"names": PROCESS_SYSTEM_TAGS,
|
||||
"enabled": "process_auto_apply_enabled",
|
||||
"threshold": "process_auto_apply_threshold",
|
||||
"conflict": "process_conflict_threshold",
|
||||
"source": _PROCESS_SOURCE,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _presentation_heads(session: Session, embedding_version: str):
|
||||
"""Trained heads for the presentation chrome tags (banner / editor screenshot).
|
||||
They fire at the FLAT presentation threshold regardless of graduation — a head
|
||||
exists once the operator has labelled enough chrome (head_min_positives)."""
|
||||
def _system_tag_heads(session: Session, embedding_version: str, names):
|
||||
"""Trained heads for a system-tag group (chrome banner / process wip+editor).
|
||||
They fire at the group's FLAT threshold regardless of graduation — a head
|
||||
exists once the operator has labelled enough (head_min_positives)."""
|
||||
return session.execute(
|
||||
select(TagHead.tag_id, Tag.name, TagHead.weights, TagHead.bias)
|
||||
.join(Tag, Tag.id == TagHead.tag_id)
|
||||
.where(TagHead.embedding_version == embedding_version)
|
||||
.where(Tag.is_system.is_(True))
|
||||
.where(Tag.name.in_(PRESENTATION_SYSTEM_TAGS))
|
||||
.where(Tag.name.in_(names))
|
||||
).all()
|
||||
|
||||
|
||||
@@ -802,27 +826,33 @@ def _valued_image_ids(session: Session) -> set[int]:
|
||||
return {r[0] for r in rows}
|
||||
|
||||
|
||||
def presentation_auto_apply_sweep(session: Session, dry_run: bool = False) -> dict:
|
||||
"""Auto-hide presentation chrome (banner / editor screenshot) at the FLAT
|
||||
presentation threshold (#141) — NOT the per-head graduated threshold. Two
|
||||
guards keep it safe: (1) never hide an image carrying a human/confirmed content
|
||||
tag; (2) if an image about to be hidden ALSO scores >= the conflict threshold
|
||||
on a content head, still hide it but flag it (PresentationReview) so the Hidden
|
||||
view surfaces "also looks like <X>" for review. No-op unless
|
||||
presentation_auto_apply_enabled. numpy-only (no sklearn). Returns
|
||||
{n_applied, n_flagged, concepts}."""
|
||||
def system_tag_auto_apply_sweep(
|
||||
session: Session, *, mode: str, dry_run: bool = False
|
||||
) -> dict:
|
||||
"""Auto-apply a system-tag group at its FLAT threshold. mode='chrome' (banner,
|
||||
#141) hides the image; mode='process' (wip / editor screenshot, #1464) keeps it
|
||||
VISIBLE — the ONLY difference is the tag group's gallery membership, not this
|
||||
sweep. Two guards keep it safe: (1) never touch an image carrying a
|
||||
human/confirmed content tag; (2) if the image ALSO scores >= the conflict
|
||||
threshold on a content head, still apply but flag it (PresentationReview,
|
||||
mode=<mode>) so the review strip surfaces "also looks like <X>". The source is
|
||||
PROVISIONAL so the head never trains on its own output. No-op unless the mode's
|
||||
enabled flag is set. numpy-only (no sklearn). Returns {n_applied, n_flagged,
|
||||
concepts}."""
|
||||
import numpy as np
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
|
||||
cfg = _SWEEP_MODES[mode]
|
||||
settings = _settings(session)
|
||||
if not dry_run and not settings.presentation_auto_apply_enabled:
|
||||
if not dry_run and not getattr(settings, cfg["enabled"]):
|
||||
return {"n_applied": 0, "n_flagged": 0, "concepts": []}
|
||||
ver = settings.embedder_model_version
|
||||
pres = _presentation_heads(session, ver)
|
||||
pres = _system_tag_heads(session, ver, cfg["names"])
|
||||
if not pres:
|
||||
return {"n_applied": 0, "n_flagged": 0, "concepts": []}
|
||||
thr = float(settings.presentation_auto_apply_threshold)
|
||||
conflict_thr = float(settings.presentation_conflict_threshold)
|
||||
thr = float(getattr(settings, cfg["threshold"]))
|
||||
conflict_thr = float(getattr(settings, cfg["conflict"]))
|
||||
source = cfg["source"]
|
||||
|
||||
Wp = np.vstack([np.asarray(r.weights, dtype=np.float32) for r in pres])
|
||||
bp = np.asarray([r.bias for r in pres], dtype=np.float32)
|
||||
@@ -884,11 +914,13 @@ def presentation_auto_apply_sweep(session: Session, dry_run: bool = False) -> di
|
||||
pg_insert(image_tag)
|
||||
.values(
|
||||
image_record_id=iid, tag_id=tid,
|
||||
source=_PRESENTATION_SOURCE,
|
||||
source=source,
|
||||
)
|
||||
.on_conflict_do_nothing()
|
||||
)
|
||||
# Guard 2: also looks like content → hide but flag for review.
|
||||
# Guard 2: also looks like real content → still apply, but flag it
|
||||
# for the review strip instead of silently marking (chrome hides,
|
||||
# process stays visible — either way the operator gets a heads-up).
|
||||
if Wc is not None and float(max_c[idx]) >= conflict_thr:
|
||||
n_flagged += 1
|
||||
if not dry_run:
|
||||
@@ -898,6 +930,7 @@ def presentation_auto_apply_sweep(session: Session, dry_run: bool = False) -> di
|
||||
image_record_id=iid, tag_id=tid,
|
||||
conflict_tag_id=conf_tag_ids[int(arg_c[idx])],
|
||||
conflict_score=float(max_c[idx]),
|
||||
mode=mode,
|
||||
)
|
||||
.on_conflict_do_nothing()
|
||||
)
|
||||
|
||||
@@ -29,7 +29,10 @@ from ...models.tag import image_tag
|
||||
# a CCIP reference) unless the operator confirms them (milestone 139). Keeping
|
||||
# auto-applied predictions out of training is what makes them "soft" — a misfire
|
||||
# can't reinforce itself, so the retraction sweep can actually drop it.
|
||||
_AUTO_SOURCES = ("head_auto", "ccip_auto", "ml_auto", "presentation_auto")
|
||||
# `process_auto` (#1464): wip/editor screenshot applied by the process sweep are
|
||||
# ALSO provisional — the head must learn only from title (`wip_title`) + manual
|
||||
# labels, never its own auto-applied output, or it would runaway (operator 2026-07-12).
|
||||
_AUTO_SOURCES = ("head_auto", "ccip_auto", "ml_auto", "presentation_auto", "process_auto")
|
||||
|
||||
|
||||
def _hygiene_excluded_ids(session: Session) -> set[int]:
|
||||
|
||||
Reference in New Issue
Block a user