feat: Improve face identification process with validation and error handling

This commit enhances the face identification process by adding validation checks for person ID and names, ensuring that users provide necessary information before proceeding. It also introduces detailed logging for better debugging and user feedback during the identification process. Additionally, error handling is improved to provide user-friendly messages in case of failures, enhancing the overall user experience.
This commit is contained in:
Tanya 2026-01-05 13:37:45 -05:00
parent 0b95cd2492
commit c69604573d
6 changed files with 59 additions and 8 deletions

View File

@ -692,7 +692,19 @@ export default function Identify() {
const handleIdentify = async () => {
if (!currentFace) return
console.log('handleIdentify called', { currentFace, personId, firstName, lastName, canIdentify })
if (!currentFace) {
console.warn('handleIdentify: No current face')
return
}
// Validate that we have either a person ID or both first and last name
if (!personId && (!firstName.trim() || !lastName.trim())) {
alert('Please select an existing person or enter first name and last name.')
return
}
setBusy(true)
const trimmedFirstName = firstName.trim()
const trimmedLastName = lastName.trim()
@ -719,7 +731,10 @@ export default function Identify() {
payload.date_of_birth = trimmedDob
}
}
console.log('Identifying face:', currentFace.id, 'with payload:', payload)
await facesApi.identify(currentFace.id, payload)
// Optimistic: remove identified faces from list
const identifiedSet = new Set([currentFace.id, ...additional])
const remaining = faces.filter((f) => !identifiedSet.has(f.id))
@ -741,6 +756,10 @@ export default function Identify() {
}
// Don't clear form - let the useEffect handle restoring/clearing when face changes
} catch (error: any) {
console.error('Error identifying face:', error)
const errorMessage = error.response?.data?.detail || error.message || 'Failed to identify face. Please try again.'
alert(errorMessage)
} finally {
setBusy(false)
}
@ -1378,13 +1397,15 @@ export default function Identify() {
disabled={!!personId} />
</div>
<div className="col-span-2 flex gap-2 mt-2">
<button disabled={!canIdentify || busy}
<button
type="button"
disabled={!canIdentify || busy}
onClick={handleIdentify}
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'}
</button>
<button onClick={() => setCurrentIdx((i) => Math.max(0, i - 1))} className="px-3 py-2 rounded border">Back</button>
<button onClick={() => setCurrentIdx((i) => Math.min(faces.length - 1, i + 1))} className="px-3 py-2 rounded border">Next</button>
<button type="button" onClick={() => setCurrentIdx((i) => Math.max(0, i - 1))} className="px-3 py-2 rounded border">Back</button>
<button type="button" onClick={() => setCurrentIdx((i) => Math.min(faces.length - 1, i + 1))} className="px-3 py-2 rounded border">Next</button>
</div>
</div>
</div>

View File

@ -2,6 +2,8 @@
from __future__ import annotations
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi.responses import FileResponse, Response
from rq import Queue
@ -306,12 +308,14 @@ def identify_face(
status_code=status.HTTP_400_BAD_REQUEST,
detail="first_name and last_name are required to create a person",
)
# Explicitly set created_date to ensure it's a valid datetime object
person = Person(
first_name=first_name,
last_name=last_name,
middle_name=middle_name,
maiden_name=maiden_name,
date_of_birth=request.date_of_birth,
created_date=datetime.utcnow(),
)
db.add(person)
db.flush() # get person.id

View File

@ -330,12 +330,14 @@ def approve_deny_pending_identifications(
# Create person if doesn't exist
created_person = False
if not person:
# Explicitly set created_date to ensure it's a valid datetime object
person = Person(
first_name=row.first_name,
last_name=row.last_name,
middle_name=row.middle_name,
maiden_name=row.maiden_name,
date_of_birth=row.date_of_birth,
created_date=datetime.utcnow(),
)
main_db.add(person)
main_db.flush() # get person.id

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import datetime
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
@ -118,12 +119,14 @@ def create_person(request: PersonCreateRequest, db: Session = Depends(get_db)) -
last_name = request.last_name.strip()
middle_name = request.middle_name.strip() if request.middle_name else None
maiden_name = request.maiden_name.strip() if request.maiden_name else None
# Explicitly set created_date to ensure it's a valid datetime object
person = Person(
first_name=first_name,
last_name=last_name,
middle_name=middle_name,
maiden_name=maiden_name,
date_of_birth=request.date_of_birth,
created_date=datetime.utcnow(),
)
db.add(person)
try:

View File

@ -10,6 +10,7 @@ from sqlalchemy import (
Column,
Date,
DateTime,
String,
ForeignKey,
Index,
Integer,
@ -37,19 +38,37 @@ class PrismaCompatibleDateTime(TypeDecorator):
Prisma's SQLite driver has issues with microseconds in datetime strings.
This type ensures datetimes are stored in ISO format without microseconds:
'YYYY-MM-DD HH:MM:SS' instead of 'YYYY-MM-DD HH:MM:SS.ffffff'
Uses String as the underlying type for SQLite to have full control over the format.
"""
impl = DateTime
impl = String
cache_ok = True
def process_bind_param(self, value, dialect):
"""Convert Python datetime to SQL string format."""
"""Convert Python datetime to SQL string format without microseconds."""
if value is None:
return None
if isinstance(value, datetime):
# Strip microseconds and format as ISO string without microseconds
# This ensures Prisma can read it correctly
value = value.replace(microsecond=0)
return value.strftime('%Y-%m-%d %H:%M:%S')
return value.replace(microsecond=0).strftime('%Y-%m-%d %H:%M:%S')
# If it's already a string, ensure it doesn't have microseconds
if isinstance(value, str):
try:
# Parse and reformat to remove microseconds
if '.' in value:
# Has microseconds or timezone info - strip them
dt = datetime.strptime(value.split('.')[0], '%Y-%m-%d %H:%M:%S')
elif 'T' in value:
# ISO format with T
dt = datetime.fromisoformat(value.replace('Z', '+00:00').split('.')[0])
else:
# Already in correct format
return value
return dt.strftime('%Y-%m-%d %H:%M:%S')
except (ValueError, TypeError):
# If parsing fails, return as-is
return value
return value
def process_result_value(self, value, dialect):

View File

@ -224,12 +224,14 @@ def identify_person_in_video(
if existing_person:
person = existing_person
else:
# Explicitly set created_date to ensure it's a valid datetime object
person = Person(
first_name=first_name,
last_name=last_name,
middle_name=middle_name,
maiden_name=maiden_name,
date_of_birth=date_of_birth,
created_date=datetime.utcnow(),
)
db.add(person)
db.flush() # Get person.id