From ee3638b929868837f559fcef463aa9246fa322ed Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Fri, 26 Sep 2025 12:38:46 -0400 Subject: [PATCH] Enhance PhotoTagger by adding support for middle and maiden names in the database and GUI. Update data handling to accommodate new input fields, ensuring comprehensive data capture during identification. Revise database queries and improve user interface for better data entry experience. --- photo_tagger.py | 265 +++++++++++++++++++++++++++--------------------- 1 file changed, 147 insertions(+), 118 deletions(-) diff --git a/photo_tagger.py b/photo_tagger.py index 84a19e6..09aec32 100644 --- a/photo_tagger.py +++ b/photo_tagger.py @@ -163,9 +163,11 @@ class PhotoTagger: id INTEGER PRIMARY KEY AUTOINCREMENT, first_name TEXT NOT NULL, last_name TEXT NOT NULL, + middle_name TEXT, + maiden_name TEXT, date_of_birth DATE, created_date DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE(first_name, last_name, date_of_birth) + UNIQUE(first_name, last_name, middle_name, maiden_name, date_of_birth) ) ''') @@ -427,13 +429,11 @@ class PhotoTagger: saved_count = 0 for face_id, person_data in face_person_names.items(): - # Handle both old format (string) and new format (dict) - if isinstance(person_data, dict): - person_name = person_data.get('name', '').strip() - date_of_birth = person_data.get('date_of_birth', '').strip() - else: - person_name = person_data.strip() if person_data else '' - date_of_birth = '' + # 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: @@ -448,8 +448,8 @@ class PhotoTagger: first_name = parts[0] if parts else '' last_name = '' - cursor.execute('INSERT OR IGNORE INTO people (first_name, last_name, date_of_birth) VALUES (?, ?, ?)', (first_name, last_name, date_of_birth)) - cursor.execute('SELECT id FROM people WHERE first_name = ? AND last_name = ? AND date_of_birth = ?', (first_name, last_name, date_of_birth)) + 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 @@ -490,23 +490,19 @@ class PhotoTagger: pending_identifications = {} for k, v in face_person_names.items(): if k not in face_status or face_status[k] != 'identified': - # Handle both old format (string) and new format (dict) - if isinstance(v, dict): - 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 - else: - # Old string format - not complete (missing date of birth) - pass + # 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 pending_identifications: # Ask user if they want to save pending identifications @@ -589,26 +585,39 @@ class PhotoTagger: input_frame.columnconfigure(1, weight=1) input_frame.columnconfigure(3, weight=1) input_frame.columnconfigure(5, weight=1) + input_frame.columnconfigure(7, weight=1) - # First name input with dropdown + # First name input ttk.Label(input_frame, text="First name:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10)) first_name_var = tk.StringVar() - first_name_combo = ttk.Combobox(input_frame, textvariable=first_name_var, width=12, state="normal") - first_name_combo.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 5)) + first_name_entry = ttk.Entry(input_frame, textvariable=first_name_var, width=12) + first_name_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=(0, 5)) - # Last name input with dropdown + # Last name input ttk.Label(input_frame, text="Last name:").grid(row=0, column=2, sticky=tk.W, padx=(10, 10)) last_name_var = tk.StringVar() - last_name_combo = ttk.Combobox(input_frame, textvariable=last_name_var, width=12, state="normal") - last_name_combo.grid(row=0, column=3, sticky=(tk.W, tk.E), padx=(0, 5)) + last_name_entry = ttk.Entry(input_frame, textvariable=last_name_var, width=12) + last_name_entry.grid(row=0, column=3, sticky=(tk.W, tk.E), padx=(0, 5)) + + # Middle name input + ttk.Label(input_frame, text="Middle name:").grid(row=0, column=4, sticky=tk.W, padx=(10, 10)) + middle_name_var = tk.StringVar() + middle_name_entry = ttk.Entry(input_frame, textvariable=middle_name_var, width=12) + middle_name_entry.grid(row=0, column=5, sticky=(tk.W, tk.E), padx=(0, 5)) # Date of birth input with calendar chooser - ttk.Label(input_frame, text="Date of birth:").grid(row=0, column=4, sticky=tk.W, padx=(10, 10)) + ttk.Label(input_frame, text="Date of birth:").grid(row=1, column=0, sticky=tk.W, padx=(0, 10)) date_of_birth_var = tk.StringVar() # Create a frame for the date picker date_frame = ttk.Frame(input_frame) - date_frame.grid(row=0, column=5, sticky=(tk.W, tk.E), padx=(0, 10)) + date_frame.grid(row=1, column=1, sticky=(tk.W, tk.E), padx=(0, 10)) + + # Maiden name input + ttk.Label(input_frame, text="Maiden name:").grid(row=1, column=2, sticky=tk.W, padx=(10, 10)) + maiden_name_var = tk.StringVar() + maiden_name_entry = ttk.Entry(input_frame, textvariable=maiden_name_var, width=12) + maiden_name_entry.grid(row=1, column=3, sticky=(tk.W, tk.E), padx=(0, 5)) # Date display entry (read-only) date_of_birth_entry = ttk.Entry(date_frame, textvariable=date_of_birth_var, width=12, state='readonly') @@ -1025,6 +1034,8 @@ class PhotoTagger: # Save person name and date of birth 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: @@ -1036,9 +1047,11 @@ class PhotoTagger: current_name = first_name else: current_name = "" - # Store both name and date of birth + # Store all fields face_person_names[current_face_id] = { 'name': current_name, + 'middle_name': middle_name, + 'maiden_name': maiden_name, 'date_of_birth': date_of_birth } @@ -1050,6 +1063,8 @@ class PhotoTagger: nonlocal command, waiting_for_input 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() compare_enabled = compare_var.get() @@ -1083,6 +1098,13 @@ class PhotoTagger: else: command = "" + # Store the additional fields for database insertion + # We'll pass them through the command structure + if middle_name or maiden_name: + command += f"|{middle_name}|{maiden_name}|{date_of_birth}" + else: + command += f"|||{date_of_birth}" + if not command: print("⚠️ Please enter at least a first name or last name before identifying") return @@ -1145,23 +1167,19 @@ class PhotoTagger: pending_identifications = {} for k, v in face_person_names.items(): if k not in face_status or face_status[k] != 'identified': - # Handle both old format (string) and new format (dict) - if isinstance(v, dict): - 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 - else: - # Old string format - not complete (missing date of birth) - pass + # 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 pending_identifications: # Ask user if they want to save pending identifications @@ -1200,31 +1218,6 @@ class PhotoTagger: root.quit() - def update_people_dropdown(): - """Update the dropdowns with current people names""" - # Use cached people names instead of database query - if 'people_names' in identify_data_cache: - # Split cached names back into first and last names - first_names = set() - last_names = set() - for full_name in identify_data_cache['people_names']: - parts = full_name.strip().split(' ', 1) - if parts: - first_names.add(parts[0]) - if len(parts) > 1: - last_names.add(parts[1]) - first_name_combo['values'] = sorted(list(first_names)) - last_name_combo['values'] = sorted(list(last_names)) - else: - # Fallback to database if cache not available - with self.get_db_connection() as conn: - cursor = conn.cursor() - cursor.execute('SELECT first_name, last_name FROM people ORDER BY first_name, last_name') - people = cursor.fetchall() - first_names = sorted(list(set([first for first, last in people if first]))) - last_names = sorted(list(set([last for first, last in people if last]))) - first_name_combo['values'] = first_names - last_name_combo['values'] = last_names def update_button_states(): """Update button states based on current position and unidentified faces""" @@ -1275,8 +1268,8 @@ class PhotoTagger: def on_enter(event): on_identify() - first_name_combo.bind('', on_enter) - last_name_combo.bind('', on_enter) + first_name_entry.bind('', on_enter) + last_name_entry.bind('', on_enter) # Bottom control panel control_frame = ttk.Frame(main_frame) @@ -1305,8 +1298,6 @@ class PhotoTagger: conn.close() return 0 - # Initialize the people dropdown - update_people_dropdown() # Process each face with back navigation support @@ -1403,8 +1394,9 @@ class PhotoTagger: # Update button states update_button_states() - # Update similar faces panel - update_similar_faces() + # Update similar faces panel if compare is enabled + if compare_var.get(): + update_similar_faces() # Update photo info if is_already_identified: @@ -1479,7 +1471,7 @@ class PhotoTagger: last_name_var.set(parts[0].strip()) first_name_var.set(parts[1].strip()) else: - # Fallback for old format or single name + # Single name format first_name_var.set(full_name) last_name_var.set("") elif is_already_identified: @@ -1487,7 +1479,7 @@ class PhotoTagger: with self.get_db_connection() as conn: cursor = conn.cursor() cursor.execute(''' - SELECT p.first_name, p.last_name FROM people p + SELECT p.first_name, p.last_name, p.middle_name, p.maiden_name, p.date_of_birth FROM people p JOIN faces f ON p.id = f.person_id WHERE f.id = ? ''', (face_id,)) @@ -1495,17 +1487,25 @@ class PhotoTagger: if result: first_name_var.set(result[0] or "") last_name_var.set(result[1] or "") + middle_name_var.set(result[2] or "") + maiden_name_var.set(result[3] or "") + date_of_birth_var.set(result[4] or "") else: first_name_var.set("") last_name_var.set("") + middle_name_var.set("") + maiden_name_var.set("") + date_of_birth_var.set("") else: first_name_var.set("") last_name_var.set("") + middle_name_var.set("") + maiden_name_var.set("") date_of_birth_var.set("") # Keep compare checkbox state persistent across navigation - first_name_combo.focus_set() - first_name_combo.icursor(0) + first_name_entry.focus_set() + first_name_entry.icursor(0) # Force GUI update before waiting for input root.update_idletasks() @@ -1581,9 +1581,14 @@ class PhotoTagger: # Clear date of birth field when moving to next face date_of_birth_var.set("") + # Clear middle name and maiden name fields when moving to next face + middle_name_var.set("") + maiden_name_var.set("") update_button_states() - update_similar_faces() + # Only update similar faces if compare is enabled + if compare_var.get(): + update_similar_faces() continue elif command.lower() == 'back': @@ -1611,6 +1616,8 @@ class PhotoTagger: if isinstance(person_data, dict): 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() # Parse "Last, First" format back to separate fields if ', ' in person_name: @@ -1618,25 +1625,33 @@ class PhotoTagger: last_name_var.set(parts[0].strip()) first_name_var.set(parts[1].strip()) else: - # Fallback for single name + # Single name format first_name_var.set(person_name) last_name_var.set("") - # Repopulate date of birth + # Repopulate all fields date_of_birth_var.set(date_of_birth) + middle_name_var.set(middle_name) + maiden_name_var.set(maiden_name) else: - # Old format - clear fields + # Clear fields first_name_var.set("") last_name_var.set("") + middle_name_var.set("") + maiden_name_var.set("") date_of_birth_var.set("") else: # No saved data - clear fields first_name_var.set("") last_name_var.set("") + middle_name_var.set("") + maiden_name_var.set("") date_of_birth_var.set("") update_button_states() - update_similar_faces() + # Only update similar faces if compare is enabled + if compare_var.get(): + update_similar_faces() continue elif command.lower() == 'list': @@ -1657,15 +1672,18 @@ class PhotoTagger: 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)] + # Parse person_name with additional fields (middle_name|maiden_name|date_of_birth) + name_part, middle_name, maiden_name, date_of_birth = person_name.split('|', 3) + parts = [p.strip() for p in name_part.split(',', 1)] + if len(parts) == 2: last_name, first_name = parts[0], parts[1] else: first_name = parts[0] if parts else '' last_name = '' - cursor.execute('INSERT OR IGNORE INTO people (first_name, last_name, date_of_birth) VALUES (?, ?, ?)', (first_name, last_name, None)) - cursor.execute('SELECT id FROM people WHERE first_name = ? AND last_name = ? AND date_of_birth IS NULL', (first_name, last_name)) + 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 @@ -1692,8 +1710,6 @@ class PhotoTagger: print(f"✅ Identified current face and {len(selected_face_ids)} similar faces as: {person_name}") identified_count += 1 + len(selected_face_ids) - # Update the people dropdown to include the new person - update_people_dropdown() # Update person encodings after database transaction is complete self._update_person_encodings(person_id) @@ -1705,15 +1721,18 @@ class PhotoTagger: cursor = conn.cursor() # Add person if doesn't exist # Parse command in "Last, First" or single-token format - parts = [p.strip() for p in command.split(',', 1)] + # Parse command with additional fields (middle_name|maiden_name|date_of_birth) + name_part, middle_name, maiden_name, date_of_birth = command.split('|', 3) + parts = [p.strip() for p in name_part.split(',', 1)] + if len(parts) == 2: last_name, first_name = parts[0], parts[1] else: first_name = parts[0] if parts else '' last_name = '' - cursor.execute('INSERT OR IGNORE INTO people (first_name, last_name, date_of_birth) VALUES (?, ?, ?)', (first_name, last_name, None)) - cursor.execute('SELECT id FROM people WHERE first_name = ? AND last_name = ? AND date_of_birth IS NULL', (first_name, last_name)) + 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 @@ -1737,30 +1756,33 @@ class PhotoTagger: # Mark this face as identified in our tracking face_status[face_id] = 'identified' - # Update the people dropdown to include the new person - update_people_dropdown() # Update person encodings after database transaction is complete self._update_person_encodings(person_id) except Exception as e: print(f"❌ Error: {e}") + + # Increment index for normal flow (identification or error) - but not if we're at the last item + if i < len(original_faces) - 1: + i += 1 + update_button_states() + # Only update similar faces if compare is enabled + if compare_var.get(): + update_similar_faces() + + # Clean up current face crop when moving forward after identification + if face_crop_path and os.path.exists(face_crop_path): + try: + os.remove(face_crop_path) + except: + pass # Ignore cleanup errors + current_face_crop_path = None # Clear tracked path + + # Continue to next face after processing command + continue else: print("Please enter a name, 's' to skip, 'q' to quit, or use buttons") - - # Increment index for normal flow (identification or error) - but not if we're at the last item - if i < len(original_faces) - 1: - i += 1 - update_button_states() - update_similar_faces() - - # Clean up current face crop when moving forward after identification - if face_crop_path and os.path.exists(face_crop_path): - try: - os.remove(face_crop_path) - except: - pass # Ignore cleanup errors - current_face_crop_path = None # Clear tracked path # Only close the window if user explicitly quit (not when reaching end of faces) if not window_destroyed: @@ -2260,7 +2282,14 @@ class PhotoTagger: # Top people cursor.execute(''' - SELECT p.name, COUNT(f.id) as face_count + SELECT + CASE + WHEN p.last_name AND p.first_name THEN p.last_name || ', ' || p.first_name + WHEN p.first_name THEN p.first_name + WHEN p.last_name THEN p.last_name + ELSE 'Unknown' + END as full_name, + COUNT(f.id) as face_count FROM people p LEFT JOIN faces f ON p.id = f.person_id GROUP BY p.id