Refactor PhotoTagger to enhance data handling by storing person information in separate fields for first name, last name, middle name, maiden name, and date of birth. Improve identification logic to ensure data integrity and streamline user experience. Update README to reflect changes in data storage and new features.

This commit is contained in:
tanyar09 2025-09-26 15:22:24 -04:00
parent 9f11a1a647
commit 8c9da7362b
2 changed files with 124 additions and 110 deletions

View File

@ -129,7 +129,7 @@ python3 photo_tagger.py auto-match --auto --show-faces
**🎯 New GUI-Based Identification Features:**
- 🖼️ **Visual Face Display** - See individual face crops in the GUI
- 📝 **Separate Name Fields** - Dedicated text input fields for first name, last name, middle name, and maiden name
- 🎯 **Smart Name Parsing** - Supports "Last, First" format that gets properly parsed and stored
- 🎯 **Direct Field Storage** - Names are stored directly in separate fields for maximum reliability
- ☑️ **Compare with Similar Faces** - Compare current face with similar unidentified faces
- 🎨 **Modern Interface** - Clean, intuitive GUI with buttons and input fields
- 💾 **Window Size Memory** - Remembers your preferred window size
@ -142,6 +142,7 @@ python3 photo_tagger.py auto-match --auto --show-faces
- 💾 **Quit Confirmation** - Saves pending identifications when closing the application
- ⚡ **Performance Optimized** - Pre-fetched data for faster similar faces display
- 🎯 **Clean Database Storage** - Names are stored as separate first_name and last_name fields without commas
- 🔧 **Improved Data Handling** - Fixed field restoration and quit confirmation logic for better reliability
**🎯 New Auto-Match GUI Features:**
- 📊 **Person-Centric View** - Shows matched person on left, all their unidentified faces on right
@ -626,7 +627,14 @@ This is now a minimal, focused tool. Key principles:
## 🆕 Recent Improvements (Latest Version)
### 🔄 Field Navigation & Preservation Fixes (NEW!)
### 🔧 Data Storage & Reliability Improvements (NEW!)
- ✅ **Eliminated Redundant Storage** - Removed unnecessary combined name field for cleaner data structure
- ✅ **Direct Field Access** - Names stored and accessed directly without parsing/combining logic
- ✅ **Fixed Quit Confirmation** - Proper detection of pending identifications when quitting
- ✅ **Improved Error Handling** - Better type consistency prevents runtime errors
- ✅ **Enhanced Performance** - Eliminated string manipulation overhead for faster operations
### 🔄 Field Navigation & Preservation Fixes
- ✅ **Fixed Name Field Confusion** - First and last names now stay in correct fields during navigation
- ✅ **Enhanced Data Storage** - Individual field tracking prevents name swapping issues
- ✅ **Date of Birth Preservation** - Date of birth now preserved even when entered alone (without names)

View File

@ -430,47 +430,48 @@ class PhotoTagger:
for face_id, person_data in face_person_names.items():
# Handle person data dict format
person_name = person_data.get('name', '').strip()
date_of_birth = person_data.get('date_of_birth', '').strip()
middle_name = person_data.get('middle_name', '').strip()
maiden_name = person_data.get('maiden_name', '').strip()
if person_name:
try:
with self.get_db_connection() as conn:
cursor = conn.cursor()
# Add person if doesn't exist
# Parse person_name in "Last, First" or single-token format
parts = [p.strip() for p in person_name.split(',', 1)]
if len(parts) == 2:
last_name, first_name = parts[0], parts[1]
else:
first_name = parts[0] if parts else ''
last_name = ''
if isinstance(person_data, dict):
first_name = person_data.get('first_name', '').strip()
last_name = person_data.get('last_name', '').strip()
date_of_birth = person_data.get('date_of_birth', '').strip()
middle_name = person_data.get('middle_name', '').strip()
maiden_name = person_data.get('maiden_name', '').strip()
# Only save if we have at least a first or last name
if first_name or last_name:
try:
with self.get_db_connection() as conn:
cursor = conn.cursor()
# Add person if doesn't exist
cursor.execute('INSERT OR IGNORE INTO people (first_name, last_name, middle_name, maiden_name, date_of_birth) VALUES (?, ?, ?, ?, ?)', (first_name, last_name, middle_name, maiden_name, date_of_birth))
cursor.execute('SELECT id FROM people WHERE first_name = ? AND last_name = ? AND middle_name = ? AND maiden_name = ? AND date_of_birth = ?', (first_name, last_name, middle_name, maiden_name, date_of_birth))
result = cursor.fetchone()
person_id = result[0] if result else None
cursor.execute('INSERT OR IGNORE INTO people (first_name, last_name, middle_name, maiden_name, date_of_birth) VALUES (?, ?, ?, ?, ?)', (first_name, last_name, middle_name, maiden_name, date_of_birth))
cursor.execute('SELECT id FROM people WHERE first_name = ? AND last_name = ? AND middle_name = ? AND maiden_name = ? AND date_of_birth = ?', (first_name, last_name, middle_name, maiden_name, date_of_birth))
result = cursor.fetchone()
person_id = result[0] if result else None
# Update people cache if new person was added
display_name = f"{last_name}, {first_name}" if last_name and first_name else (last_name or first_name)
if display_name not in identify_data_cache['people_names']:
identify_data_cache['people_names'].append(display_name)
identify_data_cache['people_names'].sort() # Keep sorted
# Assign face to person
cursor.execute(
'UPDATE faces SET person_id = ? WHERE id = ?',
(person_id, face_id)
)
# Update people cache if new person was added
if person_name not in identify_data_cache['people_names']:
identify_data_cache['people_names'].append(person_name)
identify_data_cache['people_names'].sort() # Keep sorted
# Update person encodings
self._update_person_encodings(person_id)
saved_count += 1
display_name = f"{last_name}, {first_name}" if last_name and first_name else (last_name or first_name)
print(f"✅ Saved identification: {display_name}")
# Assign face to person
cursor.execute(
'UPDATE faces SET person_id = ? WHERE id = ?',
(person_id, face_id)
)
# Update person encodings
self._update_person_encodings(person_id)
saved_count += 1
print(f"✅ Saved identification: {person_name}")
except Exception as e:
print(f"❌ Error saving identification for {person_name}: {e}")
except Exception as e:
display_name = f"{last_name}, {first_name}" if last_name and first_name else (last_name or first_name)
print(f"❌ Error saving identification for {display_name}: {e}")
else:
# Handle legacy string format - skip for now as it doesn't have complete data
pass
if saved_count > 0:
identified_count += saved_count
@ -491,18 +492,17 @@ class PhotoTagger:
for k, v in face_person_names.items():
if k not in face_status or face_status[k] != 'identified':
# Handle person data dict format
person_name = v.get('name', '').strip()
date_of_birth = v.get('date_of_birth', '').strip()
# Check if name has both first and last name
if person_name and date_of_birth:
# Parse name to check for both first and last name
if ',' in person_name:
last_name, first_name = person_name.split(',', 1)
if last_name.strip() and first_name.strip():
pending_identifications[k] = v
else:
# Single name format - not complete
pass
if isinstance(v, dict):
first_name = v.get('first_name', '').strip()
last_name = v.get('last_name', '').strip()
date_of_birth = v.get('date_of_birth', '').strip()
# Check if we have complete data (both first and last name, plus date of birth)
if first_name and last_name and date_of_birth:
pending_identifications[k] = v
else:
# Handle legacy string format - not considered complete without date of birth
pass
if pending_identifications:
# Ask user if they want to save pending identifications
@ -640,7 +640,7 @@ class PhotoTagger:
calendar_window.grab_set()
# Calculate center position before showing the window
window_width = 350
window_width = 400
window_height = 400
screen_width = calendar_window.winfo_screenwidth()
screen_height = calendar_window.winfo_screenheight()
@ -928,16 +928,19 @@ class PhotoTagger:
current_face_id = original_faces[i][0]
first_name = first_name_var.get().strip()
last_name = last_name_var.get().strip()
middle_name = middle_name_var.get().strip()
maiden_name = maiden_name_var.get().strip()
date_of_birth = date_of_birth_var.get().strip()
if first_name or last_name:
if last_name and first_name:
current_name = f"{last_name}, {first_name}"
elif last_name:
current_name = last_name
elif first_name:
current_name = first_name
else:
current_name = ""
face_person_names[current_face_id] = current_name
# Store as dictionary to maintain consistency
face_person_names[current_face_id] = {
'first_name': first_name,
'last_name': last_name,
'middle_name': middle_name,
'maiden_name': maiden_name,
'date_of_birth': date_of_birth
}
elif current_face_id in face_person_names:
# Remove empty names from storage
del face_person_names[current_face_id]
@ -947,9 +950,6 @@ class PhotoTagger:
# Buttons moved to bottom of window
# Instructions
instructions = ttk.Label(input_frame, text="Select from dropdown or type new name", foreground="gray")
instructions.grid(row=2, column=0, columnspan=4, pady=(10, 0))
# Right panel for similar faces
similar_faces_frame = ttk.Frame(right_panel)
@ -1039,17 +1039,10 @@ class PhotoTagger:
date_of_birth = date_of_birth_var.get().strip()
if first_name or last_name:
if last_name and first_name:
current_name = f"{last_name}, {first_name}"
elif last_name:
current_name = last_name
elif first_name:
current_name = first_name
else:
current_name = ""
# Store all fields
face_person_names[current_face_id] = {
'name': current_name,
'first_name': first_name,
'last_name': last_name,
'middle_name': middle_name,
'maiden_name': maiden_name,
'date_of_birth': date_of_birth
@ -1168,18 +1161,20 @@ class PhotoTagger:
for k, v in face_person_names.items():
if k not in face_status or face_status[k] != 'identified':
# Handle person data dict format
person_name = v.get('name', '').strip()
date_of_birth = v.get('date_of_birth', '').strip()
# Check if name has both first and last name
if person_name and date_of_birth:
# Parse name to check for both first and last name
if ',' in person_name:
last_name, first_name = person_name.split(',', 1)
if last_name.strip() and first_name.strip():
pending_identifications[k] = v
else:
# Single name format - not complete
pass
if isinstance(v, dict):
first_name = v.get('first_name', '').strip()
last_name = v.get('last_name', '').strip()
date_of_birth = v.get('date_of_birth', '').strip()
# Check if we have complete data (both first and last name, plus date of birth)
if first_name and last_name and date_of_birth:
pending_identifications[k] = v
else:
# Handle legacy string format
person_name = v.strip()
date_of_birth = '' # Legacy format doesn't have date_of_birth
# Legacy format is not considered complete without date of birth
pass
if pending_identifications:
# Ask user if they want to save pending identifications
@ -1464,16 +1459,33 @@ class PhotoTagger:
# Set person name input - restore saved name or use database/empty value
if face_id in face_person_names:
# Restore previously entered name for this face
full_name = face_person_names[face_id]
# Parse "Last, First" format back to separate fields
if ', ' in full_name:
parts = full_name.split(', ', 1)
last_name_var.set(parts[0].strip())
first_name_var.set(parts[1].strip())
person_data = face_person_names[face_id]
if isinstance(person_data, dict):
# Handle dictionary format - use individual field values for proper restoration
first_name = person_data.get('first_name', '').strip()
last_name = person_data.get('last_name', '').strip()
middle_name = person_data.get('middle_name', '').strip()
maiden_name = person_data.get('maiden_name', '').strip()
date_of_birth = person_data.get('date_of_birth', '').strip()
# Restore all fields directly
first_name_var.set(first_name)
last_name_var.set(last_name)
middle_name_var.set(middle_name)
maiden_name_var.set(maiden_name)
date_of_birth_var.set(date_of_birth)
else:
# Single name format
first_name_var.set(full_name)
last_name_var.set("")
# Handle legacy string format (for backward compatibility)
full_name = person_data
# Parse "Last, First" format back to separate fields
if ', ' in full_name:
parts = full_name.split(', ', 1)
last_name_var.set(parts[0].strip())
first_name_var.set(parts[1].strip())
else:
# Single name format
first_name_var.set(full_name)
last_name_var.set("")
elif is_already_identified:
# Pre-populate with the current person name from database
with self.get_db_connection() as conn:
@ -1614,25 +1626,19 @@ class PhotoTagger:
if current_face_id in face_person_names:
person_data = face_person_names[current_face_id]
if isinstance(person_data, dict):
person_name = person_data.get('name', '').strip()
date_of_birth = person_data.get('date_of_birth', '').strip()
# Use individual field values for proper restoration
first_name = person_data.get('first_name', '').strip()
last_name = person_data.get('last_name', '').strip()
middle_name = person_data.get('middle_name', '').strip()
maiden_name = person_data.get('maiden_name', '').strip()
date_of_birth = person_data.get('date_of_birth', '').strip()
# Parse "Last, First" format back to separate fields
if ', ' in person_name:
parts = person_name.split(', ', 1)
last_name_var.set(parts[0].strip())
first_name_var.set(parts[1].strip())
else:
# Single name format
first_name_var.set(person_name)
last_name_var.set("")
# Repopulate all fields
date_of_birth_var.set(date_of_birth)
# Restore all fields directly
first_name_var.set(first_name)
last_name_var.set(last_name)
middle_name_var.set(middle_name)
maiden_name_var.set(maiden_name)
date_of_birth_var.set(date_of_birth)
else:
# Clear fields
first_name_var.set("")
@ -3700,7 +3706,7 @@ class PhotoTagger:
calendar_window.grab_set()
# Calculate center position before showing the window
window_width = 350
window_width = 400
window_height = 400
screen_width = calendar_window.winfo_screenwidth()
screen_height = calendar_window.winfo_screenheight()