From 5ecfe1121e3b87b7878de6c08a0f51ff6bf5f480 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Fri, 26 Sep 2025 13:10:30 -0400 Subject: [PATCH] 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. --- photo_tagger.py | 499 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 453 insertions(+), 46 deletions(-) diff --git a/photo_tagger.py b/photo_tagger.py index 09aec32..ff6c6e2 100644 --- a/photo_tagger.py +++ b/photo_tagger.py @@ -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('', lambda e: save_rename()) - last_entry.bind('', 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('', lambda e: try_save()) + last_entry.bind('', lambda e: try_save()) + middle_entry.bind('', lambda e: try_save()) + maiden_entry.bind('', lambda e: try_save()) + dob_entry.bind('', lambda e: try_save()) first_entry.bind('', lambda e: cancel_edit()) last_entry.bind('', lambda e: cancel_edit()) + middle_entry.bind('', lambda e: cancel_edit()) + maiden_entry.bind('', lambda e: cancel_edit()) + dob_entry.bind('', lambda e: cancel_edit()) def rebuild_row(row_frame, p, i): # Edit button (on the left)