From 20a8e4df5d498db7c2fa08f1e292928c584eb482 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Tue, 11 Nov 2025 12:17:23 -0500 Subject: [PATCH] feat: Add tag filtering and developer mode options in Identify and AutoMatch components This commit introduces new features for tag filtering in the Identify and AutoMatch components. Users can now filter unidentified faces by tags, with options to match all or any specified tags. The UI has been updated to include tag selection and management, enhancing user experience. Additionally, developer mode options have been added to display tolerance and auto-accept threshold settings conditionally. The API has been updated to support these new parameters, ensuring seamless integration. Documentation has been updated to reflect these changes. --- frontend/src/api/faces.ts | 2 + frontend/src/pages/AutoMatch.tsx | 122 +++++++++--------------------- frontend/src/pages/Identify.tsx | 125 ++++++++++++++++++++++++------- frontend/src/pages/Search.tsx | 3 - src/web/api/faces.py | 9 +++ src/web/services/face_service.py | 36 ++++++++- 6 files changed, 181 insertions(+), 116 deletions(-) diff --git a/frontend/src/api/faces.ts b/frontend/src/api/faces.ts index a7fbe17..766f5ab 100644 --- a/frontend/src/api/faces.ts +++ b/frontend/src/api/faces.ts @@ -156,6 +156,8 @@ export const facesApi = { date_processed_to?: string sort_by?: 'quality' | 'date_taken' | 'date_added' sort_dir?: 'asc' | 'desc' + tag_names?: string + match_all?: boolean }): Promise => { const response = await apiClient.get('/api/v1/faces/unidentified', { params, diff --git a/frontend/src/pages/AutoMatch.tsx b/frontend/src/pages/AutoMatch.tsx index 338655a..8afa4ab 100644 --- a/frontend/src/pages/AutoMatch.tsx +++ b/frontend/src/pages/AutoMatch.tsx @@ -2,10 +2,12 @@ import { useState, useEffect, useMemo } from 'react' import facesApi, { AutoMatchResponse, AutoMatchPersonItem, AutoMatchFaceItem } from '../api/faces' import peopleApi from '../api/people' import { apiClient } from '../api/client' +import { useDeveloperMode } from '../context/DeveloperModeContext' const DEFAULT_TOLERANCE = 0.6 export default function AutoMatch() { + const { isDeveloperMode } = useDeveloperMode() const [tolerance, setTolerance] = useState(DEFAULT_TOLERANCE) const [autoAcceptThreshold, setAutoAcceptThreshold] = useState(70) const [isActive, setIsActive] = useState(false) @@ -19,7 +21,6 @@ export default function AutoMatch() { const [saving, setSaving] = useState(false) const [hasNoResults, setHasNoResults] = useState(false) const [isRefreshing, setIsRefreshing] = useState(false) - const [showHelpTooltip, setShowHelpTooltip] = useState(false) const currentPerson = useMemo(() => { const activePeople = filteredPeople.length > 0 ? filteredPeople : people @@ -260,20 +261,22 @@ export default function AutoMatch() { > {isRefreshing ? 'Refreshing...' : '🔄 Refresh'} -
- - setTolerance(parseFloat(e.target.value) || 0)} - disabled={busy} - className="w-20 px-2 py-1 border border-gray-300 rounded text-sm" - /> - (lower = stricter matching) -
+ {isDeveloperMode && ( +
+ + setTolerance(parseFloat(e.target.value) || 0)} + disabled={busy} + className="w-20 px-2 py-1 border border-gray-300 rounded text-sm" + /> + (lower = stricter matching) +
+ )}
-
- - {showHelpTooltip && ( -
-
-
Auto-Match Criteria:
-
-
Face Pose:
-
- • Reference face: Frontal or tilted (not profile) -
- • Match face: Frontal or tilted (not profile) -
-
-
-
Similarity Threshold:
-
- • Minimum: {autoAcceptThreshold}% similarity -
- • Only matches ≥ {autoAcceptThreshold}% will be auto-accepted -
-
-
- Note: Profile faces are excluded for better accuracy -
-
-
-
- )} +
+ {isDeveloperMode && ( +
+ + setAutoAcceptThreshold(parseInt(e.target.value) || 70)} + disabled={busy || hasNoResults} + className="w-20 px-2 py-1 border border-gray-300 rounded text-sm" + /> + % (min similarity)
-
-
- - setAutoAcceptThreshold(parseInt(e.target.value) || 70)} - disabled={busy || hasNoResults} - className="w-20 px-2 py-1 border border-gray-300 rounded text-sm" - /> - % (min similarity) -
+ )}
- ℹ️ Auto-Match Criteria: Only frontal or tilted faces (not profile) with similarity ≥ {autoAcceptThreshold}% will be auto-accepted. Click the info icon for details. + ℹ️ Auto-Match Criteria: Only faces with similarity higher than 70% will be auto-matched. Profile faces are excluded for better accuracy.
@@ -574,11 +527,6 @@ export default function AutoMatch() { )} - {!isActive && ( -
-

Click "Start Auto-Match" to begin matching unidentified faces with identified people.

-
- )} ) } diff --git a/frontend/src/pages/Identify.tsx b/frontend/src/pages/Identify.tsx index 2c93216..440be6e 100644 --- a/frontend/src/pages/Identify.tsx +++ b/frontend/src/pages/Identify.tsx @@ -2,11 +2,14 @@ import { useEffect, useMemo, useState, useRef } from 'react' import facesApi, { FaceItem, SimilarFaceItem } from '../api/faces' import peopleApi, { Person } from '../api/people' import { apiClient } from '../api/client' +import tagsApi, { TagResponse } from '../api/tags' +import { useDeveloperMode } from '../context/DeveloperModeContext' type SortBy = 'quality' | 'date_taken' | 'date_added' type SortDir = 'asc' | 'desc' export default function Identify() { + const { isDeveloperMode } = useDeveloperMode() const [faces, setFaces] = useState([]) const [, setTotal] = useState(0) const [pageSize, setPageSize] = useState(50) @@ -33,8 +36,11 @@ export default function Identify() { const [dob, setDob] = useState('') const [busy, setBusy] = useState(false) const [imageLoading, setImageLoading] = useState(false) - const [filtersCollapsed, setFiltersCollapsed] = useState(false) + const [filtersCollapsed, setFiltersCollapsed] = useState(true) const [loadingFaces, setLoadingFaces] = useState(false) + const [availableTags, setAvailableTags] = useState([]) + const [selectedTags, setSelectedTags] = useState([]) + const [tagsExpanded, setTagsExpanded] = useState(false) // Store form data per face ID (matching desktop behavior) const [faceFormData, setFaceFormData] = useState 0 ? selectedTags.join(', ') : undefined, + match_all: false, // Default to match any tag }) // Apply unique faces filter if enabled @@ -200,6 +208,15 @@ export default function Identify() { setPeople(res.items) } + const loadTags = async () => { + try { + const res = await tagsApi.list() + setAvailableTags(res.items) + } catch (error) { + console.error('Error loading tags:', error) + } + } + const loadSimilar = async (faceId: number) => { if (!compareEnabled) { setSimilar([]) @@ -220,6 +237,7 @@ export default function Identify() { initialLoadRef.current = true loadFaces() loadPeople() + loadTags() } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -232,6 +250,14 @@ export default function Identify() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [uniqueFacesOnly]) + // Reload when pageSize changes (immediate reload) + useEffect(() => { + if (initialLoadRef.current) { + loadFaces() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pageSize]) + useEffect(() => { if (currentFace) { setImageLoading(true) // Show loading indicator when face changes @@ -398,20 +424,33 @@ export default function Identify() {
{/* Left: Controls and current face */}
- {/* Unique Faces Checkbox - Outside Filters */} + {/* Unique Faces Checkbox and Batch Size - Outside Filters */}
- -

- Hide duplicates with ≥60% match confidence -

+
+
+ +

+ Hide duplicates with ≥60% match confidence +

+
+
+ + +
+
@@ -437,15 +476,6 @@ export default function Identify() { onChange={(e) => setMinQuality(parseFloat(e.target.value))} className="w-full" />
{(minQuality * 100).toFixed(0)}%
-
- - -
setDateFrom(e.target.value)} @@ -474,6 +504,51 @@ export default function Identify() {
+
+
+

With Tags

+ +
+ {tagsExpanded && ( +
+
+ + +
+

+ Hold Ctrl/Cmd to select multiple tags +

+
+ )} +
{/* Pose mode display */} - {currentFace.pose_mode && ( + {isDeveloperMode && currentFace.pose_mode && (
Pose: {currentFace.pose_mode}
@@ -738,7 +813,7 @@ export default function Identify() {
{/* Pose mode */} - {s.pose_mode && ( + {isDeveloperMode && s.pose_mode && (
{s.pose_mode}
diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx index ee656d5..e8d3f49 100644 --- a/frontend/src/pages/Search.tsx +++ b/frontend/src/pages/Search.tsx @@ -523,9 +523,6 @@ export default function Search() { {tagsExpanded && (
-