diff --git a/frontend/src/api/faces.ts b/frontend/src/api/faces.ts index 63498ac..e173cd1 100644 --- a/frontend/src/api/faces.ts +++ b/frontend/src/api/faces.ts @@ -217,8 +217,10 @@ export const facesApi = { }) return response.data }, - getSimilar: async (faceId: number): Promise => { - const response = await apiClient.get(`/api/v1/faces/${faceId}/similar`) + getSimilar: async (faceId: number, includeExcluded?: boolean): Promise => { + const response = await apiClient.get(`/api/v1/faces/${faceId}/similar`, { + params: { include_excluded: includeExcluded || false }, + }) return response.data }, batchSimilarity: async (request: BatchSimilarityRequest): Promise => { diff --git a/frontend/src/pages/Help.tsx b/frontend/src/pages/Help.tsx index c678b30..9e7b39f 100644 --- a/frontend/src/pages/Help.tsx +++ b/frontend/src/pages/Help.tsx @@ -73,11 +73,11 @@ function NavigationOverview({ onPageClick }: { onPageClick: (page: PageId) => vo const mainNavItems = [ { id: 'scan' as PageId, icon: '🗂️', label: 'Scan', description: 'Import photos from folders or upload files' }, { id: 'process' as PageId, icon: '⚙️', label: 'Process', description: 'Detect and process faces in photos' }, - { id: 'identify' as PageId, icon: '👤', label: 'Identify', description: 'Manually identify people in faces' }, + { id: 'identify' as PageId, icon: '👤', label: 'Identify People', description: 'Manually identify people in faces' }, { id: 'auto-match' as PageId, icon: '🤖', label: 'Auto-Match', description: 'Automatically match similar faces to previously identified faces' }, - { id: 'search' as PageId, icon: '🔍', label: 'Search', description: 'Search and filter photos' }, - { id: 'modify' as PageId, icon: '✏️', label: 'Modify', description: 'Edit person information' }, - { id: 'tags' as PageId, icon: '🏷️', label: 'Tags', description: 'Tag photos and manage photo tags' }, + { id: 'search' as PageId, icon: '🔍', label: 'Search Photos', description: 'Search and filter photos' }, + { id: 'modify' as PageId, icon: '✏️', label: 'Modify People', description: 'Edit person information' }, + { id: 'tags' as PageId, icon: '🏷️', label: 'Tag Photos', description: 'Tag photos and manage photo tags' }, ] const maintenanceNavItems = [ @@ -257,7 +257,7 @@ function ProcessPageHelp({ onBack }: { onBack: () => void }) { function IdentifyPageHelp({ onBack }: { onBack: () => void }) { return ( - +

Purpose

@@ -270,11 +270,11 @@ function IdentifyPageHelp({ onBack }: { onBack: () => void }) {

Features:

  • Face Navigation: Browse through unidentified faces using Prev/Next buttons
  • -
  • Filters: Filter by date taken, tags
  • +
  • Filters: Filter by date taken, tags, quality, and excluded faces
  • Unique Faces Only: Hide similar faces (faces with 60%+ similarity to others)
  • Batch Size: Control how many faces to load at once
  • Similar Faces Panel: View and identify multiple similar faces at once
  • - +
  • Exclude Faces: Block faces from identification (e.g., for false detections or unwanted faces)
@@ -288,11 +288,20 @@ function IdentifyPageHelp({ onBack }: { onBack: () => void }) {
  • Click "Filters" to expand filter options
  • Set date range, minimum quality, or select tags
  • Choose sort order (Quality, Date Taken, or Date Processed)
  • +
  • Check "Include excluded faces" to show faces that have been excluded from identification
  • Click "Apply Filters" to load faces
  • Toggle "Unique faces only" checkbox to hide duplicate faces (recommended)
  • View the current face on the left panel
  • +
  • To exclude a face from identification: +
      +
    • Click the 🚫 button next to the face counter
    • +
    • The face will be marked as excluded and automatically skipped
    • +
    • Excluded faces are hidden by default (check "Include excluded faces" in filters to see them)
    • +
    • Click the 🚫 button again to include the face back in identification
    • +
    +
  • Choose how to identify:
    • Select existing person: Choose from the dropdown at the top
    • @@ -370,6 +379,8 @@ function IdentifyPageHelp({ onBack }: { onBack: () => void }) {
    • Tag filtering lets you identify faces only from photos with specific tags
    • Click on face images to open the full photo in a new window
    • For videos, you can identify multiple people in the same video
    • +
    • Use the exclude (🚫) button to exclude unknown, unwanted faces, or low-quality faces from appearing in your identification workflow
    • +
    • Excluded faces are hidden by default, but you can view them by checking "Include excluded faces" in the filters
  • @@ -468,7 +479,7 @@ function AutoMatchPageHelp({ onBack }: { onBack: () => void }) { function SearchPageHelp({ onBack }: { onBack: () => void }) { return ( - +

    Purpose

    @@ -580,7 +591,7 @@ function SearchPageHelp({ onBack }: { onBack: () => void }) { function ModifyPageHelp({ onBack }: { onBack: () => void }) { return ( - +

    Purpose

    @@ -701,7 +712,7 @@ function ModifyPageHelp({ onBack }: { onBack: () => void }) { function TagsPageHelp({ onBack }: { onBack: () => void }) { return ( - +

    Purpose

    diff --git a/frontend/src/pages/Identify.tsx b/frontend/src/pages/Identify.tsx index 6c8e478..699f93c 100644 --- a/frontend/src/pages/Identify.tsx +++ b/frontend/src/pages/Identify.tsx @@ -348,7 +348,7 @@ export default function Identify() { return } try { - const res = await facesApi.getSimilar(faceId) + const res = await facesApi.getSimilar(faceId, includeExcludedFaces) setSimilar(res.items || []) setSelectedSimilar({}) } catch (error) { diff --git a/src/web/api/faces.py b/src/web/api/faces.py index d28caf1..784913b 100644 --- a/src/web/api/faces.py +++ b/src/web/api/faces.py @@ -192,11 +192,15 @@ def get_unidentified_faces( @router.get("/{face_id}/similar", response_model=SimilarFacesResponse) -def get_similar_faces(face_id: int, db: Session = Depends(get_db)) -> SimilarFacesResponse: +def get_similar_faces( + face_id: int, + include_excluded: bool = Query(False, description="Include excluded faces in results"), + db: Session = Depends(get_db) +) -> SimilarFacesResponse: """Return similar unidentified faces for a given face.""" import logging logger = logging.getLogger(__name__) - logger.info(f"API: get_similar_faces called for face_id={face_id}") + logger.info(f"API: get_similar_faces called for face_id={face_id}, include_excluded={include_excluded}") # Validate face exists base = db.query(Face).filter(Face.id == face_id).first() @@ -204,8 +208,8 @@ def get_similar_faces(face_id: int, db: Session = Depends(get_db)) -> SimilarFac logger.warning(f"API: Face {face_id} not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Face {face_id} not found") - logger.info(f"API: Calling find_similar_faces for face_id={face_id}") - results = find_similar_faces(db, face_id) + logger.info(f"API: Calling find_similar_faces for face_id={face_id}, include_excluded={include_excluded}") + results = find_similar_faces(db, face_id, include_excluded=include_excluded) logger.info(f"API: find_similar_faces returned {len(results)} results") items = [ diff --git a/src/web/services/face_service.py b/src/web/services/face_service.py index c30b0a0..619c436 100644 --- a/src/web/services/face_service.py +++ b/src/web/services/face_service.py @@ -1512,6 +1512,7 @@ def find_similar_faces( limit: int = 20000, # Very high default limit - effectively unlimited tolerance: float = 0.6, # DEFAULT_FACE_TOLERANCE from desktop filter_frontal_only: bool = False, # New: Only return frontal or tilted faces (not profile) + include_excluded: bool = False, # Include excluded faces in results ) -> List[Tuple[Face, float, float]]: # Returns (face, distance, confidence_pct) """Find similar faces matching desktop logic exactly. @@ -1528,6 +1529,7 @@ def find_similar_faces( Args: filter_frontal_only: Only return frontal or tilted faces (not profile) + include_excluded: Include excluded faces in results (default: False) """ from src.web.db.models import Photo @@ -1586,6 +1588,10 @@ def find_similar_faces( is_unidentified = f.person_id is None if is_unidentified and confidence_pct >= 40: + # Filter by excluded status if not including excluded faces + if not include_excluded and getattr(f, "excluded", False): + continue + # Filter by pose_mode if requested (only frontal or tilted faces) if filter_frontal_only and not _is_acceptable_pose_for_auto_match(f.pose_mode): continue @@ -1848,9 +1854,11 @@ def find_auto_match_matches( # Desktop: similar_faces = self.face_processor._get_filtered_similar_faces( # reference_face_id, tolerance, include_same_photo=False, face_status=None) # This filters by: person_id is None (unidentified), confidence >= 40%, sorts by distance + # Auto-match always excludes excluded faces similar_faces = find_similar_faces( db, reference_face_id, tolerance=tolerance, - filter_frontal_only=filter_frontal_only + filter_frontal_only=filter_frontal_only, + include_excluded=False # Auto-match always excludes excluded faces ) if similar_faces: @@ -1994,9 +2002,11 @@ def get_auto_match_person_matches( return [] # Find similar faces using existing function + # Auto-match always excludes excluded faces similar_faces = find_similar_faces( db, reference_face.id, tolerance=tolerance, - filter_frontal_only=filter_frontal_only + filter_frontal_only=filter_frontal_only, + include_excluded=False # Auto-match always excludes excluded faces ) return similar_faces