- 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>
25 lines
979 B
Python
25 lines
979 B
Python
"""
|
|
Face embedding model — stores per-face detection + recognition vectors.
|
|
|
|
cluster_id FKs to tags.id where kind='face_cluster'. Null means
|
|
unclustered (will be assigned by recluster_faces).
|
|
"""
|
|
from sqlalchemy import Column, String, Float, ForeignKey, DateTime, func
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
from pgvector.sqlalchemy import Vector
|
|
import uuid
|
|
|
|
from app.database import Base
|
|
|
|
|
|
class FaceEmbedding(Base):
|
|
__tablename__ = 'face_embeddings'
|
|
|
|
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
photo_id = Column(String, ForeignKey('photos.id', ondelete='CASCADE'), nullable=False, index=True)
|
|
bbox = Column(JSONB) # [x1, y1, x2, y2] normalized 0-1
|
|
vector = Column(Vector(128)) # SFace → 128-d
|
|
cluster_id = Column(String, ForeignKey('tags.id', ondelete='SET NULL'), nullable=True, index=True)
|
|
quality = Column(Float)
|
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|