feat(explore): exclude WIP-tagged work from the Explore rabbit-hole
Explore's neighbour grid (/api/gallery/similar → gallery_service.similar) now takes an Explore-only exclude_wip flag that drops `wip` system-tagged images from the candidates, alongside the banner/editor presentation tags. The gallery's own "similar" button is unchanged (keeps wip, #1274) — only the Explore store passes exclude_wip=1. The anchor itself may still be a WIP; only neighbours are filtered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -145,6 +145,9 @@ async def similar():
|
||||
filters, _sort = _parse_filters()
|
||||
except (KeyError, ValueError):
|
||||
return jsonify({"error": "similar_to query param required"}), 400
|
||||
# Explore passes exclude_wip=1 to also drop work-in-progress from the
|
||||
# rabbit-hole; the gallery's own "similar" button omits it (keeps wip, #1274).
|
||||
exclude_wip = request.args.get("exclude_wip") in ("1", "true", "True")
|
||||
# post_id is the exclusive post-detail view — not a similarity scope.
|
||||
# include_hidden is a gallery-browse flag; similar() has its OWN presentation
|
||||
# exclusion (a similarity-quality concern, #1274), so drop it here (#141).
|
||||
@@ -154,7 +157,8 @@ async def similar():
|
||||
async with get_session() as session:
|
||||
svc = GalleryService(session)
|
||||
try:
|
||||
images = await svc.similar(image_id=similar_to, limit=limit, **scope)
|
||||
images = await svc.similar(
|
||||
image_id=similar_to, limit=limit, exclude_wip=exclude_wip, **scope)
|
||||
except ValueError as exc:
|
||||
return jsonify({"error": str(exc)}), 400
|
||||
if images is None:
|
||||
|
||||
@@ -48,6 +48,10 @@ class TagKind(StrEnum):
|
||||
# content. `wip` is real art: only the training pipelines exclude it.
|
||||
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).
|
||||
WIP_SYSTEM_TAG = "wip"
|
||||
|
||||
image_tag = Table(
|
||||
"image_tag",
|
||||
|
||||
@@ -31,7 +31,7 @@ from ..models import (
|
||||
Tag,
|
||||
TagPositiveConfirmation,
|
||||
)
|
||||
from ..models.tag import PRESENTATION_SYSTEM_TAGS, image_tag
|
||||
from ..models.tag import PRESENTATION_SYSTEM_TAGS, WIP_SYSTEM_TAG, image_tag
|
||||
from .pagination import decode_cursor, encode_cursor
|
||||
from .tag_query import (
|
||||
fandom_join_alias,
|
||||
@@ -715,6 +715,7 @@ class GalleryService:
|
||||
platform: str | None = None,
|
||||
untagged: bool = False, no_artist: bool = False,
|
||||
date_from: datetime | None = None, date_to: datetime | None = None,
|
||||
exclude_wip: bool = False,
|
||||
) -> list[GalleryImage] | None:
|
||||
"""Visual "more like this": images near `image_id`'s SigLIP embedding
|
||||
(pgvector, HNSW-indexed — alembic 0036), then DIVERSIFIED so the result
|
||||
@@ -751,14 +752,18 @@ class GalleryService:
|
||||
# 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, and `wip` stays surfaced (real art; only
|
||||
# the training pipelines exclude it).
|
||||
# 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
|
||||
if exclude_wip:
|
||||
excluded_system_tags = (*PRESENTATION_SYSTEM_TAGS, WIP_SYSTEM_TAG)
|
||||
presentation = (
|
||||
select(image_tag.c.image_record_id)
|
||||
.join(Tag, Tag.id == image_tag.c.tag_id)
|
||||
.where(
|
||||
Tag.is_system.is_(True),
|
||||
Tag.name.in_(PRESENTATION_SYSTEM_TAGS),
|
||||
Tag.name.in_(excluded_system_tags),
|
||||
)
|
||||
)
|
||||
stmt = stmt.where(
|
||||
|
||||
@@ -44,7 +44,9 @@ export const useExploreStore = defineStore('explore', () => {
|
||||
// empty and let the view explain why (anchor.has_embedding === false).
|
||||
if (detail.has_embedding) {
|
||||
const body = await api.get('/api/gallery/similar', {
|
||||
params: { similar_to: numId, limit: NEIGHBOR_LIMIT },
|
||||
// exclude_wip: keep work-in-progress out of the Explore rabbit-hole
|
||||
// (the gallery's own "similar" button still shows it) — operator 2026-07-08.
|
||||
params: { similar_to: numId, limit: NEIGHBOR_LIMIT, exclude_wip: 1 },
|
||||
})
|
||||
if (!t.isCurrent()) return
|
||||
neighbors.value = body.images || []
|
||||
|
||||
@@ -98,6 +98,30 @@ async def test_similar_excludes_presentation_tagged_images(db):
|
||||
assert {i.id for i in res_from_banner} == {src.id, wipped.id, plain.id}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_similar_exclude_wip_drops_wip_neighbors(db):
|
||||
"""Explore passes exclude_wip=True to also hide work-in-progress from the
|
||||
rabbit-hole (banner is always hidden; wip only when asked)."""
|
||||
src = await _img(db, 1, _vec(1, 0))
|
||||
bannered = await _img(db, 2, _vec(1, 0.02)) # always hidden
|
||||
wipped = await _img(db, 3, _vec(1, 0.3)) # hidden only with exclude_wip
|
||||
plain = await _img(db, 4, _vec(1, 0.6))
|
||||
banner_tag = (await db.execute(select(Tag).where(
|
||||
Tag.is_system.is_(True), Tag.name == "banner"))).scalar_one()
|
||||
wip_tag = (await db.execute(select(Tag).where(
|
||||
Tag.is_system.is_(True), Tag.name == "wip"))).scalar_one()
|
||||
await db.execute(image_tag.insert().values(
|
||||
image_record_id=bannered.id, tag_id=banner_tag.id, source="manual"))
|
||||
await db.execute(image_tag.insert().values(
|
||||
image_record_id=wipped.id, tag_id=wip_tag.id, source="manual"))
|
||||
svc = GalleryService(db)
|
||||
res = await svc.similar(src.id, limit=10, exclude_wip=True)
|
||||
assert [i.id for i in res] == [plain.id] # wip + banner both gone
|
||||
# Default (gallery "similar" button) still keeps wip (#1274).
|
||||
res_default = await svc.similar(src.id, limit=10)
|
||||
assert wipped.id in {i.id for i in res_default}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_similar_composes_with_tag_filter(db):
|
||||
src = await _img(db, 1, _vec(1, 0))
|
||||
|
||||
Reference in New Issue
Block a user