feat(fc2b): schema migration 0003 — ML pipeline tables
Renames image_record.wd14_* -> tagger_* (we're on Camie now, not WD14). Adds tag_allowlist (auto-apply opt-in, per-tag confidence), tag_suggestion_rejection (per-image dismissals), tag_alias (composite (string, category) -> canonical tag, resolved at read time), tag_reference_embedding (per-tag SigLIP centroids), and the ml_settings singleton (per-category + centroid thresholds, model version pins). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,14 @@ from .image_record import ImageRecord
|
||||
from .import_batch import ImportBatch
|
||||
from .import_settings import ImportSettings
|
||||
from .import_task import ImportTask
|
||||
from .ml_settings import MLSettings
|
||||
from .post import Post
|
||||
from .source import Source
|
||||
from .tag import Tag, TagKind, image_tag
|
||||
from .tag_alias import TagAlias
|
||||
from .tag_allowlist import TagAllowlist
|
||||
from .tag_reference_embedding import TagReferenceEmbedding
|
||||
from .tag_suggestion_rejection import TagSuggestionRejection
|
||||
|
||||
__all__ = [
|
||||
"Base",
|
||||
@@ -28,4 +33,9 @@ __all__ = [
|
||||
"ImportBatch",
|
||||
"ImportTask",
|
||||
"ImportSettings",
|
||||
"MLSettings",
|
||||
"TagAlias",
|
||||
"TagAllowlist",
|
||||
"TagReferenceEmbedding",
|
||||
"TagSuggestionRejection",
|
||||
]
|
||||
|
||||
@@ -56,8 +56,8 @@ class ImageRecord(Base):
|
||||
)
|
||||
|
||||
# ML fields (populated by FC-2's ml-worker)
|
||||
wd14_predictions: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
wd14_model_version: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
tagger_predictions: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||
tagger_model_version: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
# 1152 = SigLIP-so400m embedding dim. Swapping models in FC-2 may require
|
||||
# a column-width migration.
|
||||
siglip_embedding: Mapped[list[float] | None] = mapped_column(Vector(1152), nullable=True)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""MLSettings — single-row table holding ML pipeline tunables."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import CheckConstraint, DateTime, Float, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class MLSettings(Base):
|
||||
__tablename__ = "ml_settings"
|
||||
__table_args__ = (CheckConstraint("id = 1", name="ck_ml_settings_singleton"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
suggestion_threshold_artist: Mapped[float] = mapped_column(
|
||||
Float, nullable=False, default=0.30
|
||||
)
|
||||
suggestion_threshold_character: Mapped[float] = mapped_column(
|
||||
Float, nullable=False, default=0.50
|
||||
)
|
||||
suggestion_threshold_copyright: Mapped[float] = mapped_column(
|
||||
Float, nullable=False, default=0.50
|
||||
)
|
||||
suggestion_threshold_general: Mapped[float] = mapped_column(
|
||||
Float, nullable=False, default=0.95
|
||||
)
|
||||
centroid_similarity_threshold: Mapped[float] = mapped_column(
|
||||
Float, nullable=False, default=0.55
|
||||
)
|
||||
min_reference_images: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=5
|
||||
)
|
||||
tagger_model_version: Mapped[str] = mapped_column(
|
||||
String(128), nullable=False, default="camie-tagger-v2"
|
||||
)
|
||||
embedder_model_version: Mapped[str] = mapped_column(
|
||||
String(128), nullable=False, default="siglip-so400m-patch14-384"
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""TagAlias — maps a model's (name, category) prediction to the operator's
|
||||
canonical tag. Resolved at suggestion-read time so raw predictions stay
|
||||
unmolested in image_record.tagger_predictions.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class TagAlias(Base):
|
||||
__tablename__ = "tag_alias"
|
||||
|
||||
alias_string: Mapped[str] = mapped_column(String(255), primary_key=True)
|
||||
alias_category: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
canonical_tag_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("tag.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
"""TagAllowlist — tags the operator opted-in to auto-apply via maintenance."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import CheckConstraint, DateTime, Float, ForeignKey, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class TagAllowlist(Base):
|
||||
__tablename__ = "tag_allowlist"
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"min_confidence > 0 AND min_confidence <= 1",
|
||||
name="ck_tag_allowlist_confidence_range",
|
||||
),
|
||||
)
|
||||
|
||||
tag_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("tag.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
min_confidence: Mapped[float] = mapped_column(Float, nullable=False, default=0.95)
|
||||
added_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""TagReferenceEmbedding — per-tag centroid (mean SigLIP embedding of members)."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pgvector.sqlalchemy import Vector
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class TagReferenceEmbedding(Base):
|
||||
__tablename__ = "tag_reference_embedding"
|
||||
|
||||
tag_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("tag.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
embedding: Mapped[list[float]] = mapped_column(Vector(1152), nullable=False)
|
||||
reference_count: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
model_version: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""TagSuggestionRejection — per-image dismissed suggestions.
|
||||
|
||||
Prevents re-suggestion AND prevents allowlist auto-apply on that image.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class TagSuggestionRejection(Base):
|
||||
__tablename__ = "tag_suggestion_rejection"
|
||||
|
||||
image_record_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("image_record.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
tag_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("tag.id", ondelete="CASCADE"), primary_key=True, index=True
|
||||
)
|
||||
rejected_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
Reference in New Issue
Block a user