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.

This commit is contained in:
tanyar09 2025-09-26 12:38:46 -04:00
parent 2fcd200cd0
commit ee3638b929

View File

@ -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('<Return>', on_enter)
last_name_combo.bind('<Return>', on_enter)
first_name_entry.bind('<Return>', on_enter)
last_name_entry.bind('<Return>', 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