feat: add face detection, recognition, and clustering

- 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>
This commit is contained in:
2026-04-10 09:13:41 +02:00
parent 1ebc4bfe73
commit ad007e4cd4
4 changed files with 178 additions and 2 deletions

View File

@@ -0,0 +1,24 @@
"""
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())