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, ImportSettings,
Post, Post,
Source, Source,
Tag,
TagKind,
) )
from ..models.tag import image_tag
from ..utils.paths import derive_subdir, derive_top_level_artist, hash_suffixed_name from ..utils.paths import derive_subdir, derive_top_level_artist, hash_suffixed_name
from ..utils.phash import compute_phash, find_similar from ..utils.phash import compute_phash, find_similar
from ..utils.sidecar import find_sidecar, parse_sidecar from ..utils.sidecar import find_sidecar, parse_sidecar
@@ -244,7 +241,7 @@ class Importer:
artist = None artist = None
artist_name = derive_top_level_artist(source, self.import_root) artist_name = derive_top_level_artist(source, self.import_root)
if artist_name: 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). # Sidecar provenance (best-effort; never fails the import).
self._apply_sidecar(record, source, artist) self._apply_sidecar(record, source, artist)
@@ -266,33 +263,15 @@ class Importer:
self.session.flush() self.session.flush()
return artist return artist
def _attach_artist_tag(self, image_id: int, artist_name: str) -> Artist: def _attach_artist(self, record: ImageRecord, artist_name: str) -> Artist:
"""Idempotently creates Artist + artist tag, attaches to image. """Upsert the Artist and set it as the image's canonical artist.
Returns the Artist (sidecar provenance attaches its Source to it).""" 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) artist = self._upsert_artist(artist_name)
if record.artist_id is None:
tag = self.session.execute( record.artist_id = artist.id
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)
self.session.flush() 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 return artist
@staticmethod @staticmethod
@@ -339,6 +318,9 @@ class Importer:
) )
return return
if record.artist_id is None:
record.artist_id = artist.id
platform = sd.platform or "unknown" platform = sd.platform or "unknown"
url = sd.post_url or f"sidecar:{platform}" url = sd.post_url or f"sidecar:{platform}"
src = self.session.execute( 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