feat: Implement auto-match automation plan with enhanced API and frontend support
This commit introduces a comprehensive auto-match automation plan that automates the face matching process in the application. Key features include the ability to automatically identify faces based on pose and similarity thresholds, with configurable options for auto-acceptance. The API has been updated to support new parameters for auto-acceptance and pose filtering, while the frontend has been enhanced to allow users to set an auto-accept threshold and view results. Documentation has been updated to reflect these changes, improving user experience and functionality.
This commit is contained in:
@@ -76,6 +76,8 @@ export interface BatchUnmatchResponse {
|
||||
|
||||
export interface AutoMatchRequest {
|
||||
tolerance: number
|
||||
auto_accept?: boolean
|
||||
auto_accept_threshold?: number
|
||||
}
|
||||
|
||||
export interface AutoMatchFaceItem {
|
||||
@@ -86,6 +88,7 @@ export interface AutoMatchFaceItem {
|
||||
quality_score: number
|
||||
similarity: number // Confidence percentage (0-100)
|
||||
distance: number
|
||||
pose_mode?: string
|
||||
}
|
||||
|
||||
export interface AutoMatchPersonItem {
|
||||
@@ -95,6 +98,7 @@ export interface AutoMatchPersonItem {
|
||||
reference_photo_id: number
|
||||
reference_photo_filename: string
|
||||
reference_location: string
|
||||
reference_pose_mode?: string
|
||||
face_count: number
|
||||
matches: AutoMatchFaceItem[]
|
||||
total_matches: number
|
||||
@@ -104,6 +108,10 @@ export interface AutoMatchResponse {
|
||||
people: AutoMatchPersonItem[]
|
||||
total_people: number
|
||||
total_matches: number
|
||||
auto_accepted?: boolean
|
||||
auto_accepted_faces?: number
|
||||
skipped_persons?: number
|
||||
skipped_matches?: number
|
||||
}
|
||||
|
||||
export interface AcceptMatchesRequest {
|
||||
|
||||
@@ -7,6 +7,7 @@ const DEFAULT_TOLERANCE = 0.6
|
||||
|
||||
export default function AutoMatch() {
|
||||
const [tolerance, setTolerance] = useState(DEFAULT_TOLERANCE)
|
||||
const [autoAcceptThreshold, setAutoAcceptThreshold] = useState(70)
|
||||
const [isActive, setIsActive] = useState(false)
|
||||
const [people, setPeople] = useState<AutoMatchPersonItem[]>([])
|
||||
const [filteredPeople, setFilteredPeople] = useState<AutoMatchPersonItem[]>([])
|
||||
@@ -16,6 +17,8 @@ export default function AutoMatch() {
|
||||
const [originalSelectedFaces, setOriginalSelectedFaces] = useState<Record<number, boolean>>({})
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [hasNoResults, setHasNoResults] = useState(false)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
|
||||
const currentPerson = useMemo(() => {
|
||||
const activePeople = filteredPeople.length > 0 ? filteredPeople : people
|
||||
@@ -26,9 +29,48 @@ export default function AutoMatch() {
|
||||
return currentPerson?.matches || []
|
||||
}, [currentPerson])
|
||||
|
||||
// Auto-start auto-match when component mounts or tolerance changes
|
||||
// Shared function for auto-load and refresh
|
||||
const loadAutoMatch = async () => {
|
||||
if (tolerance < 0 || tolerance > 1) {
|
||||
return
|
||||
}
|
||||
|
||||
setBusy(true)
|
||||
setIsRefreshing(true)
|
||||
try {
|
||||
const response = await facesApi.autoMatch({
|
||||
tolerance,
|
||||
auto_accept: false // Don't auto-accept on load/refresh, only on button click
|
||||
})
|
||||
|
||||
if (response.people.length === 0) {
|
||||
setHasNoResults(true)
|
||||
setPeople([])
|
||||
setFilteredPeople([])
|
||||
setIsActive(false)
|
||||
setBusy(false)
|
||||
setIsRefreshing(false)
|
||||
return
|
||||
}
|
||||
|
||||
setHasNoResults(false)
|
||||
setPeople(response.people)
|
||||
setFilteredPeople([])
|
||||
setCurrentIndex(0)
|
||||
setSelectedFaces({})
|
||||
setOriginalSelectedFaces({})
|
||||
setIsActive(true)
|
||||
} catch (error) {
|
||||
console.error('Auto-match failed:', error)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-start auto-match when component mounts or tolerance changes (without auto-accept)
|
||||
useEffect(() => {
|
||||
startAutoMatch()
|
||||
loadAutoMatch()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tolerance])
|
||||
|
||||
@@ -64,16 +106,43 @@ export default function AutoMatch() {
|
||||
return
|
||||
}
|
||||
|
||||
if (autoAcceptThreshold < 0 || autoAcceptThreshold > 100) {
|
||||
alert('Please enter a valid auto-accept threshold between 0 and 100.')
|
||||
return
|
||||
}
|
||||
|
||||
setBusy(true)
|
||||
try {
|
||||
const response = await facesApi.autoMatch({ tolerance })
|
||||
const response = await facesApi.autoMatch({
|
||||
tolerance,
|
||||
auto_accept: true,
|
||||
auto_accept_threshold: autoAcceptThreshold
|
||||
})
|
||||
|
||||
// Show summary if auto-accept was performed
|
||||
if (response.auto_accepted) {
|
||||
const summary = [
|
||||
`✅ Auto-matched ${response.auto_accepted_faces || 0} faces`,
|
||||
response.skipped_persons ? `⚠️ Skipped ${response.skipped_persons} persons (non-frontal reference)` : '',
|
||||
response.skipped_matches ? `ℹ️ Skipped ${response.skipped_matches} matches (didn't meet criteria)` : ''
|
||||
].filter(Boolean).join('\n')
|
||||
|
||||
if (summary) {
|
||||
alert(summary)
|
||||
}
|
||||
}
|
||||
|
||||
if (response.people.length === 0) {
|
||||
alert('🔍 No similar faces found for auto-identification')
|
||||
setHasNoResults(true)
|
||||
setPeople([])
|
||||
setFilteredPeople([])
|
||||
setIsActive(false)
|
||||
setBusy(false)
|
||||
return
|
||||
}
|
||||
|
||||
setHasNoResults(false)
|
||||
setPeople(response.people)
|
||||
setFilteredPeople([])
|
||||
setCurrentIndex(0)
|
||||
@@ -180,11 +249,11 @@ export default function AutoMatch() {
|
||||
<div className="bg-white rounded-lg shadow p-4 mb-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={startAutoMatch}
|
||||
disabled={busy || isActive}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
onClick={loadAutoMatch}
|
||||
disabled={busy}
|
||||
className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
{busy ? 'Processing...' : '🚀 Start Auto-Match'}
|
||||
{isRefreshing ? 'Refreshing...' : '🔄 Refresh'}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Tolerance:</label>
|
||||
@@ -195,11 +264,33 @@ export default function AutoMatch() {
|
||||
step="0.1"
|
||||
value={tolerance}
|
||||
onChange={(e) => setTolerance(parseFloat(e.target.value) || 0)}
|
||||
disabled={isActive}
|
||||
disabled={busy}
|
||||
className="w-20 px-2 py-1 border border-gray-300 rounded text-sm"
|
||||
/>
|
||||
<span className="text-xs text-gray-500">(lower = stricter matching)</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={startAutoMatch}
|
||||
disabled={busy || hasNoResults}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
title={hasNoResults ? 'No matches found. Adjust tolerance or process more photos.' : ''}
|
||||
>
|
||||
{busy ? 'Processing...' : hasNoResults ? 'No Matches Available' : '🚀 Start Auto-Match'}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">Auto-Accept Threshold:</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
step="5"
|
||||
value={autoAcceptThreshold}
|
||||
onChange={(e) => setAutoAcceptThreshold(parseInt(e.target.value) || 70)}
|
||||
disabled={busy || hasNoResults}
|
||||
className="w-20 px-2 py-1 border border-gray-300 rounded text-sm"
|
||||
/>
|
||||
<span className="text-xs text-gray-500">% (min similarity)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user