"""fc2b: ML pipeline — allowlist, aliases, centroids, ml_settings Revision ID: 0003 Revises: 0002 Create Date: 2026-05-15 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op from pgvector.sqlalchemy import Vector revision: str = "0003" down_revision: Union[str, None] = "0002" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # 3.1 rename wd14_* -> tagger_* op.alter_column("image_record", "wd14_predictions", new_column_name="tagger_predictions") op.alter_column( "image_record", "wd14_model_version", new_column_name="tagger_model_version" ) # 3.2 tag_allowlist op.create_table( "tag_allowlist", sa.Column("tag_id", sa.Integer(), nullable=False), sa.Column( "min_confidence", sa.Float(), nullable=False, server_default="0.95" ), sa.Column( "added_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), ), sa.ForeignKeyConstraint( ["tag_id"], ["tag.id"], name="fk_tag_allowlist_tag_id_tag", ondelete="CASCADE", ), sa.PrimaryKeyConstraint("tag_id", name="pk_tag_allowlist"), sa.CheckConstraint( "min_confidence > 0 AND min_confidence <= 1", name="ck_tag_allowlist_confidence_range", ), ) # 3.3 tag_suggestion_rejection op.create_table( "tag_suggestion_rejection", sa.Column("image_record_id", sa.Integer(), nullable=False), sa.Column("tag_id", sa.Integer(), nullable=False), sa.Column( "rejected_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), ), sa.ForeignKeyConstraint( ["image_record_id"], ["image_record.id"], name="fk_tsr_image_record_id_image_record", ondelete="CASCADE", ), sa.ForeignKeyConstraint( ["tag_id"], ["tag.id"], name="fk_tsr_tag_id_tag", ondelete="CASCADE", ), sa.PrimaryKeyConstraint( "image_record_id", "tag_id", name="pk_tag_suggestion_rejection" ), ) op.create_index( "ix_tag_suggestion_rejection_tag", "tag_suggestion_rejection", ["tag_id"] ) # 3.4 tag_alias op.create_table( "tag_alias", sa.Column("alias_string", sa.String(length=255), nullable=False), sa.Column("alias_category", sa.String(length=32), nullable=False), sa.Column("canonical_tag_id", sa.Integer(), nullable=False), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), ), sa.ForeignKeyConstraint( ["canonical_tag_id"], ["tag.id"], name="fk_tag_alias_canonical_tag_id_tag", ondelete="CASCADE", ), sa.PrimaryKeyConstraint( "alias_string", "alias_category", name="pk_tag_alias" ), ) op.create_index("ix_tag_alias_canonical", "tag_alias", ["canonical_tag_id"]) # 3.5 tag_reference_embedding (centroids) op.create_table( "tag_reference_embedding", sa.Column("tag_id", sa.Integer(), nullable=False), sa.Column("embedding", Vector(1152), nullable=False), sa.Column("reference_count", sa.Integer(), nullable=False), sa.Column("model_version", sa.String(length=128), nullable=False), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), ), sa.ForeignKeyConstraint( ["tag_id"], ["tag.id"], name="fk_tag_reference_embedding_tag_id_tag", ondelete="CASCADE", ), sa.PrimaryKeyConstraint("tag_id", name="pk_tag_reference_embedding"), ) # 3.6 ml_settings singleton op.create_table( "ml_settings", sa.Column("id", sa.Integer(), nullable=False), sa.Column( "suggestion_threshold_artist", sa.Float(), nullable=False, server_default="0.30", ), sa.Column( "suggestion_threshold_character", sa.Float(), nullable=False, server_default="0.50", ), sa.Column( "suggestion_threshold_copyright", sa.Float(), nullable=False, server_default="0.50", ), sa.Column( "suggestion_threshold_general", sa.Float(), nullable=False, server_default="0.95", ), sa.Column( "centroid_similarity_threshold", sa.Float(), nullable=False, server_default="0.55", ), sa.Column( "min_reference_images", sa.Integer(), nullable=False, server_default="5", ), sa.Column( "tagger_model_version", sa.String(length=128), nullable=False, server_default="camie-tagger-v2", ), sa.Column( "embedder_model_version", sa.String(length=128), nullable=False, server_default="siglip-so400m-patch14-384", ), sa.Column( "updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), ), sa.PrimaryKeyConstraint("id", name="pk_ml_settings"), sa.CheckConstraint("id = 1", name="ck_ml_settings_singleton"), ) op.execute("INSERT INTO ml_settings (id) VALUES (1)") def downgrade() -> None: op.drop_table("ml_settings") op.drop_table("tag_reference_embedding") op.drop_index("ix_tag_alias_canonical", table_name="tag_alias") op.drop_table("tag_alias") op.drop_index( "ix_tag_suggestion_rejection_tag", table_name="tag_suggestion_rejection" ) op.drop_table("tag_suggestion_rejection") op.drop_table("tag_allowlist") op.alter_column( "image_record", "tagger_model_version", new_column_name="wd14_model_version" ) op.alter_column( "image_record", "tagger_predictions", new_column_name="wd14_predictions" )