Enhance Search GUI and SearchStats with photos without tags feature

This commit introduces functionality to search for and display photos that lack associated tags in the SearchGUI and SearchStats classes. The SearchGUI has been updated to manage the visibility of relevant columns based on the selected search type, ensuring a seamless user experience. Additionally, the SearchStats class now retrieves photos without tags from the database, improving the overall search capabilities of the application.
This commit is contained in:
tanyar09 2025-10-08 14:31:04 -04:00
parent 8a9834b056
commit 1c8856209a
3 changed files with 79 additions and 5 deletions

View File

@ -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,

View File

@ -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

View File

@ -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)"""