"""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.)") class SimilarFacesResponse(BaseModel): """Response containing similar faces for a given face.""" model_config = ConfigDict(protected_namespaces=()) base_face_id: int items: list[SimilarFaceItem] 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.6, 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%)") 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