feat(deep-scan): Importer(deep=) re-derive on sha-match; import_file wires batch.scan_mode

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 15:32:26 -04:00
parent 9535533385
commit 5dfdbf5a60
3 changed files with 139 additions and 4 deletions
+37 -4
View File
@@ -117,12 +117,14 @@ class Importer:
import_root: Path,
thumbnailer: Thumbnailer,
settings: ImportSettings,
deep: bool = False,
):
self.session = session
self.images_root = images_root
self.import_root = import_root
self.thumbnailer = thumbnailer
self.settings = settings
self.deep = deep
self.attachments = AttachmentStore(images_root)
def import_one(self, source: Path) -> ImportResult:
@@ -290,10 +292,12 @@ class Importer:
existing_stmt = select(ImageRecord).where(ImageRecord.sha256 == sha)
existing = self.session.execute(existing_stmt).scalar_one_or_none()
if existing:
return ImportResult(
status="skipped", skip_reason=SkipReason.duplicate_hash,
image_id=existing.id, error="sha256 already present",
)
if not self.deep:
return ImportResult(
status="skipped", skip_reason=SkipReason.duplicate_hash,
image_id=existing.id, error="sha256 already present",
)
return self._deep_rederive(existing, source, attribution_path)
# Perceptual near-dup (images only; videos keep phash NULL).
phash = None
@@ -374,6 +378,35 @@ class Importer:
self.session.commit()
return ImportResult(status="imported", image_id=record.id)
def _deep_rederive(
self, existing: ImageRecord, source: Path, attribution_path: Path
) -> ImportResult:
"""Deep scan: backfill phash/provenance/artist on an
already-imported record. METADATA ONLY — never re-runs the pHash
near-dup / supersede path. NULL-only, idempotent."""
if existing.phash is None and not is_video(source):
try:
with Image.open(source) as im:
ph = compute_phash(im)
if ph is not None:
existing.phash = ph
except Exception as exc:
log.warning("deep rephash failed for %s: %s", source, exc)
artist = None
name = derive_top_level_artist(attribution_path, self.import_root)
if name:
artist = self._upsert_artist(name)
if existing.artist_id is None:
existing.artist_id = artist.id
self._apply_sidecar(existing, attribution_path, artist)
self.session.commit()
return ImportResult(
status="skipped", skip_reason=SkipReason.duplicate_hash,
image_id=existing.id, error="deep: re-derived",
)
def _upsert_artist(self, name: str) -> Artist:
slug = slugify(name)
artist = self.session.execute(
+3
View File
@@ -55,12 +55,15 @@ def import_media_file(self, import_task_id: int) -> dict:
select(ImportSettings).where(ImportSettings.id == 1)
).scalar_one()
import_root = Path(settings.import_scan_path)
batch = session.get(ImportBatch, task.batch_id)
deep = bool(batch and batch.scan_mode == "deep")
importer = Importer(
session=session,
images_root=IMAGES_ROOT,
import_root=import_root,
thumbnailer=Thumbnailer(images_root=IMAGES_ROOT),
settings=settings,
deep=deep,
)
try: