- Create face_embeddings table with pgvector Vector(128) + HNSW index - Implement extract_faces task (YuNet detection + SFace recognition) - Implement recluster_faces task (DBSCAN clustering → Tag(kind=face_cluster)) - Clusters are named "Person N" and get representative_photo_id - cluster_id FK → tags.id, SET NULL on delete for merge/rename support Migration 0005 creates the face_embeddings table. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
42 lines
1.3 KiB
Python
42 lines
1.3 KiB
Python
"""face_embeddings table
|
|
|
|
Revision ID: 0005_face_embeddings
|
|
Revises: 0004_ocr_fts
|
|
Create Date: 2026-04-10
|
|
|
|
Create face_embeddings table with pgvector Vector(128) for SFace
|
|
recognition embeddings and HNSW index.
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
|
|
revision: str = "0005_face_embeddings"
|
|
down_revision: Union[str, None] = "0004_ocr_fts"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.execute("""
|
|
CREATE TABLE IF NOT EXISTS face_embeddings (
|
|
id VARCHAR PRIMARY KEY,
|
|
photo_id VARCHAR NOT NULL REFERENCES photos(id) ON DELETE CASCADE,
|
|
bbox JSONB,
|
|
vector vector(128),
|
|
cluster_id VARCHAR REFERENCES tags(id) ON DELETE SET NULL,
|
|
quality FLOAT,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
)
|
|
""")
|
|
op.execute("CREATE INDEX IF NOT EXISTS ix_face_embeddings_photo_id ON face_embeddings(photo_id)")
|
|
op.execute("CREATE INDEX IF NOT EXISTS ix_face_embeddings_cluster_id ON face_embeddings(cluster_id)")
|
|
op.execute("""
|
|
CREATE INDEX IF NOT EXISTS ix_face_embeddings_vector_hnsw
|
|
ON face_embeddings USING hnsw (vector vector_cosine_ops)
|
|
""")
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.execute("DROP TABLE IF EXISTS face_embeddings")
|