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