SubscribeStar .art age fix, modal tag-flow sync, system tags / training hygiene (#128) #190

Merged
bvandeusen merged 9 commits from dev into main 2026-07-03 09:16:16 -04:00
3 changed files with 47 additions and 1 deletions
Showing only changes of commit 723f023e6a - Show all commits
+6
View File
@@ -43,6 +43,12 @@ class TagKind(StrEnum):
# to keep historic tag rows queryable. # 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.
SYSTEM_TAG_NAMES = ("wip", "banner", "editor screenshot")
PRESENTATION_SYSTEM_TAGS = ("banner", "editor screenshot")
image_tag = Table( image_tag = Table(
"image_tag", "image_tag",
Base.metadata, Base.metadata,
+15 -1
View File
@@ -23,7 +23,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from ..models import Artist, ImageProvenance, ImageRecord, Post, Source, Tag from ..models import Artist, ImageProvenance, ImageRecord, Post, Source, Tag
from ..models.tag import image_tag from ..models.tag import PRESENTATION_SYSTEM_TAGS, image_tag
from .pagination import decode_cursor, encode_cursor from .pagination import decode_cursor, encode_cursor
from .tag_query import ( from .tag_query import (
fandom_join_alias, fandom_join_alias,
@@ -693,9 +693,23 @@ class GalleryService:
eff = _effective_date_col() eff = _effective_date_col()
stmt = select(ImageRecord, Post.post_date, eff.label("eff")) stmt = select(ImageRecord, Post.post_date, eff.label("eff"))
stmt = _outer_join_primary_post(stmt) 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, and `wip` stays surfaced (real art; only
# the training pipelines exclude it).
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),
)
)
stmt = stmt.where( stmt = stmt.where(
ImageRecord.siglip_embedding.is_not(None), ImageRecord.siglip_embedding.is_not(None),
ImageRecord.id != image_id, ImageRecord.id != image_id,
ImageRecord.id.not_in(presentation),
) )
stmt = _apply_scope( stmt = _apply_scope(
stmt, tag_ids=tag_ids, post_id=None, stmt, tag_ids=tag_ids, post_id=None,
+26
View File
@@ -4,6 +4,7 @@ precomputed SigLIP image embeddings. No query-time ML inference."""
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
import pytest import pytest
from sqlalchemy import select
from backend.app.models import ImageRecord, Tag, TagKind from backend.app.models import ImageRecord, Tag, TagKind
from backend.app.models.tag import image_tag from backend.app.models.tag import image_tag
@@ -72,6 +73,31 @@ async def test_similar_missing_source_returns_none(db):
assert await svc.similar(99999, limit=10) is None assert await svc.similar(99999, limit=10) is None
@pytest.mark.asyncio
async def test_similar_excludes_presentation_tagged_images(db):
"""banner / editor-screenshot system tags (#128) hide from similar
RESULTS — they cluster on UI chrome, not content. A banner anchor still
gets results; `wip`-tagged images stay in (real art)."""
src = await _img(db, 1, _vec(1, 0))
bannered = await _img(db, 2, _vec(1, 0.02)) # nearest, but a banner
wipped = await _img(db, 3, _vec(1, 0.3))
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)
assert [i.id for i in res] == [wipped.id, plain.id]
# A presentation-tagged ANCHOR still answers — only candidates hide.
res_from_banner = await svc.similar(bannered.id, limit=10)
assert {i.id for i in res_from_banner} == {src.id, wipped.id, plain.id}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_similar_composes_with_tag_filter(db): async def test_similar_composes_with_tag_filter(db):
src = await _img(db, 1, _vec(1, 0)) src = await _img(db, 1, _vec(1, 0))