feat: retire the sketch/doodle WIP title tier, and the review strip asks "Is this a WIP?" (milestone 430)
CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 3s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 23s
CI and images / backend-lint-and-test (push) Successful in 32s
CI and images / integration (push) Successful in 2m24s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 5s
CI and images / build-web (push) Successful in 1m35s
CI and images / smoke-web (push) Successful in 56s
CI and images / promote (push) Successful in 1s
CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 3s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 23s
CI and images / backend-lint-and-test (push) Successful in 32s
CI and images / integration (push) Successful in 2m24s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 5s
CI and images / build-web (push) Successful in 1m35s
CI and images / smoke-web (push) Successful in 56s
CI and images / promote (push) Successful in 1s
The soft tier (#1474) tagged `wip` on any post titled sketch, doodle or scribble: 6,096 of the library's 8,876 wip tags. Its conflict audit flagged most of them, because finished art scores >= 0.5 on some content head, which filled the Gallery strip with 2,086 cards. A "sketch" is usually finished work, so the operator retired the tier. The artist's own "WIP" / "work in progress" title rule and human wip tags stay. - Removed: the soft matcher, source and prefilter; the importer and backfill soft branches; soft_wip_conflict_audit with its task and beat; the wip_soft_title_tagging_enabled setting, API field and toggle; and wip_title_soft from _AUTO_SOURCES. - Migration 0112: a soft tag the operator confirmed, or kept from the strip, becomes `manual`. Every other soft tag is deleted, along with the open review cards whose tag is gone. The column is dropped. - Review strip (#4424): each card asks "Is this a WIP?" (or banner, and so on), and the buttons read "Is a WIP" / "Is not a WIP". The content tag it also scored on is shown as the reason it was flagged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
@@ -126,53 +126,17 @@ def test_process_sweep_flags_conflict_with_process_mode(db_sync):
|
||||
|
||||
|
||||
def test_process_auto_source_never_trains_head(db_sync):
|
||||
# The runaway break: provisional wip tags (process sweep 'process_auto', soft
|
||||
# title 'wip_title_soft') are NOT training positives; a HARD title-heuristic /
|
||||
# manual one IS. So the head learns only from trusted labels, never its own
|
||||
# output or the low-precision sketch/doodle tier (#1464 + #1474).
|
||||
# The runaway break: provisional wip tags (process sweep 'process_auto') are NOT
|
||||
# training positives; a title-heuristic / manual one IS. So the head learns only
|
||||
# from trusted labels, never its own output (#1464).
|
||||
wip = _system_tag(db_sync, "wip")
|
||||
auto_img = _img(db_sync, "f" * 64, _emb(0))
|
||||
soft_img = _img(db_sync, "9" * 64, _emb(2))
|
||||
title_img = _img(db_sync, "0" * 64, _emb(1))
|
||||
db_sync.execute(image_tag.insert().values(
|
||||
image_record_id=auto_img.id, tag_id=wip.id, source="process_auto"))
|
||||
db_sync.execute(image_tag.insert().values(
|
||||
image_record_id=soft_img.id, tag_id=wip.id, source="wip_title_soft"))
|
||||
db_sync.execute(image_tag.insert().values(
|
||||
image_record_id=title_img.id, tag_id=wip.id, source="wip_title"))
|
||||
db_sync.commit()
|
||||
positives = set(_ids_with_tag(db_sync, wip.id))
|
||||
assert title_img.id in positives # trusted HARD label trains the head
|
||||
assert auto_img.id not in positives # its own auto-applied output does NOT
|
||||
assert soft_img.id not in positives # low-precision soft tier does NOT
|
||||
|
||||
|
||||
def test_soft_wip_conflict_audit_flags_ring_loud(db_sync):
|
||||
# A soft-tagged image (sketch/doodle title) that ALSO scores high on a content
|
||||
# head is probably finished art mis-tagged — flagged for review; a quiet one is not.
|
||||
from backend.app.services.ml.heads import soft_wip_conflict_audit
|
||||
|
||||
s = db_sync.execute(select(MLSettings).where(MLSettings.id == 1)).scalar_one()
|
||||
s.process_conflict_threshold = 0.6
|
||||
wip = _system_tag(db_sync, "wip")
|
||||
content = Tag(name="looksreal", kind=TagKind.general)
|
||||
db_sync.add(content)
|
||||
db_sync.flush()
|
||||
_head(db_sync, content.id, 0, weight=1.0) # sigmoid(1)=0.73 > 0.6 conflict
|
||||
ring = _img(db_sync, "1" * 64, _emb(0)) # scores on the content head
|
||||
quiet = _img(db_sync, "2" * 64, _emb(5)) # orthogonal → 0.5 < 0.6
|
||||
for img in (ring, quiet):
|
||||
db_sync.execute(image_tag.insert().values(
|
||||
image_record_id=img.id, tag_id=wip.id, source="wip_title_soft"))
|
||||
db_sync.commit()
|
||||
|
||||
res = soft_wip_conflict_audit(db_sync)
|
||||
assert res["n_flagged"] == 1
|
||||
flag = db_sync.execute(
|
||||
select(PresentationReview).where(PresentationReview.image_record_id == ring.id)
|
||||
).scalar_one()
|
||||
assert flag.mode == "process"
|
||||
assert flag.conflict_tag_id == content.id
|
||||
assert db_sync.execute(
|
||||
select(PresentationReview).where(PresentationReview.image_record_id == quiet.id)
|
||||
).scalar_one_or_none() is None
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Migration 0112 (milestone 430, #4428): retiring the sketch/doodle WIP title tier.
|
||||
|
||||
Runs the migration's data step against real rows. The soft tags the operator stood
|
||||
behind (confirmed, or kept through the review strip) survive as `manual`; the rest go,
|
||||
and so do the unresolved review cards whose tag went with them. Hard title tags and
|
||||
their flags are untouched.
|
||||
"""
|
||||
import importlib.util
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from backend.app.models import PresentationReview, TagPositiveConfirmation
|
||||
from backend.app.models.tag import image_tag
|
||||
from backend.app.services.wip_title import resolve_wip_tag_id
|
||||
from tests.factories import make_image as _img
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
_MIGRATION = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "alembic" / "versions" / "0112_retire_soft_wip_title.py"
|
||||
)
|
||||
|
||||
|
||||
def _retire():
|
||||
spec = importlib.util.spec_from_file_location("m0112", _MIGRATION)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod.retire_soft_wip_tags
|
||||
|
||||
|
||||
def _source(db, image_id, tag_id):
|
||||
return db.execute(
|
||||
select(image_tag.c.source)
|
||||
.where(image_tag.c.image_record_id == image_id)
|
||||
.where(image_tag.c.tag_id == tag_id)
|
||||
).scalar_one_or_none()
|
||||
|
||||
|
||||
def _review(db, image_id, tag_id):
|
||||
return db.execute(
|
||||
select(PresentationReview)
|
||||
.where(PresentationReview.image_record_id == image_id)
|
||||
.where(PresentationReview.tag_id == tag_id)
|
||||
).scalar_one_or_none()
|
||||
|
||||
|
||||
def _flag(db, image_id, tag_id, *, resolved):
|
||||
db.add(PresentationReview(
|
||||
image_record_id=image_id, tag_id=tag_id, conflict_score=0.8, mode="process",
|
||||
resolved_at=datetime.now(UTC) if resolved else None,
|
||||
))
|
||||
|
||||
|
||||
def test_retire_soft_wip_keeps_human_judged_and_clears_the_rest(db_sync):
|
||||
wip = resolve_wip_tag_id(db_sync)
|
||||
plain = _img(db_sync, "a" * 64) # soft, never looked at → removed
|
||||
confirmed = _img(db_sync, "b" * 64) # soft + confirmed → kept as manual
|
||||
kept = _img(db_sync, "c" * 64) # soft + "Keep tag" in the strip → manual
|
||||
pending = _img(db_sync, "d" * 64) # soft + open card → tag and card removed
|
||||
hard = _img(db_sync, "e" * 64) # hard title tag + open card → untouched
|
||||
for img in (plain, confirmed, kept, pending):
|
||||
db_sync.execute(image_tag.insert().values(
|
||||
image_record_id=img.id, tag_id=wip, source="wip_title_soft"))
|
||||
db_sync.execute(image_tag.insert().values(
|
||||
image_record_id=hard.id, tag_id=wip, source="wip_title"))
|
||||
db_sync.add(TagPositiveConfirmation(image_record_id=confirmed.id, tag_id=wip))
|
||||
_flag(db_sync, kept.id, wip, resolved=True)
|
||||
_flag(db_sync, pending.id, wip, resolved=False)
|
||||
_flag(db_sync, hard.id, wip, resolved=False)
|
||||
db_sync.flush()
|
||||
|
||||
_retire()(db_sync.connection())
|
||||
db_sync.expire_all()
|
||||
|
||||
assert _source(db_sync, plain.id, wip) is None
|
||||
assert _source(db_sync, confirmed.id, wip) == "manual"
|
||||
assert _source(db_sync, kept.id, wip) == "manual"
|
||||
assert _source(db_sync, pending.id, wip) is None
|
||||
assert _source(db_sync, hard.id, wip) == "wip_title"
|
||||
|
||||
assert _review(db_sync, pending.id, wip) is None
|
||||
assert _review(db_sync, kept.id, wip) is not None # resolved history stays
|
||||
assert _review(db_sync, hard.id, wip) is not None # its tag is still on
|
||||
assert db_sync.execute(
|
||||
select(image_tag.c.image_record_id).where(image_tag.c.source == "wip_title_soft")
|
||||
).first() is None
|
||||
+1
-27
@@ -8,7 +8,7 @@ negative cases (substrings like ``swipe`` / ``wiped``) are the load-bearing ones
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from backend.app.services.wip_title import matches_soft_wip_title, matches_wip_title
|
||||
from backend.app.services.wip_title import matches_wip_title
|
||||
|
||||
|
||||
@pytest.mark.parametrize("title", [
|
||||
@@ -48,29 +48,3 @@ def test_matches_positive(title):
|
||||
])
|
||||
def test_matches_negative(title):
|
||||
assert matches_wip_title(title) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("title", [
|
||||
"quick sketch",
|
||||
"Nami sketch",
|
||||
"morning doodle",
|
||||
"some doodles",
|
||||
"sketches from today",
|
||||
"a little scribble",
|
||||
"SKETCH",
|
||||
])
|
||||
def test_soft_matches_positive(title):
|
||||
assert matches_soft_wip_title(title) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("title", [
|
||||
None,
|
||||
"",
|
||||
"sketchbook tour", # 'sketch' inside sketchbook — must NOT match
|
||||
"kadoodle mascot", # 'doodle' mid-word
|
||||
"the final piece",
|
||||
"WIP", # a HARD cue is not a SOFT cue
|
||||
"prescribed colours", # 'scrib' inside prescribed — must NOT match
|
||||
])
|
||||
def test_soft_matches_negative(title):
|
||||
assert matches_soft_wip_title(title) is False
|
||||
|
||||
@@ -10,7 +10,6 @@ from backend.app.celery_app import celery
|
||||
from backend.app.models import Artist, ImageProvenance, ImageRecord, Post, Source
|
||||
from backend.app.models.tag import image_tag
|
||||
from backend.app.services.wip_title import (
|
||||
WIP_TITLE_SOFT_SOURCE,
|
||||
apply_wip_image_tags,
|
||||
resolve_wip_tag_id,
|
||||
)
|
||||
@@ -108,14 +107,3 @@ def test_backfill_tags_only_wip_titled_posts(db_sync):
|
||||
|
||||
# Idempotent: a second sweep finds the tag already present and applies nothing.
|
||||
assert backfill_wip_title_tags.apply().get() == 0
|
||||
|
||||
|
||||
def test_apply_soft_source_stamps_wip_title_soft(db_sync):
|
||||
# The soft tier (#1474) stamps a distinct provisional source.
|
||||
tag_id = resolve_wip_tag_id(db_sync)
|
||||
rec = _img(db_sync)
|
||||
db_sync.commit()
|
||||
assert apply_wip_image_tags(
|
||||
db_sync, [rec.id], tag_id, source=WIP_TITLE_SOFT_SOURCE
|
||||
) == 1
|
||||
assert _wip_source(db_sync, rec.id, tag_id) == "wip_title_soft"
|
||||
|
||||
Reference in New Issue
Block a user