feat: Add tag filtering and developer mode options in Identify and AutoMatch components

This commit introduces new features for tag filtering in the Identify and AutoMatch components. Users can now filter unidentified faces by tags, with options to match all or any specified tags. The UI has been updated to include tag selection and management, enhancing user experience. Additionally, developer mode options have been added to display tolerance and auto-accept threshold settings conditionally. The API has been updated to support these new parameters, ensuring seamless integration. Documentation has been updated to reflect these changes.
This commit is contained in:
tanyar09
2025-11-11 12:17:23 -05:00
parent 21c138a339
commit 20a8e4df5d
6 changed files with 181 additions and 116 deletions
+9
View File
@@ -106,6 +106,8 @@ def get_unidentified_faces(
date_to: str | None = Query(None),
sort_by: str = Query("quality"),
sort_dir: str = Query("desc"),
tag_names: str | None = Query(None, description="Comma-separated tag names for filtering"),
match_all: bool = Query(False, description="Match all tags (for tag filtering)"),
db: Session = Depends(get_db),
) -> UnidentifiedFacesResponse:
"""Get unidentified faces with filters and pagination."""
@@ -114,6 +116,11 @@ def get_unidentified_faces(
df = _date.fromisoformat(date_from) if date_from else None
dt = _date.fromisoformat(date_to) if date_to else None
# Parse tag names
tag_names_list = None
if tag_names:
tag_names_list = [t.strip() for t in tag_names.split(',') if t.strip()]
faces, total = list_unidentified_faces(
db,
page=page,
@@ -123,6 +130,8 @@ def get_unidentified_faces(
date_to=dt,
sort_by=sort_by,
sort_dir=sort_dir,
tag_names=tag_names_list,
match_all=match_all,
)
items = [
+35 -1
View File
@@ -29,7 +29,7 @@ from src.core.config import (
)
from src.utils.exif_utils import EXIFOrientationHandler
from src.utils.pose_detection import PoseDetector, RETINAFACE_AVAILABLE
from src.web.db.models import Face, Photo, Person
from src.web.db.models import Face, Photo, Person, Tag, PhotoTagLinkage
def _pre_warm_deepface(
@@ -1204,6 +1204,8 @@ def list_unidentified_faces(
date_processed_to: Optional[date] = None,
sort_by: str = "quality",
sort_dir: str = "desc",
tag_names: Optional[List[str]] = None,
match_all: bool = False,
) -> Tuple[List[Face], int]:
"""Return paginated unidentified faces with filters.
@@ -1211,12 +1213,44 @@ def list_unidentified_faces(
- Min quality
- Date taken (date_taken_from, date_taken_to)
- Date processed (date_processed_from, date_processed_to) - uses photo.date_added
- Tags (tag_names, match_all)
Legacy parameters (date_from, date_to) are kept for backward compatibility
and filter by date_taken when available, else date_added as fallback.
"""
# Base query: faces with no person
query = db.query(Face).join(Photo, Face.photo_id == Photo.id).filter(Face.person_id.is_(None))
# Tag filtering
if tag_names:
# Find tag IDs (case-insensitive)
tag_ids = (
db.query(Tag.id)
.filter(func.lower(Tag.tag_name).in_([t.lower().strip() for t in tag_names]))
.all()
)
tag_ids = [tid[0] for tid in tag_ids]
if tag_ids:
if match_all:
# Photos that have ALL specified tags
# Join with PhotoTagLinkage and filter
query = (
query.join(PhotoTagLinkage, Photo.id == PhotoTagLinkage.photo_id)
.filter(PhotoTagLinkage.tag_id.in_(tag_ids))
.group_by(Face.id, Photo.id)
.having(func.count(func.distinct(PhotoTagLinkage.tag_id)) == len(tag_ids))
)
else:
# Photos that have ANY of the specified tags
query = (
query.join(PhotoTagLinkage, Photo.id == PhotoTagLinkage.photo_id)
.filter(PhotoTagLinkage.tag_id.in_(tag_ids))
.distinct()
)
else:
# No matching tags found - return empty result
return [], 0
# Min quality (stored 0.0-1.0)
if min_quality is not None: