diff --git a/frontend/src/pages/AutoMatch.tsx b/frontend/src/pages/AutoMatch.tsx index 81e2acf..aab1783 100644 --- a/frontend/src/pages/AutoMatch.tsx +++ b/frontend/src/pages/AutoMatch.tsx @@ -233,6 +233,27 @@ export default function AutoMatch() { {currentPerson && ( <>
+
+
+ Person {currentIndex + 1} of {activePeople.length} +
+
+ + +
+

👤 Person: {currentPerson.person_name}

📁 Photo: {currentPerson.reference_photo_filename} diff --git a/frontend/src/pages/Identify.tsx b/frontend/src/pages/Identify.tsx index 6657fbb..9f21da6 100644 --- a/frontend/src/pages/Identify.tsx +++ b/frontend/src/pages/Identify.tsx @@ -22,7 +22,7 @@ export default function Identify() { const [similar, setSimilar] = useState([]) const [compareEnabled, setCompareEnabled] = useState(true) const [selectedSimilar, setSelectedSimilar] = useState>({}) - const [uniqueFacesOnly, setUniqueFacesOnly] = useState(false) + const [uniqueFacesOnly, setUniqueFacesOnly] = useState(true) const [people, setPeople] = useState([]) const [personId, setPersonId] = useState(undefined) @@ -33,6 +33,7 @@ export default function Identify() { const [dob, setDob] = useState('') const [busy, setBusy] = useState(false) const [imageLoading, setImageLoading] = useState(false) + const [filtersCollapsed, setFiltersCollapsed] = useState(false) // Store form data per face ID (matching desktop behavior) const [faceFormData, setFaceFormData] = useState { const onKey = (e: KeyboardEvent) => { - if (e.key.toLowerCase() === 'j') { - setCurrentIdx((i) => Math.min(i + 1, Math.max(0, faces.length - 1))) - } else if (e.key.toLowerCase() === 'k') { - setCurrentIdx((i) => Math.max(i - 1, 0)) - } else if (e.key === 'Enter' && canIdentify) { + if (e.key === 'Enter' && canIdentify) { handleIdentify() } } window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) - }, [faces.length, canIdentify]) + }, [canIdentify]) const handleIdentify = async () => { if (!currentFace) return @@ -349,8 +346,23 @@ export default function Identify() {

{/* Left: Controls and current face */}
-
-
+
+
setFiltersCollapsed(!filtersCollapsed)}> +

Filters

+ +
+ {!filtersCollapsed && ( +
+
-
- -

- Hide duplicates with ≥60% match confidence -

-
+
+ +

+ Hide duplicates with ≥60% match confidence +

+
+
+ )}
{currentInfo}
- - + +
{!currentFace ? ( @@ -549,8 +563,8 @@ export default function Identify() { className={`px-3 py-2 rounded text-white ${canIdentify && !busy ? 'bg-indigo-600 hover:bg-indigo-700' : 'bg-gray-400 cursor-not-allowed'}`}> {busy ? 'Identifying...' : 'Identify (Enter)'} - - + +
diff --git a/src/core/search_stats.py b/src/core/search_stats.py index ae86673..0f07dbb 100644 --- a/src/core/search_stats.py +++ b/src/core/search_stats.py @@ -335,6 +335,8 @@ class SearchStats: def get_photos_without_faces(self) -> List[Tuple]: """Get photos that have no detected faces + Only includes processed photos (photos that have been processed for face detection). + Returns: List of tuples: (photo_path, filename) """ @@ -343,11 +345,13 @@ class SearchStats: with self.db.get_db_connection() as conn: cursor = conn.cursor() # Find photos that have no faces associated with them + # Only include processed photos cursor.execute(''' SELECT p.path, p.filename FROM photos p LEFT JOIN faces f ON p.id = f.photo_id WHERE f.photo_id IS NULL + AND p.processed = 1 ORDER BY p.filename ''') for row in cursor.fetchall(): diff --git a/src/web/services/search_service.py b/src/web/services/search_service.py index 7379072..d47b6f0 100644 --- a/src/web/services/search_service.py +++ b/src/web/services/search_service.py @@ -182,12 +182,14 @@ def get_photos_without_faces( ) -> Tuple[List[Photo], int]: """Get photos that have no detected faces. + Only includes processed photos (photos that have been processed for face detection). Matches desktop behavior exactly. """ query = ( db.query(Photo) .outerjoin(Face, Photo.id == Face.photo_id) .filter(Face.photo_id.is_(None)) + .filter(Photo.processed == True) # Only include processed photos ) # Apply folder filter if provided