refactor: strip AI pipeline to binary photo/other classifier
Drops face recognition, OCR, object detection, and semantic embeddings. The sole remaining vision task is a CLIP-based binary classifier (photography vs other); photos in "other" get needs_review=true so screenshots, documents, memes and scans can be triaged from a new filter pill in the UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,6 @@ from app.models.photos import Photo
|
||||
from app.models.folders import Folder, SourceRoot
|
||||
from app.models.tags import Tag, PhotoTag
|
||||
from app.models.heaps import Heap, HeapPhoto
|
||||
from app.models.embeddings import Embedding
|
||||
from app.models.ocr_text import OCRText
|
||||
from app.models.face_embedding import FaceEmbedding
|
||||
from app.models.sharing import HeapShare, FolderShare
|
||||
|
||||
__all__ = [
|
||||
@@ -20,9 +17,6 @@ __all__ = [
|
||||
'PhotoTag',
|
||||
'Heap',
|
||||
'HeapPhoto',
|
||||
'Embedding',
|
||||
'OCRText',
|
||||
'FaceEmbedding',
|
||||
'HeapShare',
|
||||
'FolderShare',
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
"""
|
||||
Embedding model — stores CLIP/SigLIP image embeddings via pgvector.
|
||||
|
||||
Composite PK (photo_id, model) allows re-embedding with newer models
|
||||
without clobbering old vectors.
|
||||
|
||||
Vector dimension is 768 to support SigLIP2 ViT-B/16 (the default).
|
||||
OpenCLIP ViT-B/32 (512-d) embeddings are zero-padded on insert so
|
||||
both models coexist in the same column. The padding is invisible to
|
||||
cosine similarity (zeros don't affect the angle).
|
||||
"""
|
||||
from sqlalchemy import Column, String, ForeignKey, DateTime, func
|
||||
from pgvector.sqlalchemy import Vector
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Embedding(Base):
|
||||
__tablename__ = 'embeddings'
|
||||
|
||||
photo_id = Column(String, ForeignKey('photos.id', ondelete='CASCADE'), primary_key=True)
|
||||
model = Column(String(64), primary_key=True) # e.g. 'siglip2_vitb16'
|
||||
vector = Column(Vector(512)) # OpenCLIP ViT-B/32 → 512-d
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -1,24 +0,0 @@
|
||||
"""
|
||||
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(512)) # ArcFace → 512-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())
|
||||
@@ -1,20 +0,0 @@
|
||||
"""
|
||||
OCR text model — stores text regions extracted from photos via rapidocr.
|
||||
"""
|
||||
from sqlalchemy import Column, String, Float, ForeignKey, Text, DateTime, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
import uuid
|
||||
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class OCRText(Base):
|
||||
__tablename__ = 'ocr_text'
|
||||
|
||||
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
photo_id = Column(String, ForeignKey('photos.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||
text = Column(Text, nullable=False)
|
||||
language = Column(String(8), default='')
|
||||
confidence = Column(Float)
|
||||
bbox = Column(JSONB) # [x1, y1, x2, y2] normalized 0-1
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -50,6 +50,11 @@ class Photo(Base):
|
||||
# rows, and POST /folders/{id}/hide recomputes it on toggle.
|
||||
is_hidden = Column(Boolean, nullable=False, default=False, server_default='false', index=True)
|
||||
|
||||
# "Needs review" — set by the content classifier when a photo is
|
||||
# classified as 'other' (screenshot, document, meme, scan, etc.) so
|
||||
# the user can page through non-photographs in the UI and triage them.
|
||||
needs_review = Column(Boolean, nullable=False, default=False, server_default='false', index=True)
|
||||
|
||||
# "Capture date is probably wrong" — denormalized from the folder/filename
|
||||
# date-guesser. Set at scan time and recomputed on every taken_at edit so
|
||||
# the filter bar can query it directly. See services/date_guess.py for
|
||||
@@ -118,4 +123,5 @@ class Photo(Base):
|
||||
Index('ix_photos_media_type', 'media_type'),
|
||||
Index('ix_photos_processing_status', 'processing_status'),
|
||||
Index('ix_photos_lat_lon', 'latitude', 'longitude'),
|
||||
Index('ix_photos_needs_review', 'needs_review'),
|
||||
)
|
||||
@@ -6,7 +6,7 @@ labels, and face clusters via the `kind` column. The `photo_tags`
|
||||
association carries per-photo ML metadata (confidence, bounding box,
|
||||
source model).
|
||||
"""
|
||||
from sqlalchemy import Column, String, Float, ForeignKey, Table, Index, UniqueConstraint
|
||||
from sqlalchemy import Column, String, Float, ForeignKey, Table, Index, UniqueConstraint # noqa: F401
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
import uuid
|
||||
@@ -22,7 +22,7 @@ photo_tags = Table(
|
||||
# ML metadata — null for user-applied tags
|
||||
Column('confidence', Float, nullable=True),
|
||||
Column('bbox', JSONB, nullable=True), # [x1, y1, x2, y2] normalized 0-1
|
||||
Column('source', String, nullable=True), # e.g. "vision:yolov8n", "vision:sface"
|
||||
Column('source', String, nullable=True), # e.g. "vision:clip_classifier"
|
||||
Index('ix_photo_tags_photo_id', 'photo_id'),
|
||||
Index('ix_photo_tags_tag_id', 'tag_id'),
|
||||
)
|
||||
@@ -42,16 +42,11 @@ class Tag(Base):
|
||||
|
||||
# Tag classification
|
||||
kind = Column(String, nullable=False, default='user', index=True)
|
||||
# kind values: 'user' | 'object' | 'scene' | 'face_cluster'
|
||||
# kind values: 'user' | 'content_type'
|
||||
|
||||
# Which model produced this tag (null for user-created)
|
||||
source = Column(String, nullable=True)
|
||||
# e.g. "vision:yolov8n", "vision:sface", null
|
||||
|
||||
# For face clusters: the photo used as the cluster representative thumbnail
|
||||
representative_photo_id = Column(
|
||||
String, ForeignKey('photos.id', ondelete='SET NULL'), nullable=True
|
||||
)
|
||||
# e.g. "vision:clip_classifier", null
|
||||
|
||||
# Relationships
|
||||
photos = relationship("Photo", secondary=photo_tags, backref="tags")
|
||||
|
||||
Reference in New Issue
Block a user