feat: Add new analysis documents and update installation scripts for backend integration

This commit introduces several new analysis documents, including Auto-Match Load Performance Analysis, Folder Picker Analysis, Monorepo Migration Summary, and various performance analysis documents. Additionally, the installation scripts are updated to reflect changes in backend service paths, ensuring proper integration with the new backend structure. These enhancements provide better documentation and streamline the setup process for users.
This commit is contained in:
Tanya
2025-12-30 15:04:32 -05:00
parent 12c62f1deb
commit 68d280e8f5
140 changed files with 5101 additions and 933 deletions
+286
View File
@@ -0,0 +1,286 @@
"""SQLAlchemy models for PunimTag Web - matching desktop schema exactly."""
from __future__ import annotations
from datetime import datetime, date
from typing import TYPE_CHECKING
from sqlalchemy import (
Boolean,
Column,
Date,
DateTime,
ForeignKey,
Index,
Integer,
LargeBinary,
Numeric,
Text,
UniqueConstraint,
CheckConstraint,
)
from sqlalchemy.orm import declarative_base, relationship
from backend.constants.roles import DEFAULT_USER_ROLE
if TYPE_CHECKING:
pass
Base = declarative_base()
class Photo(Base):
"""Photo model - matches desktop schema exactly."""
__tablename__ = "photos"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
path = Column(Text, unique=True, nullable=False, index=True)
filename = Column(Text, nullable=False)
date_added = Column(DateTime, default=datetime.utcnow, nullable=False)
date_taken = Column(Date, nullable=True, index=True)
processed = Column(Boolean, default=False, nullable=False, index=True)
file_hash = Column(Text, nullable=False, index=True)
media_type = Column(Text, default="image", nullable=False, index=True) # "image" or "video"
faces = relationship("Face", back_populates="photo", cascade="all, delete-orphan")
photo_tags = relationship(
"PhotoTagLinkage", back_populates="photo", cascade="all, delete-orphan"
)
favorites = relationship("PhotoFavorite", back_populates="photo", cascade="all, delete-orphan")
video_people = relationship(
"PhotoPersonLinkage", back_populates="photo", cascade="all, delete-orphan"
)
__table_args__ = (
Index("idx_photos_processed", "processed"),
Index("idx_photos_date_taken", "date_taken"),
Index("idx_photos_date_added", "date_added"),
Index("idx_photos_file_hash", "file_hash"),
)
class Person(Base):
"""Person model - matches desktop schema exactly."""
__tablename__ = "people"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
first_name = Column(Text, nullable=False)
last_name = Column(Text, nullable=False)
middle_name = Column(Text, nullable=True)
maiden_name = Column(Text, nullable=True)
date_of_birth = Column(Date, nullable=True)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
faces = relationship("Face", back_populates="person")
person_encodings = relationship(
"PersonEncoding", back_populates="person", cascade="all, delete-orphan"
)
video_photos = relationship(
"PhotoPersonLinkage", back_populates="person", cascade="all, delete-orphan"
)
__table_args__ = (
UniqueConstraint(
"first_name", "last_name", "middle_name", "maiden_name", "date_of_birth",
name="uq_people_names_dob"
),
)
class Face(Base):
"""Face detection model - matches desktop schema exactly."""
__tablename__ = "faces"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
photo_id = Column(Integer, ForeignKey("photos.id"), nullable=False, index=True)
person_id = Column(Integer, ForeignKey("people.id"), nullable=True, index=True)
encoding = Column(LargeBinary, nullable=False)
location = Column(Text, nullable=False)
confidence = Column(Numeric, default=0.0, nullable=False)
quality_score = Column(Numeric, default=0.0, nullable=False, index=True)
is_primary_encoding = Column(Boolean, default=False, nullable=False)
detector_backend = Column(Text, default="retinaface", nullable=False)
model_name = Column(Text, default="ArcFace", nullable=False)
face_confidence = Column(Numeric, default=0.0, nullable=False)
exif_orientation = Column(Integer, nullable=True)
pose_mode = Column(Text, default="frontal", nullable=False, index=True)
yaw_angle = Column(Numeric, nullable=True)
pitch_angle = Column(Numeric, nullable=True)
roll_angle = Column(Numeric, nullable=True)
landmarks = Column(Text, nullable=True) # JSON string of facial landmarks
identified_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
excluded = Column(Boolean, default=False, nullable=False, index=True) # Exclude from identification
photo = relationship("Photo", back_populates="faces")
person = relationship("Person", back_populates="faces")
person_encodings = relationship(
"PersonEncoding", back_populates="face", cascade="all, delete-orphan"
)
__table_args__ = (
Index("idx_faces_person_id", "person_id"),
Index("idx_faces_photo_id", "photo_id"),
Index("idx_faces_quality", "quality_score"),
Index("idx_faces_pose_mode", "pose_mode"),
Index("idx_faces_identified_by", "identified_by_user_id"),
Index("idx_faces_excluded", "excluded"),
)
class PersonEncoding(Base):
"""Person encoding model - matches desktop schema exactly (was person_encodings)."""
__tablename__ = "person_encodings"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
person_id = Column(Integer, ForeignKey("people.id"), nullable=False, index=True)
face_id = Column(Integer, ForeignKey("faces.id"), nullable=False, index=True)
encoding = Column(LargeBinary, nullable=False)
quality_score = Column(Numeric, default=0.0, nullable=False, index=True)
detector_backend = Column(Text, default="retinaface", nullable=False)
model_name = Column(Text, default="ArcFace", nullable=False)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
person = relationship("Person", back_populates="person_encodings")
face = relationship("Face", back_populates="person_encodings")
__table_args__ = (
Index("idx_person_encodings_person_id", "person_id"),
Index("idx_person_encodings_quality", "quality_score"),
)
class Tag(Base):
"""Tag model - matches desktop schema exactly."""
__tablename__ = "tags"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
tag_name = Column(Text, unique=True, nullable=False, index=True)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
photo_tags = relationship(
"PhotoTagLinkage", back_populates="tag", cascade="all, delete-orphan"
)
class PhotoTagLinkage(Base):
"""Photo-Tag linkage model - matches desktop schema exactly (was phototaglinkage)."""
__tablename__ = "phototaglinkage"
linkage_id = Column(Integer, primary_key=True, autoincrement=True)
photo_id = Column(Integer, ForeignKey("photos.id"), nullable=False, index=True)
tag_id = Column(Integer, ForeignKey("tags.id"), nullable=False, index=True)
linkage_type = Column(
Integer, default=0, nullable=False,
server_default="0"
)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
photo = relationship("Photo", back_populates="photo_tags")
tag = relationship("Tag", back_populates="photo_tags")
__table_args__ = (
UniqueConstraint("photo_id", "tag_id", name="uq_photo_tag"),
CheckConstraint("linkage_type IN (0, 1)", name="ck_linkage_type"),
Index("idx_photo_tags_tag", "tag_id"),
Index("idx_photo_tags_photo", "photo_id"),
)
class PhotoFavorite(Base):
"""Photo favorites model - user-specific favorites."""
__tablename__ = "photo_favorites"
id = Column(Integer, primary_key=True, autoincrement=True)
username = Column(Text, nullable=False, index=True)
photo_id = Column(Integer, ForeignKey("photos.id"), nullable=False, index=True)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
photo = relationship("Photo", back_populates="favorites")
__table_args__ = (
UniqueConstraint("username", "photo_id", name="uq_user_photo_favorite"),
Index("idx_favorites_username", "username"),
Index("idx_favorites_photo", "photo_id"),
)
class User(Base):
"""User model for main database - separate from auth database users."""
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
username = Column(Text, unique=True, nullable=False, index=True)
password_hash = Column(Text, nullable=False) # Hashed password
email = Column(Text, unique=True, nullable=False, index=True)
full_name = Column(Text, nullable=False)
is_active = Column(Boolean, default=True, nullable=False)
is_admin = Column(Boolean, default=False, nullable=False, index=True)
role = Column(
Text,
nullable=False,
default=DEFAULT_USER_ROLE,
server_default=DEFAULT_USER_ROLE,
index=True,
)
password_change_required = Column(Boolean, default=True, nullable=False, index=True)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
last_login = Column(DateTime, nullable=True)
__table_args__ = (
Index("idx_users_username", "username"),
Index("idx_users_email", "email"),
Index("idx_users_is_admin", "is_admin"),
Index("idx_users_password_change_required", "password_change_required"),
Index("idx_users_role", "role"),
)
class PhotoPersonLinkage(Base):
"""Direct linkage between Video (Photo with media_type='video') and Person.
This allows identifying people in videos without requiring face detection.
Only used for videos, not photos (photos use Face model for identification).
"""
__tablename__ = "photo_person_linkage"
id = Column(Integer, primary_key=True, autoincrement=True)
photo_id = Column(Integer, ForeignKey("photos.id"), nullable=False, index=True)
person_id = Column(Integer, ForeignKey("people.id"), nullable=False, index=True)
identified_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
created_date = Column(DateTime, default=datetime.utcnow, nullable=False)
photo = relationship("Photo", back_populates="video_people")
person = relationship("Person", back_populates="video_photos")
__table_args__ = (
UniqueConstraint("photo_id", "person_id", name="uq_photo_person"),
Index("idx_photo_person_photo", "photo_id"),
Index("idx_photo_person_person", "person_id"),
Index("idx_photo_person_user", "identified_by_user_id"),
)
class RolePermission(Base):
"""Role-to-feature permission matrix."""
__tablename__ = "role_permissions"
id = Column(Integer, primary_key=True, autoincrement=True)
role = Column(Text, nullable=False, index=True)
feature_key = Column(Text, nullable=False, index=True)
allowed = Column(Boolean, nullable=False, default=False, server_default="0")
__table_args__ = (
UniqueConstraint("role", "feature_key", name="uq_role_feature"),
Index("idx_role_permissions_role_feature", "role", "feature_key"),
)