From c69604573de76397b41307368764efff4dcd4ef7 Mon Sep 17 00:00:00 2001 From: Tanya Date: Mon, 5 Jan 2026 13:37:45 -0500 Subject: [PATCH] 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. --- admin-frontend/src/pages/Identify.tsx | 29 ++++++++++++++++++++++---- backend/api/faces.py | 4 ++++ backend/api/pending_identifications.py | 2 ++ backend/api/people.py | 3 +++ backend/db/models.py | 27 ++++++++++++++++++++---- backend/services/video_service.py | 2 ++ 6 files changed, 59 insertions(+), 8 deletions(-) diff --git a/admin-frontend/src/pages/Identify.tsx b/admin-frontend/src/pages/Identify.tsx index 699f93c..0625566 100644 --- a/admin-frontend/src/pages/Identify.tsx +++ b/admin-frontend/src/pages/Identify.tsx @@ -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} />
- - - + +
diff --git a/backend/api/faces.py b/backend/api/faces.py index d9dfff3..64f9f4f 100644 --- a/backend/api/faces.py +++ b/backend/api/faces.py @@ -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 diff --git a/backend/api/pending_identifications.py b/backend/api/pending_identifications.py index 110ab59..889b627 100644 --- a/backend/api/pending_identifications.py +++ b/backend/api/pending_identifications.py @@ -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 diff --git a/backend/api/people.py b/backend/api/people.py index 27aadc4..ad6d774 100644 --- a/backend/api/people.py +++ b/backend/api/people.py @@ -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: diff --git a/backend/db/models.py b/backend/db/models.py index 45fe127..6b85d01 100644 --- a/backend/db/models.py +++ b/backend/db/models.py @@ -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): diff --git a/backend/services/video_service.py b/backend/services/video_service.py index b73423b..0009626 100644 --- a/backend/services/video_service.py +++ b/backend/services/video_service.py @@ -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