Files
photoanalyzer/photo_pipeline/models/duplicates.py

63 lines
2.8 KiB
Python

"""Duplicate clusters, their members, and not-duplicate negative links.
A cluster is a reviewable grouping produced by a detection ``method`` (exact,
pixel, or perceptual) at a ``confidence`` band. Exact/pixel clusters can be
decided automatically; perceptual clusters stay open for human review. Every
member carries the evidence behind its inclusion. Negative links record
``not_duplicate`` decisions so rescans do not re-suggest a rejected pair.
"""
from __future__ import annotations
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from photo_pipeline.db import Base
class DuplicateCluster(Base):
__tablename__ = "duplicate_clusters"
id: Mapped[str] = mapped_column(String, primary_key=True)
method: Mapped[str] = mapped_column(String, nullable=False) # exact|pixel|perceptual
confidence: Mapped[str] = mapped_column(String, nullable=False) # band name
state: Mapped[str] = mapped_column(String, nullable=False) # open|decided|dismissed|reopened
decision: Mapped[str | None] = mapped_column(String) # canonical|not_duplicate|deferred
canonical_asset_id: Mapped[str | None] = mapped_column(ForeignKey("assets.id"))
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
decided_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
class DuplicateMember(Base):
__tablename__ = "duplicate_members"
cluster_id: Mapped[str] = mapped_column(
ForeignKey("duplicate_clusters.id"), primary_key=True
)
asset_id: Mapped[str] = mapped_column(ForeignKey("assets.id"), primary_key=True)
role: Mapped[str] = mapped_column(String, nullable=False, default="member") # member|canonical|variant
distance: Mapped[int | None] = mapped_column(Integer) # phash distance to representative
evidence: Mapped[str | None] = mapped_column(String) # JSON blob explaining inclusion
added_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
class DuplicateNegativeLink(Base):
__tablename__ = "duplicate_negative_links"
asset_a: Mapped[str] = mapped_column(ForeignKey("assets.id"), primary_key=True)
asset_b: Mapped[str] = mapped_column(ForeignKey("assets.id"), primary_key=True)
reason: Mapped[str | None] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)