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:
@@ -236,8 +236,10 @@ async def jump():
|
||||
# content", surfaced in the gallery's Show-hidden review strip. -----------
|
||||
@gallery_bp.route("/hidden-review", methods=["GET"])
|
||||
async def hidden_review():
|
||||
"""Unresolved presentation auto-hide flags, most-concerning first (highest
|
||||
content score) — for the gallery's Hidden-view review strip."""
|
||||
"""Unresolved system-tag auto-apply review flags (chrome + process, #1464),
|
||||
most-concerning first (highest content score) — for the review strip. `mode`
|
||||
tells the client whether the flagged tag hid the image ('chrome') or left it
|
||||
visible ('process'), which decides the resolve labels (un-hide vs remove-tag)."""
|
||||
ptag = aliased(Tag)
|
||||
ctag = aliased(Tag)
|
||||
async with get_session() as session:
|
||||
@@ -247,6 +249,7 @@ async def hidden_review():
|
||||
PresentationReview.tag_id,
|
||||
PresentationReview.conflict_tag_id,
|
||||
PresentationReview.conflict_score,
|
||||
PresentationReview.mode,
|
||||
ImageRecord.path, ImageRecord.thumbnail_path,
|
||||
ImageRecord.sha256, ImageRecord.mime,
|
||||
ptag.name.label("tag_name"),
|
||||
@@ -266,6 +269,7 @@ async def hidden_review():
|
||||
"conflict_tag_id": r.conflict_tag_id,
|
||||
"conflict_name": r.conflict_name,
|
||||
"conflict_score": r.conflict_score,
|
||||
"mode": r.mode,
|
||||
"thumbnail_url": thumbnail_url(r.thumbnail_path, r.sha256, r.mime),
|
||||
"image_url": image_url(r.path),
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ _EDITABLE = (
|
||||
"presentation_auto_apply_enabled",
|
||||
"presentation_auto_apply_threshold",
|
||||
"presentation_conflict_threshold",
|
||||
"process_auto_apply_enabled",
|
||||
"process_auto_apply_threshold",
|
||||
"process_conflict_threshold",
|
||||
"embedder_model_name",
|
||||
"embedder_model_version",
|
||||
*_DETECTOR_FIELDS,
|
||||
@@ -102,6 +105,9 @@ async def get_settings():
|
||||
"presentation_auto_apply_enabled": s.presentation_auto_apply_enabled,
|
||||
"presentation_auto_apply_threshold": s.presentation_auto_apply_threshold,
|
||||
"presentation_conflict_threshold": s.presentation_conflict_threshold,
|
||||
"process_auto_apply_enabled": s.process_auto_apply_enabled,
|
||||
"process_auto_apply_threshold": s.process_auto_apply_threshold,
|
||||
"process_conflict_threshold": s.process_conflict_threshold,
|
||||
"embedder_model_name": s.embedder_model_name,
|
||||
**{f: getattr(s, f) for f in _DETECTOR_FIELDS},
|
||||
}
|
||||
@@ -162,6 +168,12 @@ def _validate(p: dict) -> str | None:
|
||||
return "presentation_auto_apply_threshold must be between 0.5 and 0.999"
|
||||
if not (0.0 <= float(p["presentation_conflict_threshold"]) <= 1.0):
|
||||
return "presentation_conflict_threshold must be between 0 and 1"
|
||||
# Process auto-apply (#1464). wip/editor stay VISIBLE so a false apply is
|
||||
# low-harm (excludes-from-training + a review flag), but keep the same bar.
|
||||
if not (0.5 <= float(p["process_auto_apply_threshold"]) <= 0.999):
|
||||
return "process_auto_apply_threshold must be between 0.5 and 0.999"
|
||||
if not (0.0 <= float(p["process_conflict_threshold"]) <= 1.0):
|
||||
return "process_conflict_threshold must be between 0 and 1"
|
||||
# Embedder model swap (#1190): both must be non-empty. Changing them means a
|
||||
# different embedding space — the operator must re-embed + retrain after.
|
||||
for key in ("embedder_model_name", "embedder_model_version"):
|
||||
|
||||
@@ -171,9 +171,14 @@ def make_celery() -> Celery:
|
||||
},
|
||||
"presentation-auto-apply-daily": {
|
||||
"task": "backend.app.tasks.ml.scheduled_presentation_auto_apply",
|
||||
"schedule": 86400.0, # auto-hide banner/editor chrome (#141);
|
||||
"schedule": 86400.0, # auto-hide banner chrome (#141);
|
||||
# no-op unless presentation_auto_apply_enabled
|
||||
},
|
||||
"process-auto-apply-daily": {
|
||||
"task": "backend.app.tasks.ml.scheduled_process_auto_apply",
|
||||
"schedule": 86400.0, # auto-tag wip/editor process art (#1464);
|
||||
# no-op unless process_auto_apply_enabled (opt-in)
|
||||
},
|
||||
"prune-presentation-reviews-daily": {
|
||||
"task": "backend.app.tasks.ml.prune_presentation_reviews",
|
||||
"schedule": 86400.0, # retention: drop resolved review flags >30d
|
||||
|
||||
@@ -85,12 +85,14 @@ class MLSettings(Base):
|
||||
Float, nullable=False, default=0.95
|
||||
)
|
||||
# -- Presentation chrome auto-hide (#141) -------------------------------
|
||||
# banner / editor screenshot auto-apply on the sweep with their OWN flat
|
||||
# threshold (decoupled from content-head graduation). Hiding is consequential
|
||||
# so it runs HIGH. `wip` is never auto-applied. When an image would be
|
||||
# auto-hidden but ALSO scores >= presentation_conflict_threshold on a content
|
||||
# head, it's still hidden but flagged for review (PresentationReview) instead
|
||||
# of buried silently. ON by default (opt-out); every auto-tag is reversible.
|
||||
# `banner` (chrome — clusters on UI, not content) auto-applies on the sweep
|
||||
# with its OWN flat threshold (decoupled from content-head graduation) and is
|
||||
# HIDDEN from the gallery. Hiding is consequential so it runs HIGH. When an
|
||||
# image would be auto-hidden but ALSO scores >= presentation_conflict_threshold
|
||||
# on a content head, it's still hidden but flagged for review
|
||||
# (PresentationReview, mode='chrome') instead of buried silently. ON by default
|
||||
# (opt-out); every auto-tag is reversible. NOTE (#1464): `wip` + `editor
|
||||
# screenshot` are no longer chrome — they went to the PROCESS path below.
|
||||
presentation_auto_apply_enabled: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=True
|
||||
)
|
||||
@@ -100,6 +102,26 @@ class MLSettings(Base):
|
||||
presentation_conflict_threshold: Mapped[float] = mapped_column(
|
||||
Float, nullable=False, default=0.50
|
||||
)
|
||||
# -- Process auto-apply (#1464) ----------------------------------------
|
||||
# `wip` / `editor screenshot` are PROCESS art — unfinished pieces + program
|
||||
# screenshots that must stay OUT of head/CCIP training but, unlike chrome,
|
||||
# remain VISIBLE in the gallery (operator 2026-07-12). They auto-apply on the
|
||||
# sweep with their OWN flat threshold and a PROVISIONAL source (`process_auto`,
|
||||
# in training_data._AUTO_SOURCES) so the head NEVER trains on its own output —
|
||||
# it learns only from title (`wip_title`) + manual labels, which breaks the
|
||||
# runaway loop. When a process tag would be applied but the image ALSO scores
|
||||
# >= process_conflict_threshold on a content head, it's flagged for review
|
||||
# (PresentationReview, mode='process') rather than silently marked. OFF by
|
||||
# default — a new whole-library auto-tagger is opt-in; every auto-tag reversible.
|
||||
process_auto_apply_enabled: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=False
|
||||
)
|
||||
process_auto_apply_threshold: Mapped[float] = mapped_column(
|
||||
Float, nullable=False, default=0.90
|
||||
)
|
||||
process_conflict_threshold: Mapped[float] = mapped_column(
|
||||
Float, nullable=False, default=0.50
|
||||
)
|
||||
# Default = SigLIP 2 (so400m, 512px) for new installs (migration 0069);
|
||||
# existing libraries keep their stored value until the operator re-embeds.
|
||||
embedder_model_version: Mapped[str] = mapped_column(
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
"""PresentationReview — an auto-hidden presentation tag that ALSO looked like
|
||||
real content, flagged for operator review (milestone 141).
|
||||
"""PresentationReview — a system-tag the auto-apply sweep applied that ALSO looked
|
||||
like real content, flagged for operator review (milestone 141 + #1464).
|
||||
|
||||
When the auto-apply sweep hides an image as chrome (banner / editor screenshot)
|
||||
but the image ALSO scores highly on a content head, it still hides it but records
|
||||
this row so the Hidden view can surface it ("⚠ also looks like <conflict tag>")
|
||||
for a keep-hidden / un-hide decision. Resolved rows are pruned by retention.
|
||||
When a sweep applies a system tag but the image ALSO scores highly on a content
|
||||
head, it still applies the tag but records this row so a review strip can surface
|
||||
it ("⚠ also looks like <conflict tag>"). Two modes (#1464): 'chrome' (banner —
|
||||
image is HIDDEN, review is keep-hidden / un-hide) and 'process' (wip / editor
|
||||
screenshot — image stays VISIBLE, review is confirm / remove-tag). Resolved rows
|
||||
are pruned by retention.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, func
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .base import Base
|
||||
@@ -31,6 +33,12 @@ class PresentationReview(Base):
|
||||
ForeignKey("tag.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
conflict_score: Mapped[float] = mapped_column(Float, nullable=False)
|
||||
# Which sweep flagged this (#1464): 'chrome' (banner, hidden) or 'process'
|
||||
# (wip / editor screenshot, shown). Drives which review strip surfaces it and
|
||||
# what "resolve" means (un-hide vs remove-tag). Existing rows backfill 'chrome'.
|
||||
mode: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, default="chrome", server_default="chrome"
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
|
||||
@@ -43,14 +43,19 @@ class TagKind(StrEnum):
|
||||
# to keep historic tag rows queryable.
|
||||
|
||||
|
||||
# The seeded system tags (migration 0075). PRESENTATION tags additionally
|
||||
# hide from whole-image similarity results — they cluster on UI chrome, not
|
||||
# content. `wip` is real art: only the training pipelines exclude it.
|
||||
# The seeded system tags (migration 0075). Two behavior groups (#1464):
|
||||
# CHROME (banner): clusters on UI chrome, not content → HIDDEN from the default
|
||||
# gallery + from similarity; auto-applied via the sweep's chrome mode.
|
||||
# PROCESS (wip, editor screenshot): real-but-unfinished art / program screenshots
|
||||
# → SHOWN in the gallery (operator 2026-07-12), but excluded from the Explore
|
||||
# rabbit-hole; auto-applied via the sweep's process mode (provisional source,
|
||||
# ring-loud review guard).
|
||||
# ALL three are excluded from OTHER concepts' head/CCIP training (training-hygiene,
|
||||
# keyed on is_system); a system tag's OWN head trains on them — that's what makes
|
||||
# auto-flagging work.
|
||||
SYSTEM_TAG_NAMES = ("wip", "banner", "editor screenshot")
|
||||
PRESENTATION_SYSTEM_TAGS = ("banner", "editor screenshot")
|
||||
# `wip` marks real-but-unfinished art. It's kept in the gallery's own "similar"
|
||||
# results (#1274), but the Explore rabbit-hole opts to hide it (exclude_wip) so a
|
||||
# browse doesn't keep surfacing work-in-progress (operator, 2026-07-08).
|
||||
CHROME_SYSTEM_TAGS = ("banner",)
|
||||
PROCESS_SYSTEM_TAGS = ("wip", "editor screenshot")
|
||||
WIP_SYSTEM_TAG = "wip"
|
||||
|
||||
image_tag = Table(
|
||||
|
||||
@@ -31,7 +31,7 @@ from ..models import (
|
||||
Tag,
|
||||
TagPositiveConfirmation,
|
||||
)
|
||||
from ..models.tag import PRESENTATION_SYSTEM_TAGS, WIP_SYSTEM_TAG, image_tag
|
||||
from ..models.tag import CHROME_SYSTEM_TAGS, PROCESS_SYSTEM_TAGS, image_tag
|
||||
from .pagination import decode_cursor, encode_cursor
|
||||
from .tag_query import (
|
||||
fandom_join_alias,
|
||||
@@ -419,16 +419,17 @@ class GalleryService:
|
||||
async def _hidden_tag_ids(
|
||||
self, include_hidden, tag_ids, tag_or_groups,
|
||||
) -> list[int] | None:
|
||||
"""Presentation-chrome tag ids to implicitly exclude from a gallery query,
|
||||
or None. None when the caller asked to include hidden, when the operator
|
||||
is explicitly filtering FOR a presentation tag (they clearly want to see
|
||||
it), or when no presentation tags exist. (milestone 141)"""
|
||||
"""Chrome (banner) tag ids to implicitly exclude from a gallery query, or
|
||||
None. None when the caller asked to include hidden, when the operator is
|
||||
explicitly filtering FOR a chrome tag (they clearly want to see it), or when
|
||||
no chrome tags exist. (milestone 141; #1464: editor screenshot is now PROCESS
|
||||
— shown — so only `banner` hides here.)"""
|
||||
if include_hidden:
|
||||
return None
|
||||
rows = await self.session.execute(
|
||||
select(Tag.id).where(
|
||||
Tag.is_system.is_(True),
|
||||
Tag.name.in_(PRESENTATION_SYSTEM_TAGS),
|
||||
Tag.name.in_(CHROME_SYSTEM_TAGS),
|
||||
)
|
||||
)
|
||||
pres = [r[0] for r in rows]
|
||||
@@ -749,15 +750,16 @@ class GalleryService:
|
||||
eff = _effective_date_col()
|
||||
stmt = select(ImageRecord, Post.post_date, eff.label("eff"))
|
||||
stmt = _outer_join_primary_post(stmt)
|
||||
# Presentation images (banner / editor-screenshot system tags, #128)
|
||||
# cluster on UI chrome rather than content, so near any one of them
|
||||
# they'd fill the grid. Excluded from CANDIDATES only — the anchor
|
||||
# itself may be a banner. `wip` stays surfaced here by default (real art;
|
||||
# only the training pipelines exclude it), but the Explore rabbit-hole
|
||||
# passes exclude_wip to also drop work-in-progress (operator, 2026-07-08).
|
||||
excluded_system_tags = PRESENTATION_SYSTEM_TAGS
|
||||
# Chrome (banner, #128) clusters on UI rather than content, so near any one
|
||||
# of them they'd fill the grid → excluded from CANDIDATES always (the anchor
|
||||
# itself may be a banner). PROCESS art (wip / editor screenshot) stays
|
||||
# surfaced here by default (real content; only the training pipelines exclude
|
||||
# it), but the Explore rabbit-hole passes exclude_wip to also drop the whole
|
||||
# process group so a browse doesn't keep surfacing work-in-progress
|
||||
# (operator, 2026-07-08; #1464 — editor now rides with wip here).
|
||||
excluded_system_tags = CHROME_SYSTEM_TAGS
|
||||
if exclude_wip:
|
||||
excluded_system_tags = (*PRESENTATION_SYSTEM_TAGS, WIP_SYSTEM_TAG)
|
||||
excluded_system_tags = (*CHROME_SYSTEM_TAGS, *PROCESS_SYSTEM_TAGS)
|
||||
presentation = (
|
||||
select(image_tag.c.image_record_id)
|
||||
.join(Tag, Tag.id == image_tag.c.tag_id)
|
||||
|
||||
@@ -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]:
|
||||
|
||||
+24
-6
@@ -599,15 +599,33 @@ def scheduled_ccip_auto_apply() -> str:
|
||||
soft_time_limit=1800, time_limit=2100,
|
||||
)
|
||||
def scheduled_presentation_auto_apply() -> str:
|
||||
"""Auto-hide presentation chrome (banner / editor screenshot) on a daily
|
||||
passive sweep (#141). No-op unless presentation_auto_apply_enabled. Idempotent
|
||||
— already-hidden images are skipped — so an interrupted run simply re-runs next
|
||||
cycle (that IS the recovery). Wall-clock bounded by the task time limits."""
|
||||
from ..services.ml.heads import presentation_auto_apply_sweep
|
||||
"""Auto-hide presentation chrome (banner) on a daily passive sweep (#141).
|
||||
No-op unless presentation_auto_apply_enabled. Idempotent — already-tagged images
|
||||
are skipped — so an interrupted run simply re-runs next cycle (that IS the
|
||||
recovery). Wall-clock bounded by the task time limits."""
|
||||
from ..services.ml.heads import system_tag_auto_apply_sweep
|
||||
|
||||
SessionLocal = _sync_session_factory()
|
||||
with SessionLocal() as session:
|
||||
result = presentation_auto_apply_sweep(session)
|
||||
result = system_tag_auto_apply_sweep(session, mode="chrome")
|
||||
return f"applied={result['n_applied']} flagged={result['n_flagged']}"
|
||||
|
||||
|
||||
@celery.task(
|
||||
name="backend.app.tasks.ml.scheduled_process_auto_apply",
|
||||
soft_time_limit=1800, time_limit=2100,
|
||||
)
|
||||
def scheduled_process_auto_apply() -> str:
|
||||
"""Auto-apply the PROCESS system tags (wip / editor screenshot) on a daily
|
||||
passive sweep (#1464) — provisional source, ring-loud review guard, image stays
|
||||
VISIBLE. No-op unless process_auto_apply_enabled (opt-in). Idempotent —
|
||||
already-tagged/rejected images are skipped — so an interrupted run just re-runs
|
||||
next cycle (the recovery). Wall-clock bounded by the task time limits."""
|
||||
from ..services.ml.heads import system_tag_auto_apply_sweep
|
||||
|
||||
SessionLocal = _sync_session_factory()
|
||||
with SessionLocal() as session:
|
||||
result = system_tag_auto_apply_sweep(session, mode="process")
|
||||
return f"applied={result['n_applied']} flagged={result['n_flagged']}"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user