"""ImagePrediction — one row per (image, tagger vocab prediction). Replaces the image_record.tagger_predictions JSON blob (#768). Storing the raw Camie/booru vocab name (not a tag_id) preserves the suggestion read path's semantics: raw_name → canonical Tag resolution happens at read time via the alias map, and accepting a prediction can CREATE the Tag. The store floor (ml_settings.tagger_store_floor) is applied at WRITE time, so only predictions >= the floor land here. """ from sqlalchemy import Float, ForeignKey, Index, String, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from .base import Base class ImagePrediction(Base): __tablename__ = "image_prediction" __table_args__ = ( UniqueConstraint( "image_record_id", "raw_name", name="image_raw_name", ), # Per-image read (suggestion build) and the "images with tag X above # Y" query the JSON blob never allowed. Index("ix_image_prediction_image", "image_record_id"), Index("ix_image_prediction_name_score", "raw_name", "score"), ) id: Mapped[int] = mapped_column(primary_key=True) image_record_id: Mapped[int] = mapped_column( ForeignKey("image_record.id", ondelete="CASCADE"), nullable=False, ) # The raw tagger vocab key (booru form) — NOT a tag_id. Resolved to a # canonical Tag at read time, exactly as the old JSON keys were. raw_name: Mapped[str] = mapped_column(String(255), nullable=False) category: Mapped[str] = mapped_column(String(64), nullable=False) score: Mapped[float] = mapped_column(Float, nullable=False)