diff --git a/README.md b/README.md index 0c365bb..199d214 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/photo_tagger.py b/photo_tagger.py index 787e616..dc1274a 100644 --- a/photo_tagger.py +++ b/photo_tagger.py @@ -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()