feat: Add date filters for face identification and enhance API for improved querying
This commit introduces new date filters for the face identification process, allowing users to filter faces based on the date taken and date processed. The API has been updated to support these new parameters, ensuring backward compatibility with legacy date filters. Additionally, the Identify component has been modified to incorporate these new filters in the user interface, enhancing the overall functionality and user experience. Documentation has been updated to reflect these changes.
This commit is contained in:
@@ -150,6 +150,10 @@ export const facesApi = {
|
||||
min_quality?: number
|
||||
date_from?: string
|
||||
date_to?: string
|
||||
date_taken_from?: string
|
||||
date_taken_to?: string
|
||||
date_processed_from?: string
|
||||
date_processed_to?: string
|
||||
sort_by?: 'quality' | 'date_taken' | 'date_added'
|
||||
sort_dir?: 'asc' | 'desc'
|
||||
}): Promise<UnidentifiedFacesResponse> => {
|
||||
|
||||
+37
-122
@@ -13,8 +13,10 @@ export default function Identify() {
|
||||
const [minQuality, setMinQuality] = useState(0.0)
|
||||
const [sortBy, setSortBy] = useState<SortBy>('quality')
|
||||
const [sortDir, setSortDir] = useState<SortDir>('desc')
|
||||
const [dateFrom, setDateFrom] = useState<string>('')
|
||||
const [dateTo, setDateTo] = useState<string>('')
|
||||
const [dateTakenFrom, setDateTakenFrom] = useState<string>('')
|
||||
const [dateTakenTo, setDateTakenTo] = useState<string>('')
|
||||
const [dateProcessedFrom, setDateProcessedFrom] = useState<string>('')
|
||||
const [dateProcessedTo, setDateProcessedTo] = useState<string>('')
|
||||
|
||||
const [currentIdx, setCurrentIdx] = useState(0)
|
||||
const currentFace = faces[currentIdx]
|
||||
@@ -35,7 +37,6 @@ export default function Identify() {
|
||||
const [imageLoading, setImageLoading] = useState(false)
|
||||
const [filtersCollapsed, setFiltersCollapsed] = useState(false)
|
||||
const [loadingFaces, setLoadingFaces] = useState(false)
|
||||
const [loadingProgress, setLoadingProgress] = useState({ current: 0, total: 0, message: '' })
|
||||
|
||||
// Store form data per face ID (matching desktop behavior)
|
||||
const [faceFormData, setFaceFormData] = useState<Record<number, {
|
||||
@@ -58,22 +59,22 @@ export default function Identify() {
|
||||
|
||||
const loadFaces = async () => {
|
||||
setLoadingFaces(true)
|
||||
setLoadingProgress({ current: 0, total: 0, message: 'Loading faces...' })
|
||||
|
||||
try {
|
||||
const res = await facesApi.getUnidentified({
|
||||
page: 1,
|
||||
page_size: pageSize,
|
||||
min_quality: minQuality,
|
||||
date_from: dateFrom || undefined,
|
||||
date_to: dateTo || undefined,
|
||||
date_taken_from: dateTakenFrom || undefined,
|
||||
date_taken_to: dateTakenTo || undefined,
|
||||
date_processed_from: dateProcessedFrom || undefined,
|
||||
date_processed_to: dateProcessedTo || undefined,
|
||||
sort_by: sortBy,
|
||||
sort_dir: sortDir,
|
||||
})
|
||||
|
||||
// Apply unique faces filter if enabled
|
||||
if (uniqueFacesOnly) {
|
||||
setLoadingProgress({ current: 0, total: res.items.length, message: 'Filtering unique faces...' })
|
||||
const filtered = await filterUniqueFaces(res.items)
|
||||
setFaces(filtered)
|
||||
setTotal(filtered.length)
|
||||
@@ -84,7 +85,6 @@ export default function Identify() {
|
||||
setCurrentIdx(0)
|
||||
} finally {
|
||||
setLoadingFaces(false)
|
||||
setLoadingProgress({ current: 0, total: 0, message: '' })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,58 +102,22 @@ export default function Identify() {
|
||||
similarityMap.set(face.id, new Set<number>())
|
||||
}
|
||||
|
||||
// Update progress - loading all faces once
|
||||
setLoadingProgress({
|
||||
current: 0,
|
||||
total: faces.length,
|
||||
message: 'Loading all faces from database...'
|
||||
})
|
||||
|
||||
try {
|
||||
// Get all face IDs
|
||||
const faceIds = faces.map(f => f.id)
|
||||
|
||||
// Update progress - calculating similarities
|
||||
setLoadingProgress({
|
||||
current: 0,
|
||||
total: faces.length,
|
||||
message: `Calculating similarities for ${faces.length} faces (this may take a while)...`
|
||||
})
|
||||
|
||||
// Call batch similarity endpoint - loads all faces once from DB
|
||||
// Note: This is where the heavy computation happens (comparing N faces to M faces)
|
||||
// The progress bar will show 0% during this time as we can't track backend progress
|
||||
// Call batch similarity endpoint - optimized with vectorized operations
|
||||
const batchRes = await facesApi.batchSimilarity({
|
||||
face_ids: faceIds,
|
||||
min_confidence: 60.0
|
||||
})
|
||||
|
||||
// Update progress - calculation complete, now processing results
|
||||
const totalPairs = batchRes.pairs.length
|
||||
setLoadingProgress({
|
||||
current: 0,
|
||||
total: totalPairs,
|
||||
message: `Similarity calculation complete! Processing ${totalPairs} results...`
|
||||
})
|
||||
|
||||
// Build similarity map from batch results
|
||||
// Note: results include similarities to all faces in DB, but we only care about
|
||||
// similarities between faces in the current list
|
||||
let processedPairs = 0
|
||||
for (const pair of batchRes.pairs) {
|
||||
// Only include pairs where both faces are in the current list
|
||||
if (!faceMap.has(pair.face_id_1) || !faceMap.has(pair.face_id_2)) {
|
||||
processedPairs++
|
||||
// Update progress every 100 pairs or at the end
|
||||
if (processedPairs % 100 === 0 || processedPairs === totalPairs) {
|
||||
setLoadingProgress({
|
||||
current: processedPairs,
|
||||
total: totalPairs,
|
||||
message: `Processing similarity results... (${processedPairs} / ${totalPairs})`
|
||||
})
|
||||
// Allow UI to update
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -165,18 +129,6 @@ export default function Identify() {
|
||||
const set2 = similarityMap.get(pair.face_id_2) || new Set<number>()
|
||||
set2.add(pair.face_id_1)
|
||||
similarityMap.set(pair.face_id_2, set2)
|
||||
|
||||
processedPairs++
|
||||
// Update progress every 100 pairs or at the end
|
||||
if (processedPairs % 100 === 0 || processedPairs === totalPairs) {
|
||||
setLoadingProgress({
|
||||
current: processedPairs,
|
||||
total: totalPairs,
|
||||
message: `Processing similarity results... (${processedPairs} / ${totalPairs})`
|
||||
})
|
||||
// Allow UI to update
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently skip on error - return original faces
|
||||
@@ -434,53 +386,6 @@ export default function Identify() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">Identify</h1>
|
||||
|
||||
{/* Loading Progress Bar */}
|
||||
{loadingFaces && (
|
||||
<div className="bg-white rounded-lg shadow p-4 mb-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{loadingProgress.message || 'Loading faces...'}
|
||||
</span>
|
||||
{loadingProgress.total > 0 && (
|
||||
<span className="text-sm text-gray-500">
|
||||
{loadingProgress.current} / {loadingProgress.total}
|
||||
{loadingProgress.total > 0 && (
|
||||
<span className="ml-1">
|
||||
({Math.round((loadingProgress.current / loadingProgress.total) * 100)}%)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
||||
{loadingProgress.total > 0 ? (
|
||||
<div
|
||||
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.max(1, (loadingProgress.current / loadingProgress.total) * 100)}%`
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="relative h-2.5 overflow-hidden rounded-full bg-gray-200">
|
||||
<div
|
||||
className="absolute h-2.5 bg-blue-600 rounded-full"
|
||||
style={{
|
||||
width: '30%',
|
||||
animation: 'slide 1.5s ease-in-out infinite',
|
||||
left: '-30%'
|
||||
}}
|
||||
/>
|
||||
<style>{`
|
||||
@keyframes slide {
|
||||
0% { left: -30%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
{/* Left: Controls and current face */}
|
||||
@@ -518,13 +423,23 @@ export default function Identify() {
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Date From</label>
|
||||
<input type="date" value={dateFrom} onChange={(e) => setDateFrom(e.target.value)}
|
||||
<label className="block text-sm font-medium text-gray-700">Date Taken From</label>
|
||||
<input type="date" value={dateTakenFrom} onChange={(e) => setDateTakenFrom(e.target.value)}
|
||||
className="mt-1 block w-full border rounded px-2 py-1" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Date To</label>
|
||||
<input type="date" value={dateTo} onChange={(e) => setDateTo(e.target.value)}
|
||||
<label className="block text-sm font-medium text-gray-700">Date Taken To</label>
|
||||
<input type="date" value={dateTakenTo} onChange={(e) => setDateTakenTo(e.target.value)}
|
||||
className="mt-1 block w-full border rounded px-2 py-1" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Date Processed From</label>
|
||||
<input type="date" value={dateProcessedFrom} onChange={(e) => setDateProcessedFrom(e.target.value)}
|
||||
className="mt-1 block w-full border rounded px-2 py-1" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">Date Processed To</label>
|
||||
<input type="date" value={dateProcessedTo} onChange={(e) => setDateProcessedTo(e.target.value)}
|
||||
className="mt-1 block w-full border rounded px-2 py-1" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -546,20 +461,6 @@ export default function Identify() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 pt-3 border-t">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={uniqueFacesOnly}
|
||||
onChange={(e) => setUniqueFacesOnly(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Unique faces only</span>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mt-1 ml-6">
|
||||
Hide duplicates with ≥60% match confidence
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 pt-3 border-t">
|
||||
<button
|
||||
onClick={loadFaces}
|
||||
disabled={loadingFaces}
|
||||
@@ -570,6 +471,20 @@ export default function Identify() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 border-t">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={uniqueFacesOnly}
|
||||
onChange={(e) => setUniqueFacesOnly(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Unique faces only</span>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mt-1 ml-6">
|
||||
Hide duplicates with ≥60% match confidence
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
|
||||
Reference in New Issue
Block a user