Enhance PhotoTagger to include comprehensive person details in the database and GUI. Update data handling to support middle names, maiden names, and date of birth, improving user experience and data integrity. Revise database queries and UI components for better data entry and display, ensuring all relevant information is captured during identification.

This commit is contained in:
tanyar09 2025-09-26 13:10:30 -04:00
parent ee3638b929
commit 5ecfe1121e

View File

@ -2592,12 +2592,40 @@ class PhotoTagger:
with self.get_db_connection() as conn:
cursor = conn.cursor()
# Pre-fetch all person names
# Pre-fetch all person names and details
person_ids = list(matches_by_matched.keys())
if person_ids:
placeholders = ','.join('?' * len(person_ids))
cursor.execute(f'SELECT id, first_name, last_name FROM people WHERE id IN ({placeholders})', person_ids)
data_cache['person_names'] = {row[0]: f"{row[2]}, {row[1]}".strip(", ").strip() for row in cursor.fetchall()}
cursor.execute(f'SELECT id, first_name, last_name, middle_name, maiden_name, date_of_birth FROM people WHERE id IN ({placeholders})', person_ids)
data_cache['person_details'] = {}
for row in cursor.fetchall():
person_id = row[0]
first_name = row[1] or ''
last_name = row[2] or ''
middle_name = row[3] or ''
maiden_name = row[4] or ''
date_of_birth = row[5] or ''
# Create full name display
name_parts = []
if first_name:
name_parts.append(first_name)
if middle_name:
name_parts.append(middle_name)
if last_name:
name_parts.append(last_name)
if maiden_name:
name_parts.append(f"({maiden_name})")
full_name = ' '.join(name_parts)
data_cache['person_details'][person_id] = {
'full_name': full_name,
'first_name': first_name,
'last_name': last_name,
'middle_name': middle_name,
'maiden_name': maiden_name,
'date_of_birth': date_of_birth
}
# Pre-fetch all photo paths (both matched and unidentified)
all_photo_ids = set()
@ -2612,7 +2640,7 @@ class PhotoTagger:
cursor.execute(f'SELECT id, path FROM photos WHERE id IN ({placeholders})', photo_ids_list)
data_cache['photo_paths'] = {row[0]: row[1] for row in cursor.fetchall()}
print(f"✅ Pre-fetched {len(data_cache.get('person_names', {}))} person names and {len(data_cache.get('photo_paths', {}))} photo paths")
print(f"✅ Pre-fetched {len(data_cache.get('person_details', {}))} person details and {len(data_cache.get('photo_paths', {}))} photo paths")
identified_count = 0
@ -2772,7 +2800,8 @@ class PhotoTagger:
)
# Use cached person name instead of database query
person_name = data_cache['person_names'].get(match['person_id'], "Unknown")
person_details = data_cache['person_details'].get(match['person_id'], {})
person_name = person_details.get('full_name', "Unknown")
# Track this face as identified for this person
identified_faces_per_person[matched_id].add(match['unidentified_id'])
@ -2874,7 +2903,8 @@ class PhotoTagger:
if matches_for_current_person:
person_id = matches_for_current_person[0]['person_id']
# Use cached person name instead of database query
person_name = data_cache['person_names'].get(person_id, "Unknown")
person_details = data_cache['person_details'].get(person_id, {})
person_name = person_details.get('full_name', "Unknown")
save_btn.config(text=f"💾 Save changes for {person_name}")
else:
save_btn.config(text="💾 Save Changes")
@ -2934,11 +2964,22 @@ class PhotoTagger:
first_match = matches_for_this_person[0]
# Use cached data instead of database queries
person_name = data_cache['person_names'].get(first_match['person_id'], "Unknown")
person_details = data_cache['person_details'].get(first_match['person_id'], {})
person_name = person_details.get('full_name', "Unknown")
date_of_birth = person_details.get('date_of_birth', '')
matched_photo_path = data_cache['photo_paths'].get(first_match['matched_photo_id'], None)
# Create detailed person info display
person_info_lines = [f"👤 Person: {person_name}"]
if date_of_birth:
person_info_lines.append(f"📅 Born: {date_of_birth}")
person_info_lines.extend([
f"📁 Photo: {first_match['matched_filename']}",
f"📍 Face location: {first_match['matched_location']}"
])
# Update matched person info
matched_info_label.config(text=f"👤 Person: {person_name}\n📁 Photo: {first_match['matched_filename']}\n📍 Face location: {first_match['matched_location']}")
matched_info_label.config(text="\n".join(person_info_lines))
# Display matched person face
matched_canvas.delete("all")
@ -3244,30 +3285,43 @@ class PhotoTagger:
cursor = conn.cursor()
cursor.execute(
"""
SELECT p.id, p.first_name, p.last_name, COUNT(f.id) as face_count
SELECT p.id, p.first_name, p.last_name, p.middle_name, p.maiden_name, p.date_of_birth, COUNT(f.id) as face_count
FROM people p
JOIN faces f ON f.person_id = p.id
GROUP BY p.id, p.last_name, p.first_name
GROUP BY p.id, p.last_name, p.first_name, p.middle_name, p.maiden_name, p.date_of_birth
HAVING face_count > 0
ORDER BY p.last_name, p.first_name COLLATE NOCASE
"""
)
people_data = []
for (pid, first_name, last_name, count) in cursor.fetchall():
if last_name and first_name:
display_name = f"{last_name}, {first_name}"
elif last_name:
display_name = last_name
elif first_name:
display_name = first_name
else:
display_name = "Unknown"
for (pid, first_name, last_name, middle_name, maiden_name, date_of_birth, count) in cursor.fetchall():
# Create full name display with all available information
name_parts = []
if first_name:
name_parts.append(first_name)
if middle_name:
name_parts.append(middle_name)
if last_name:
name_parts.append(last_name)
if maiden_name:
name_parts.append(f"({maiden_name})")
full_name = ' '.join(name_parts) if name_parts else "Unknown"
# Create detailed display with date of birth if available
display_name = full_name
if date_of_birth:
display_name += f" - Born: {date_of_birth}"
people_data.append({
'id': pid,
'name': display_name,
'full_name': full_name,
'first_name': first_name or "",
'last_name': last_name or "",
'middle_name': middle_name or "",
'maiden_name': maiden_name or "",
'date_of_birth': date_of_birth or "",
'count': count
})
@ -3475,43 +3529,324 @@ class PhotoTagger:
# Use pre-loaded data instead of database query
cur_first = person_record.get('first_name', '')
cur_last = person_record.get('last_name', '')
cur_middle = person_record.get('middle_name', '')
cur_maiden = person_record.get('maiden_name', '')
cur_dob = person_record.get('date_of_birth', '')
# Create a container frame for the text boxes and labels
# Create a larger container frame for the text boxes and labels
edit_container = ttk.Frame(row_frame)
edit_container.pack(side=tk.LEFT)
edit_container.pack(side=tk.LEFT, fill=tk.X, expand=True)
# Text boxes row
text_frame = ttk.Frame(edit_container)
text_frame.pack(side=tk.TOP)
# Separate Last name and First name inputs
last_var = tk.StringVar(value=cur_last)
last_entry = ttk.Entry(text_frame, textvariable=last_var, width=12)
last_entry.pack(side=tk.LEFT)
# Create a grid layout for better organization
# First name field with label
first_frame = ttk.Frame(edit_container)
first_frame.grid(row=0, column=0, padx=(0, 10), pady=(0, 5), sticky=tk.W)
first_var = tk.StringVar(value=cur_first)
first_entry = ttk.Entry(text_frame, textvariable=first_var, width=12)
first_entry.pack(side=tk.LEFT, padx=(5, 0))
first_entry = ttk.Entry(first_frame, textvariable=first_var, width=15)
first_entry.pack(side=tk.TOP)
first_entry.focus_set()
# Help text labels row (below text boxes)
help_frame = ttk.Frame(edit_container)
help_frame.pack(side=tk.TOP, pady=(2, 0))
first_label = ttk.Label(first_frame, text="First Name", font=("Arial", 8), foreground="gray")
first_label.pack(side=tk.TOP, pady=(2, 0))
last_label = ttk.Label(help_frame, text="Last", font=("Arial", 8), foreground="gray")
last_label.pack(side=tk.LEFT)
# Last name field with label
last_frame = ttk.Frame(edit_container)
last_frame.grid(row=0, column=1, padx=(0, 10), pady=(0, 5), sticky=tk.W)
first_label = ttk.Label(help_frame, text="First", font=("Arial", 8), foreground="gray")
first_label.pack(side=tk.LEFT, padx=(5, 0))
last_var = tk.StringVar(value=cur_last)
last_entry = ttk.Entry(last_frame, textvariable=last_var, width=15)
last_entry.pack(side=tk.TOP)
last_label = ttk.Label(last_frame, text="Last Name", font=("Arial", 8), foreground="gray")
last_label.pack(side=tk.TOP, pady=(2, 0))
# Middle name field with label
middle_frame = ttk.Frame(edit_container)
middle_frame.grid(row=0, column=2, padx=(0, 10), pady=(0, 5), sticky=tk.W)
middle_var = tk.StringVar(value=cur_middle)
middle_entry = ttk.Entry(middle_frame, textvariable=middle_var, width=15)
middle_entry.pack(side=tk.TOP)
middle_label = ttk.Label(middle_frame, text="Middle Name", font=("Arial", 8), foreground="gray")
middle_label.pack(side=tk.TOP, pady=(2, 0))
# Maiden name field with label
maiden_frame = ttk.Frame(edit_container)
maiden_frame.grid(row=1, column=0, padx=(0, 10), pady=(0, 5), sticky=tk.W)
maiden_var = tk.StringVar(value=cur_maiden)
maiden_entry = ttk.Entry(maiden_frame, textvariable=maiden_var, width=15)
maiden_entry.pack(side=tk.TOP)
maiden_label = ttk.Label(maiden_frame, text="Maiden Name", font=("Arial", 8), foreground="gray")
maiden_label.pack(side=tk.TOP, pady=(2, 0))
# Date of birth field with label and calendar button
dob_frame = ttk.Frame(edit_container)
dob_frame.grid(row=1, column=1, columnspan=2, padx=(0, 10), pady=(0, 5), sticky=tk.W)
# Create a frame for the date picker
date_picker_frame = ttk.Frame(dob_frame)
date_picker_frame.pack(side=tk.TOP)
dob_var = tk.StringVar(value=cur_dob)
dob_entry = ttk.Entry(date_picker_frame, textvariable=dob_var, width=20, state='readonly')
dob_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
# Calendar button
calendar_btn = ttk.Button(date_picker_frame, text="📅", width=3, command=lambda: open_calendar())
calendar_btn.pack(side=tk.RIGHT, padx=(5, 0))
dob_label = ttk.Label(dob_frame, text="Date of Birth", font=("Arial", 8), foreground="gray")
dob_label.pack(side=tk.TOP, pady=(2, 0))
def open_calendar():
"""Open a visual calendar dialog to select date of birth"""
from datetime import datetime, date, timedelta
import calendar
# Create calendar window
calendar_window = tk.Toplevel(root)
calendar_window.title("Select Date of Birth")
calendar_window.resizable(False, False)
calendar_window.transient(root)
calendar_window.grab_set()
# Calculate center position before showing the window
window_width = 350
window_height = 400
screen_width = calendar_window.winfo_screenwidth()
screen_height = calendar_window.winfo_screenheight()
x = (screen_width // 2) - (window_width // 2)
y = (screen_height // 2) - (window_height // 2)
# Set geometry with center position before showing
calendar_window.geometry(f"{window_width}x{window_height}+{x}+{y}")
# Calendar variables
current_date = datetime.now()
# Check if there's already a date selected
existing_date_str = dob_var.get().strip()
if existing_date_str:
try:
existing_date = datetime.strptime(existing_date_str, '%Y-%m-%d').date()
display_year = existing_date.year
display_month = existing_date.month
selected_date = existing_date
except ValueError:
# If existing date is invalid, use default
display_year = current_date.year - 25
display_month = 1
selected_date = None
else:
# Default to 25 years ago
display_year = current_date.year - 25
display_month = 1
selected_date = None
# Month names
month_names = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"]
# Configure custom styles for better visual highlighting
style = ttk.Style()
# Selected date style - bright blue background with white text
style.configure("Selected.TButton",
background="#0078d4",
foreground="white",
font=("Arial", 9, "bold"),
relief="raised",
borderwidth=2)
style.map("Selected.TButton",
background=[("active", "#106ebe")],
relief=[("pressed", "sunken")])
# Today's date style - orange background
style.configure("Today.TButton",
background="#ff8c00",
foreground="white",
font=("Arial", 9, "bold"),
relief="raised",
borderwidth=1)
style.map("Today.TButton",
background=[("active", "#e67e00")],
relief=[("pressed", "sunken")])
# Calendar-specific normal button style (don't affect global TButton)
style.configure("Calendar.TButton",
font=("Arial", 9),
relief="flat")
style.map("Calendar.TButton",
background=[("active", "#e1e1e1")],
relief=[("pressed", "sunken")])
# Main frame
main_cal_frame = ttk.Frame(calendar_window, padding="10")
main_cal_frame.pack(fill=tk.BOTH, expand=True)
# Header frame with navigation
header_frame = ttk.Frame(main_cal_frame)
header_frame.pack(fill=tk.X, pady=(0, 10))
# Month/Year display and navigation
nav_frame = ttk.Frame(header_frame)
nav_frame.pack()
def update_calendar():
"""Update the calendar display"""
# Clear existing calendar
for widget in calendar_frame.winfo_children():
widget.destroy()
# Update header
month_year_label.config(text=f"{month_names[display_month-1]} {display_year}")
# Get calendar data
cal = calendar.monthcalendar(display_year, display_month)
# Day headers
day_headers = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
for i, day in enumerate(day_headers):
label = ttk.Label(calendar_frame, text=day, font=("Arial", 9, "bold"))
label.grid(row=0, column=i, padx=1, pady=1, sticky="nsew")
# Calendar days
for week_num, week in enumerate(cal):
for day_num, day in enumerate(week):
if day == 0:
# Empty cell
label = ttk.Label(calendar_frame, text="")
label.grid(row=week_num+1, column=day_num, padx=1, pady=1, sticky="nsew")
else:
# Day button
def make_day_handler(day_value):
def select_day():
nonlocal selected_date
selected_date = date(display_year, display_month, day_value)
# Reset all buttons to normal calendar style
for widget in calendar_frame.winfo_children():
if isinstance(widget, ttk.Button):
widget.config(style="Calendar.TButton")
# Highlight selected day with prominent style
for widget in calendar_frame.winfo_children():
if isinstance(widget, ttk.Button) and widget.cget("text") == str(day_value):
widget.config(style="Selected.TButton")
return select_day
day_btn = ttk.Button(calendar_frame, text=str(day),
command=make_day_handler(day),
width=3, style="Calendar.TButton")
day_btn.grid(row=week_num+1, column=day_num, padx=1, pady=1, sticky="nsew")
# Check if this day should be highlighted
is_today = (display_year == current_date.year and
display_month == current_date.month and
day == current_date.day)
is_selected = (selected_date and
selected_date.year == display_year and
selected_date.month == display_month and
selected_date.day == day)
if is_selected:
day_btn.config(style="Selected.TButton")
elif is_today:
day_btn.config(style="Today.TButton")
# Navigation functions
def prev_year():
nonlocal display_year
display_year = max(1900, display_year - 1)
update_calendar()
def next_year():
nonlocal display_year
display_year = min(current_date.year, display_year + 1)
update_calendar()
def prev_month():
nonlocal display_month, display_year
if display_month > 1:
display_month -= 1
else:
display_month = 12
display_year = max(1900, display_year - 1)
update_calendar()
def next_month():
nonlocal display_month, display_year
if display_month < 12:
display_month += 1
else:
display_month = 1
display_year = min(current_date.year, display_year + 1)
update_calendar()
# Navigation buttons
prev_year_btn = ttk.Button(nav_frame, text="<<", width=3, command=prev_year)
prev_year_btn.pack(side=tk.LEFT, padx=(0, 2))
prev_month_btn = ttk.Button(nav_frame, text="<", width=3, command=prev_month)
prev_month_btn.pack(side=tk.LEFT, padx=(0, 5))
month_year_label = ttk.Label(nav_frame, text="", font=("Arial", 12, "bold"))
month_year_label.pack(side=tk.LEFT, padx=5)
next_month_btn = ttk.Button(nav_frame, text=">", width=3, command=next_month)
next_month_btn.pack(side=tk.LEFT, padx=(5, 2))
next_year_btn = ttk.Button(nav_frame, text=">>", width=3, command=next_year)
next_year_btn.pack(side=tk.LEFT)
# Calendar grid frame
calendar_frame = ttk.Frame(main_cal_frame)
calendar_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# Configure grid weights
for i in range(7):
calendar_frame.columnconfigure(i, weight=1)
for i in range(7):
calendar_frame.rowconfigure(i, weight=1)
# Buttons frame
buttons_frame = ttk.Frame(main_cal_frame)
buttons_frame.pack(fill=tk.X)
def select_date():
"""Select the date and close calendar"""
if selected_date:
date_str = selected_date.strftime('%Y-%m-%d')
dob_var.set(date_str)
calendar_window.destroy()
else:
messagebox.showwarning("No Date Selected", "Please select a date from the calendar.")
def cancel_selection():
"""Cancel date selection"""
calendar_window.destroy()
# Buttons
ttk.Button(buttons_frame, text="Select", command=select_date).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(buttons_frame, text="Cancel", command=cancel_selection).pack(side=tk.LEFT)
# Initialize calendar
update_calendar()
def save_rename():
new_first = first_var.get().strip()
new_last = last_var.get().strip()
new_middle = middle_var.get().strip()
new_maiden = maiden_var.get().strip()
new_dob = dob_var.get().strip()
if not new_first and not new_last:
messagebox.showwarning("Invalid name", "At least one of First or Last name must be provided.")
return
# Check for duplicates in local data first
# Check for duplicates in local data first (based on first and last name only)
for person in people_data:
if person['id'] != person_record['id'] and person['first_name'] == new_first and person['last_name'] == new_last:
display_name = f"{new_last}, {new_first}".strip(", ").strip()
@ -3521,13 +3856,37 @@ class PhotoTagger:
# Single database access - save to database
with self.get_db_connection() as conn:
cursor = conn.cursor()
cursor.execute('UPDATE people SET first_name = ?, last_name = ? WHERE id = ?', (new_first, new_last, person_record['id']))
cursor.execute('UPDATE people SET first_name = ?, last_name = ?, middle_name = ?, maiden_name = ?, date_of_birth = ? WHERE id = ?',
(new_first, new_last, new_middle, new_maiden, new_dob, person_record['id']))
conn.commit()
# Update local data structure
person_record['first_name'] = new_first
person_record['last_name'] = new_last
person_record['name'] = f"{new_last}, {new_first}".strip(", ").strip() if new_first or new_last else "Unknown"
person_record['middle_name'] = new_middle
person_record['maiden_name'] = new_maiden
person_record['date_of_birth'] = new_dob
# Recreate the full display name with all available information
name_parts = []
if new_first:
name_parts.append(new_first)
if new_middle:
name_parts.append(new_middle)
if new_last:
name_parts.append(new_last)
if new_maiden:
name_parts.append(f"({new_maiden})")
full_name = ' '.join(name_parts) if name_parts else "Unknown"
# Create detailed display with date of birth if available
display_name = full_name
if new_dob:
display_name += f" - Born: {new_dob}"
person_record['name'] = display_name
person_record['full_name'] = full_name
# Refresh list
current_selected_id = person_record['id']
@ -3556,16 +3915,64 @@ class PhotoTagger:
w.destroy()
rebuild_row(row_frame, person_record, row_index)
save_btn = ttk.Button(row_frame, text="💾 Save", command=save_rename)
save_btn = ttk.Button(row_frame, text="💾", width=3, command=save_rename)
save_btn.pack(side=tk.LEFT, padx=(5, 0))
cancel_btn = ttk.Button(row_frame, text=" Cancel", command=cancel_edit)
cancel_btn = ttk.Button(row_frame, text="", width=3, command=cancel_edit)
cancel_btn.pack(side=tk.LEFT, padx=(5, 0))
# Keyboard shortcuts
first_entry.bind('<Return>', lambda e: save_rename())
last_entry.bind('<Return>', lambda e: save_rename())
# Configure custom disabled button style for better visibility
style = ttk.Style()
style.configure("Disabled.TButton",
background="#d3d3d3", # Light gray background
foreground="#808080", # Dark gray text
relief="flat",
borderwidth=1)
def validate_save_button():
"""Enable/disable save button based on required fields"""
first_val = first_var.get().strip()
last_val = last_var.get().strip()
dob_val = dob_var.get().strip()
# Enable save button only if both name fields and date of birth are provided
has_first = bool(first_val)
has_last = bool(last_val)
has_dob = bool(dob_val)
if has_first and has_last and has_dob:
save_btn.config(state="normal")
# Reset to normal styling when enabled
save_btn.config(style="TButton")
else:
save_btn.config(state="disabled")
# Apply custom disabled styling for better visibility
save_btn.config(style="Disabled.TButton")
# Set up validation callbacks for all input fields
first_var.trace('w', lambda *args: validate_save_button())
last_var.trace('w', lambda *args: validate_save_button())
middle_var.trace('w', lambda *args: validate_save_button())
maiden_var.trace('w', lambda *args: validate_save_button())
dob_var.trace('w', lambda *args: validate_save_button())
# Initial validation
validate_save_button()
# Keyboard shortcuts (only work when save button is enabled)
def try_save():
if save_btn.cget('state') == 'normal':
save_rename()
first_entry.bind('<Return>', lambda e: try_save())
last_entry.bind('<Return>', lambda e: try_save())
middle_entry.bind('<Return>', lambda e: try_save())
maiden_entry.bind('<Return>', lambda e: try_save())
dob_entry.bind('<Return>', lambda e: try_save())
first_entry.bind('<Escape>', lambda e: cancel_edit())
last_entry.bind('<Escape>', lambda e: cancel_edit())
middle_entry.bind('<Escape>', lambda e: cancel_edit())
maiden_entry.bind('<Escape>', lambda e: cancel_edit())
dob_entry.bind('<Escape>', lambda e: cancel_edit())
def rebuild_row(row_frame, p, i):
# Edit button (on the left)