diff --git a/photo_tagger.py b/photo_tagger.py index 58b4aae..91790c4 100644 --- a/photo_tagger.py +++ b/photo_tagger.py @@ -295,6 +295,10 @@ class PhotoTagger: def main(): """Main CLI interface""" + # Suppress pkg_resources deprecation warning from face_recognition library + import warnings + warnings.filterwarnings("ignore", message="pkg_resources is deprecated", category=UserWarning) + parser = argparse.ArgumentParser( description="PunimTag CLI - Simple photo face tagger (Refactored)", formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/search_gui.py b/search_gui.py index e380c8d..45bbd82 100644 --- a/search_gui.py +++ b/search_gui.py @@ -26,7 +26,7 @@ class SearchGUI: "Most common tags (planned)", "Most photographed people (planned)", "Photos without faces", - "Photos without tags (planned)", + "Photos without tags", "Duplicate faces (planned)", "Face quality distribution (planned)", ] @@ -227,6 +227,21 @@ class SearchGUI: tree.heading("open_photo", text="") # Also hide the columns from display tree["displaycolumns"] = ("select", "tags", "open_dir", "path") + # Auto-run search for photos without faces + do_search() + elif choice == self.SEARCH_TYPES[7]: # Photos without tags + # No input needed for this search type + search_btn.configure(state="normal") + # Show person column since photos without tags might still have people + tree.column("person", width=180, minwidth=50, anchor="w") + tree.heading("person", text="Person", command=lambda: sort_treeview("person")) + # Show the people icon column since there might be faces/people + tree.column("open_photo", width=50, minwidth=50, anchor="center") + tree.heading("open_photo", text="👤") + # Show all columns since we want to display person info and tags (which will be empty) + tree["displaycolumns"] = ("select", "person", "tags", "open_dir", "open_photo", "path") + # Auto-run search for photos without tags + do_search() else: planned_label.pack(anchor="w") name_entry.configure(state="disabled") @@ -269,6 +284,12 @@ class SearchGUI: path, tag_info = row photo_tags = get_photo_tags_for_display(path) tree.insert("", tk.END, values=("☐", "", photo_tags, "📁", "", path)) + elif search_type_var.get() == self.SEARCH_TYPES[7]: # Photos without tags + # For photos without tags: (path, filename) - show all columns but tags will be empty + path, filename = row + person_name = get_person_name_for_photo(path) + photo_tags = get_photo_tags_for_display(path) # Will be "No tags" + tree.insert("", tk.END, values=("☐", person_name, photo_tags, "📁", "👤", path)) else: # For name search: (full_name, path) - show person column full_name, p = row @@ -284,6 +305,9 @@ class SearchGUI: elif search_type_var.get() == self.SEARCH_TYPES[6]: # Photos without faces # Sort by path column for photos without faces self.sort_column = "path" + elif search_type_var.get() == self.SEARCH_TYPES[7]: # Photos without tags + # Sort by person column for photos without tags + self.sort_column = "person" else: # Sort by person column for name search self.sort_column = "person" @@ -339,6 +363,15 @@ class SearchGUI: # For photos without faces, we don't have person info, so we use empty string formatted_rows = [(path, "") for path, filename in rows] add_results(formatted_rows) + elif choice == self.SEARCH_TYPES[7]: # Photos without tags + rows = self.search_stats.get_photos_without_tags() + if not rows: + messagebox.showinfo("Search", "No photos without tags found.", parent=root) + else: + # Convert to the format expected by add_results: (path, filename) + # For photos without tags, we have both path and filename + formatted_rows = [(path, filename) for path, filename in rows] + add_results(formatted_rows) def open_dir(path: str): try: @@ -899,6 +932,7 @@ class SearchGUI: # Determine column offsets based on search type is_tag_search = (search_type_var.get() == self.SEARCH_TYPES[2]) is_photos_without_faces = (search_type_var.get() == self.SEARCH_TYPES[6]) + is_photos_without_tags = (search_type_var.get() == self.SEARCH_TYPES[7]) if is_tag_search: # Tag search: person column is hidden, select is first select_col = "#1" # select is now column 1 @@ -913,6 +947,13 @@ class SearchGUI: face_col = "#4" # open_photo is now column 4 (but hidden) path_col = "#4" # path is now column 4 (since people icon is hidden) path_index = 5 # path is at index 5 in values array + elif is_photos_without_tags: + # Photos without tags: all columns visible, same as name search + select_col = "#1" # select is now column 1 + open_dir_col = "#4" # open_dir is column 4 + face_col = "#5" # open_photo is column 5 + path_col = "#6" # path is column 6 + path_index = 5 # path is at index 5 in values array else: # Name search: all columns visible, select is first select_col = "#1" # select is now column 1 @@ -977,6 +1018,7 @@ class SearchGUI: # Determine column offsets based on search type is_tag_search = (search_type_var.get() == self.SEARCH_TYPES[2]) is_photos_without_faces = (search_type_var.get() == self.SEARCH_TYPES[6]) + is_photos_without_tags = (search_type_var.get() == self.SEARCH_TYPES[7]) if is_tag_search: # Tag search: person column is hidden, select is first tags_col = "#2" # tags is now column 2 @@ -991,6 +1033,13 @@ class SearchGUI: face_col = "#4" # open_photo is now column 4 (but hidden) path_col = "#4" # path is now column 4 (since people icon is hidden) path_index = 5 # path is at index 5 in values array + elif is_photos_without_tags: + # Photos without tags: all columns visible, same as name search + tags_col = "#3" # tags is column 3 + open_dir_col = "#4" # open_dir is column 4 + face_col = "#5" # open_photo is column 5 + path_col = "#6" # path is column 6 + path_index = 5 # path is at index 5 in values array else: # Name search: all columns visible, select is first tags_col = "#3" # tags is column 3 diff --git a/search_stats.py b/search_stats.py index 4ecb863..847e4a8 100644 --- a/search_stats.py +++ b/search_stats.py @@ -303,10 +303,31 @@ class SearchStats: return results def get_photos_without_tags(self) -> List[Tuple]: - """Get photos that have no tags""" - # This would need to be implemented in the database module - # For now, return empty list - return [] + """Get photos that have no tags + + Returns: + List of tuples: (photo_path, filename) + """ + results = [] + try: + with self.db.get_db_connection() as conn: + cursor = conn.cursor() + # Find photos that have no tags associated with them + cursor.execute(''' + SELECT p.path, p.filename + FROM photos p + LEFT JOIN phototaglinkage ptl ON p.id = ptl.photo_id + WHERE ptl.photo_id IS NULL + ORDER BY p.filename + ''') + for row in cursor.fetchall(): + if row and row[0]: + results.append((row[0], row[1])) + except Exception as e: + if self.verbose > 0: + print(f"Error searching photos without tags: {e}") + + return results def get_duplicate_faces(self, tolerance: float = 0.6) -> List[Dict]: """Get potential duplicate faces (same person, different photos)"""