- Add debug mode support for encoding statistics in API responses - Debug info includes encoding length, min/max/mean/std, and first 10 values - Frontend logs encoding stats to browser console when debug enabled - Identify page enables debug mode by default - Implement distance-based confidence thresholds for stricter matching - Borderline distances require higher confidence (70-95% vs 50%) - Applied when use_distance_based_thresholds=True (auto-match) - Reduces false positives for borderline matches - Dual tolerance system for auto-match - Default tolerance 0.6 for regular browsing (more lenient) - Run auto-match button uses 0.5 tolerance with distance-based thresholds (stricter) - Auto-accept threshold updated to 85% (from 70%) - Enhance pose detection with single-eye detection - Profile threshold reduced from 30° to 15° (stricter) - Detect single-eye visibility for extreme profile views - Infer profile direction from landmark visibility - Improved face width threshold (20px vs 10px) - Clean up debug code - Remove test photo UUID checks from production code - Remove debug print statements - Replace print statements with proper logging
352 lines
10 KiB
Python
352 lines
10 KiB
Python
"""Face processing and identify workflow schemas."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from typing import Optional
|
|
|
|
from pydantic import BaseModel, Field, ConfigDict
|
|
|
|
|
|
class ProcessFacesRequest(BaseModel):
|
|
"""Request to process faces in photos."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
batch_size: Optional[int] = Field(
|
|
None,
|
|
ge=1,
|
|
description="Maximum number of photos to process (None = all unprocessed)",
|
|
)
|
|
detector_backend: str = Field(
|
|
"retinaface",
|
|
description="DeepFace detector backend (retinaface, mtcnn, opencv, ssd)",
|
|
)
|
|
model_name: str = Field(
|
|
"ArcFace",
|
|
description="DeepFace model name (ArcFace, Facenet, Facenet512, VGG-Face)",
|
|
)
|
|
|
|
|
|
class ProcessFacesResponse(BaseModel):
|
|
"""Response after initiating face processing."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
job_id: str
|
|
message: str
|
|
batch_size: Optional[int] = None
|
|
detector_backend: str
|
|
model_name: str
|
|
|
|
|
|
class FaceItem(BaseModel):
|
|
"""Minimal face item for list views."""
|
|
|
|
model_config = ConfigDict(from_attributes=True, protected_namespaces=())
|
|
|
|
id: int
|
|
photo_id: int
|
|
quality_score: float
|
|
face_confidence: float
|
|
location: str
|
|
pose_mode: Optional[str] = Field("frontal", description="Pose classification (frontal, profile_left, etc.)")
|
|
excluded: bool = Field(False, description="Whether this face is excluded from identification")
|
|
|
|
|
|
class UnidentifiedFacesQuery(BaseModel):
|
|
"""Query params for listing unidentified faces."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
page: int = 1
|
|
page_size: int = 50
|
|
min_quality: float = 0.0
|
|
date_from: Optional[date] = None
|
|
date_to: Optional[date] = None
|
|
sort_by: str = Field("quality", description="quality|date_taken|date_added")
|
|
sort_dir: str = Field("desc", description="asc|desc")
|
|
|
|
|
|
class UnidentifiedFacesResponse(BaseModel):
|
|
"""Paginated unidentified faces list."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
items: list[FaceItem]
|
|
page: int
|
|
page_size: int
|
|
total: int
|
|
|
|
|
|
class SimilarFaceItem(BaseModel):
|
|
"""Similar face with similarity score (0-1)."""
|
|
|
|
id: int
|
|
photo_id: int
|
|
similarity: float
|
|
location: str
|
|
quality_score: float
|
|
filename: str
|
|
pose_mode: Optional[str] = Field("frontal", description="Pose classification (frontal, profile_left, etc.)")
|
|
debug_info: Optional[dict] = Field(None, description="Debug information (encoding stats) when debug mode is enabled")
|
|
|
|
|
|
class SimilarFacesResponse(BaseModel):
|
|
"""Response containing similar faces for a given face."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
base_face_id: int
|
|
items: list[SimilarFaceItem]
|
|
debug_info: Optional[dict] = Field(None, description="Debug information (base face encoding stats) when debug mode is enabled")
|
|
|
|
|
|
class BatchSimilarityRequest(BaseModel):
|
|
"""Request to get similarities between multiple faces."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
face_ids: list[int] = Field(..., description="List of face IDs to calculate similarities for")
|
|
min_confidence: float = Field(60.0, ge=0.0, le=100.0, description="Minimum confidence percentage (0-100)")
|
|
|
|
|
|
class FaceSimilarityPair(BaseModel):
|
|
"""A pair of similar faces with their similarity score."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
face_id_1: int
|
|
face_id_2: int
|
|
similarity: float # 0-1 range
|
|
confidence_pct: float # 0-100 range
|
|
|
|
|
|
class BatchSimilarityResponse(BaseModel):
|
|
"""Response containing similarities between face pairs."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
pairs: list[FaceSimilarityPair] = Field(..., description="List of similar face pairs")
|
|
|
|
|
|
class IdentifyFaceRequest(BaseModel):
|
|
"""Identify a face by selecting existing or creating new person."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
# Either provide person_id or the fields to create new person
|
|
person_id: Optional[int] = None
|
|
first_name: Optional[str] = None
|
|
last_name: Optional[str] = None
|
|
middle_name: Optional[str] = None
|
|
maiden_name: Optional[str] = None
|
|
date_of_birth: Optional[date] = None
|
|
# Optionally identify a batch of face IDs along with this one
|
|
additional_face_ids: Optional[list[int]] = None
|
|
|
|
|
|
class IdentifyFaceResponse(BaseModel):
|
|
"""Result of identify operation."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
identified_face_ids: list[int]
|
|
person_id: int
|
|
created_person: bool
|
|
|
|
|
|
class FaceUnmatchResponse(BaseModel):
|
|
"""Result of unmatch operation."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
face_id: int
|
|
message: str
|
|
|
|
|
|
class BatchUnmatchRequest(BaseModel):
|
|
"""Request to batch unmatch multiple faces."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
face_ids: list[int] = Field(..., min_items=1)
|
|
|
|
|
|
class BatchUnmatchResponse(BaseModel):
|
|
"""Result of batch unmatch operation."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
unmatched_face_ids: list[int]
|
|
count: int
|
|
message: str
|
|
|
|
|
|
class PersonFaceItem(BaseModel):
|
|
"""Face item for person's faces list (includes photo info)."""
|
|
|
|
model_config = ConfigDict(from_attributes=True, protected_namespaces=())
|
|
|
|
id: int
|
|
photo_id: int
|
|
photo_path: str
|
|
photo_filename: str
|
|
location: str
|
|
face_confidence: float
|
|
quality_score: float
|
|
detector_backend: str
|
|
model_name: str
|
|
|
|
|
|
class PersonFacesResponse(BaseModel):
|
|
"""Response containing all faces for a person."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
person_id: int
|
|
items: list[PersonFaceItem]
|
|
total: int
|
|
|
|
|
|
class AutoMatchRequest(BaseModel):
|
|
"""Request to start auto-match process."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
tolerance: float = Field(0.5, ge=0.0, le=1.0, description="Tolerance threshold (lower = stricter matching)")
|
|
auto_accept: bool = Field(False, description="Enable automatic acceptance of matching faces")
|
|
auto_accept_threshold: float = Field(70.0, ge=0.0, le=100.0, description="Similarity threshold for auto-acceptance (0-100%)")
|
|
use_distance_based_thresholds: bool = Field(False, description="Use distance-based confidence thresholds (stricter for borderline distances)")
|
|
|
|
|
|
class AutoMatchFaceItem(BaseModel):
|
|
"""Unidentified face match for a person."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
id: int
|
|
photo_id: int
|
|
photo_filename: str
|
|
location: str
|
|
quality_score: float
|
|
similarity: float # Confidence percentage (0-100)
|
|
distance: float
|
|
pose_mode: str = Field("frontal", description="Pose classification (frontal, profile_left, etc.)")
|
|
|
|
|
|
class AutoMatchPersonItem(BaseModel):
|
|
"""Person with matches for auto-match workflow."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
person_id: int
|
|
person_name: str
|
|
reference_face_id: int
|
|
reference_photo_id: int
|
|
reference_photo_filename: str
|
|
reference_location: str
|
|
reference_pose_mode: str = Field("frontal", description="Reference face pose classification")
|
|
face_count: int # Number of faces already identified for this person
|
|
matches: list[AutoMatchFaceItem]
|
|
total_matches: int
|
|
|
|
|
|
class AutoMatchPersonSummary(BaseModel):
|
|
"""Person summary without matches (for fast initial load)."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
person_id: int
|
|
person_name: str
|
|
reference_face_id: int
|
|
reference_photo_id: int
|
|
reference_photo_filename: str
|
|
reference_location: str
|
|
reference_pose_mode: str = Field("frontal", description="Reference face pose classification")
|
|
face_count: int # Number of faces already identified for this person
|
|
total_matches: int = Field(0, description="Total matches (loaded separately)")
|
|
|
|
|
|
class AutoMatchPeopleResponse(BaseModel):
|
|
"""Response containing people list without matches (for fast initial load)."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
people: list[AutoMatchPersonSummary]
|
|
total_people: int
|
|
|
|
|
|
class AutoMatchPersonMatchesResponse(BaseModel):
|
|
"""Response containing matches for a specific person."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
person_id: int
|
|
matches: list[AutoMatchFaceItem]
|
|
total_matches: int
|
|
|
|
|
|
class AutoMatchResponse(BaseModel):
|
|
"""Response from auto-match start operation."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
people: list[AutoMatchPersonItem]
|
|
total_people: int
|
|
total_matches: int
|
|
auto_accepted: bool = Field(False, description="Whether auto-acceptance was performed")
|
|
auto_accepted_faces: int = Field(0, description="Number of faces automatically accepted")
|
|
skipped_persons: int = Field(0, description="Number of persons skipped (non-frontal reference)")
|
|
skipped_matches: int = Field(0, description="Number of matches skipped (didn't meet criteria)")
|
|
|
|
|
|
class AcceptMatchesRequest(BaseModel):
|
|
"""Request to accept matches for a person."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
face_ids: list[int] = Field(..., min_items=0, description="Face IDs to identify with this person")
|
|
|
|
|
|
class MaintenanceFaceItem(BaseModel):
|
|
"""Face item for maintenance view with person info and file path."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
id: int
|
|
photo_id: int
|
|
photo_path: str
|
|
photo_filename: str
|
|
quality_score: float
|
|
person_id: Optional[int] = None
|
|
person_name: Optional[str] = None # Full name if identified
|
|
excluded: bool
|
|
|
|
|
|
class MaintenanceFacesResponse(BaseModel):
|
|
"""Response containing all faces for maintenance."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
items: list[MaintenanceFaceItem]
|
|
total: int
|
|
|
|
|
|
class DeleteFacesRequest(BaseModel):
|
|
"""Request to delete multiple faces."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
face_ids: list[int] = Field(..., min_items=1, description="Face IDs to delete")
|
|
|
|
|
|
class DeleteFacesResponse(BaseModel):
|
|
"""Response after deleting faces."""
|
|
|
|
model_config = ConfigDict(protected_namespaces=())
|
|
|
|
deleted_face_ids: list[int]
|
|
count: int
|
|
message: str
|