feat(provenance): importer sets image_record.artist_id, stops minting artist tags

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 21:33:55 -04:00
parent 63e7185811
commit 914cf14d9e
2 changed files with 65 additions and 29 deletions
+11 -29
View File
@@ -27,10 +27,7 @@ from ..models import (
ImportSettings,
Post,
Source,
Tag,
TagKind,
)
from ..models.tag import image_tag
from ..utils.paths import derive_subdir, derive_top_level_artist, hash_suffixed_name
from ..utils.phash import compute_phash, find_similar
from ..utils.sidecar import find_sidecar, parse_sidecar
@@ -244,7 +241,7 @@ class Importer:
artist = None
artist_name = derive_top_level_artist(source, self.import_root)
if artist_name:
artist = self._attach_artist_tag(record.id, artist_name)
artist = self._attach_artist(record, artist_name)
# Sidecar provenance (best-effort; never fails the import).
self._apply_sidecar(record, source, artist)
@@ -266,33 +263,15 @@ class Importer:
self.session.flush()
return artist
def _attach_artist_tag(self, image_id: int, artist_name: str) -> Artist:
"""Idempotently creates Artist + artist tag, attaches to image.
Returns the Artist (sidecar provenance attaches its Source to it)."""
def _attach_artist(self, record: ImageRecord, artist_name: str) -> Artist:
"""Upsert the Artist and set it as the image's canonical artist.
Artist-kind tags were retired in FC-2d-vii-c — the Artist row is
the single source of truth. Returns the Artist (sidecar
provenance attaches its Source to it)."""
artist = self._upsert_artist(artist_name)
tag = self.session.execute(
select(Tag).where(Tag.name == artist_name).where(Tag.kind == TagKind.artist)
).scalar_one_or_none()
if tag is None:
tag = Tag(name=artist_name, kind=TagKind.artist)
self.session.add(tag)
if record.artist_id is None:
record.artist_id = artist.id
self.session.flush()
# Idempotent association; using a raw insert + on-conflict-do-nothing
# is cleaner here than fetching first, but for a sync session we just
# check existence.
already = self.session.execute(
select(image_tag.c.tag_id)
.where(image_tag.c.image_record_id == image_id)
.where(image_tag.c.tag_id == tag.id)
).scalar_one_or_none()
if already is None:
self.session.execute(
image_tag.insert().values(
image_record_id=image_id, tag_id=tag.id, source="auto",
)
)
return artist
@staticmethod
@@ -339,6 +318,9 @@ class Importer:
)
return
if record.artist_id is None:
record.artist_id = artist.id
platform = sd.platform or "unknown"
url = sd.post_url or f"sidecar:{platform}"
src = self.session.execute(
+54
View File
@@ -0,0 +1,54 @@
"""FC-2d-vii-c: importer sets artist_id, creates no artist tags."""
import pytest
from PIL import Image
from sqlalchemy import func, select
from backend.app.models import Artist, ImageRecord, ImportSettings, Tag, TagKind
from backend.app.services.importer import Importer
from backend.app.services.thumbnailer import Thumbnailer
pytestmark = pytest.mark.integration
@pytest.fixture
def import_layout(tmp_path):
import_root = tmp_path / "import"
images_root = tmp_path / "images"
import_root.mkdir()
images_root.mkdir()
return import_root, images_root
@pytest.fixture
def importer(db_sync, import_layout):
import_root, images_root = import_layout
settings = db_sync.execute(
select(ImportSettings).where(ImportSettings.id == 1)
).scalar_one()
return Importer(
session=db_sync, images_root=images_root, import_root=import_root,
thumbnailer=Thumbnailer(images_root=images_root), settings=settings,
)
def _img(path):
path.parent.mkdir(parents=True, exist_ok=True)
Image.new("RGB", (64, 64), (123, 200, 50)).save(path, "JPEG")
def test_folder_import_sets_artist_id_no_tag(importer, import_layout):
import_root, _ = import_layout
m = import_root / "Alice" / "pic.jpg"
_img(m)
r = importer.import_one(m)
assert r.status == "imported"
rec = importer.session.get(ImageRecord, r.image_id)
artist = importer.session.execute(
select(Artist).where(Artist.slug == "alice")
).scalar_one()
assert rec.artist_id == artist.id
n_artist_tags = importer.session.execute(
select(func.count()).select_from(Tag).where(Tag.kind == TagKind.artist)
).scalar_one()
assert n_artist_tags == 0