feat: Add processed and unprocessed photo search options and sessionStorage management
This commit introduces new search options for processed and unprocessed photos in the Search component, enhancing the photo management capabilities. The Identify component has been updated to clear sessionStorage settings on logout and authentication failure, improving user experience by ensuring a clean state. Additionally, the API has been modified to support these new search parameters, ensuring seamless integration with the frontend. Documentation has been updated to reflect these changes.
This commit is contained in:
@@ -134,6 +134,10 @@ def get_unidentified_faces(
|
||||
if tag_names:
|
||||
tag_names_list = [t.strip() for t in tag_names.split(',') if t.strip()]
|
||||
|
||||
# Convert single date_processed to date_processed_from and date_processed_to (exact date match)
|
||||
date_processed_from = dp
|
||||
date_processed_to = dp
|
||||
|
||||
faces, total = list_unidentified_faces(
|
||||
db,
|
||||
page=page,
|
||||
@@ -141,7 +145,8 @@ def get_unidentified_faces(
|
||||
min_quality=min_quality,
|
||||
date_taken_from=dtf,
|
||||
date_taken_to=dtt,
|
||||
date_processed=dp,
|
||||
date_processed_from=date_processed_from,
|
||||
date_processed_to=date_processed_to,
|
||||
sort_by=sort_by,
|
||||
sort_dir=sort_dir,
|
||||
tag_names=tag_names_list,
|
||||
|
||||
+49
-1
@@ -35,6 +35,8 @@ from src.web.services.search_service import (
|
||||
get_photo_tags,
|
||||
get_photos_without_faces,
|
||||
get_photos_without_tags,
|
||||
get_processed_photos,
|
||||
get_unprocessed_photos,
|
||||
search_photos_by_date,
|
||||
search_photos_by_name,
|
||||
search_photos_by_tags,
|
||||
@@ -46,7 +48,7 @@ router = APIRouter(prefix="/photos", tags=["photos"])
|
||||
|
||||
@router.get("", response_model=SearchPhotosResponse)
|
||||
def search_photos(
|
||||
search_type: str = Query("name", description="Search type: name, date, tags, no_faces, no_tags"),
|
||||
search_type: str = Query("name", description="Search type: name, date, tags, no_faces, no_tags, processed, unprocessed"),
|
||||
person_name: Optional[str] = Query(None, description="Person name for name search"),
|
||||
tag_names: Optional[str] = Query(None, description="Comma-separated tag names for tag search"),
|
||||
match_all: bool = Query(False, description="Match all tags (for tag search)"),
|
||||
@@ -65,6 +67,8 @@ def search_photos(
|
||||
- Search by tags: tag_names required (comma-separated)
|
||||
- Search no faces: returns photos without faces
|
||||
- Search no tags: returns photos without tags
|
||||
- Search processed: returns photos that have been processed for face detection
|
||||
- Search unprocessed: returns photos that have not been processed for face detection
|
||||
"""
|
||||
items: List[PhotoSearchResult] = []
|
||||
total = 0
|
||||
@@ -202,6 +206,50 @@ def search_photos(
|
||||
face_count=face_count,
|
||||
)
|
||||
)
|
||||
elif search_type == "processed":
|
||||
results, total = get_processed_photos(db, folder_path, page, page_size)
|
||||
for photo in results:
|
||||
tags = get_photo_tags(db, photo.id)
|
||||
face_count = get_photo_face_count(db, photo.id)
|
||||
person_name_val = get_photo_person(db, photo.id)
|
||||
# Convert datetime to date for date_added
|
||||
date_added = photo.date_added.date() if isinstance(photo.date_added, datetime) else photo.date_added
|
||||
items.append(
|
||||
PhotoSearchResult(
|
||||
id=photo.id,
|
||||
path=photo.path,
|
||||
filename=photo.filename,
|
||||
date_taken=photo.date_taken,
|
||||
date_added=date_added,
|
||||
processed=photo.processed,
|
||||
person_name=person_name_val,
|
||||
tags=tags,
|
||||
has_faces=face_count > 0,
|
||||
face_count=face_count,
|
||||
)
|
||||
)
|
||||
elif search_type == "unprocessed":
|
||||
results, total = get_unprocessed_photos(db, folder_path, page, page_size)
|
||||
for photo in results:
|
||||
tags = get_photo_tags(db, photo.id)
|
||||
face_count = get_photo_face_count(db, photo.id)
|
||||
person_name_val = get_photo_person(db, photo.id)
|
||||
# Convert datetime to date for date_added
|
||||
date_added = photo.date_added.date() if isinstance(photo.date_added, datetime) else photo.date_added
|
||||
items.append(
|
||||
PhotoSearchResult(
|
||||
id=photo.id,
|
||||
path=photo.path,
|
||||
filename=photo.filename,
|
||||
date_taken=photo.date_taken,
|
||||
date_added=date_added,
|
||||
processed=photo.processed,
|
||||
person_name=person_name_val,
|
||||
tags=tags,
|
||||
has_faces=face_count > 0,
|
||||
face_count=face_count,
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
|
||||
@@ -1200,7 +1200,6 @@ def list_unidentified_faces(
|
||||
date_to: Optional[date] = None,
|
||||
date_taken_from: Optional[date] = None,
|
||||
date_taken_to: Optional[date] = None,
|
||||
date_processed: Optional[date] = None,
|
||||
date_processed_from: Optional[date] = None,
|
||||
date_processed_to: Optional[date] = None,
|
||||
sort_by: str = "quality",
|
||||
@@ -1264,9 +1263,6 @@ def list_unidentified_faces(
|
||||
query = query.filter(Photo.date_taken <= date_taken_to)
|
||||
|
||||
# Date processed filters (uses photo.date_added)
|
||||
if date_processed is not None:
|
||||
# Filter by exact date processed
|
||||
query = query.filter(func.date(Photo.date_added) == date_processed)
|
||||
if date_processed_from is not None:
|
||||
query = query.filter(func.date(Photo.date_added) >= date_processed_from)
|
||||
if date_processed_to is not None:
|
||||
@@ -1732,8 +1728,7 @@ def find_auto_match_matches(
|
||||
|
||||
Args:
|
||||
tolerance: Similarity tolerance (default: 0.6)
|
||||
filter_frontal_only: Only include persons with frontal or tilted reference face (not profile).
|
||||
When True (auto-accept mode), also requires reference faces to have quality > 0.5
|
||||
filter_frontal_only: Only include persons with frontal or tilted reference face (not profile)
|
||||
|
||||
Returns:
|
||||
List of (person_id, reference_face_id, reference_face, matches) tuples
|
||||
@@ -1752,25 +1747,15 @@ def find_auto_match_matches(
|
||||
# JOIN photos p ON f.photo_id = p.id
|
||||
# WHERE f.person_id IS NOT NULL AND f.quality_score >= 0.3
|
||||
# ORDER BY f.person_id, f.quality_score DESC
|
||||
#
|
||||
# For auto-accept mode (filter_frontal_only=True), also require quality > 0.5
|
||||
quality_threshold = 0.3
|
||||
identified_faces: List[Face] = (
|
||||
db.query(Face)
|
||||
.join(Photo, Face.photo_id == Photo.id)
|
||||
.filter(Face.person_id.isnot(None))
|
||||
.filter(Face.quality_score >= quality_threshold)
|
||||
.filter(Face.quality_score >= 0.3)
|
||||
.order_by(Face.person_id, Face.quality_score.desc())
|
||||
.all()
|
||||
)
|
||||
|
||||
# For auto-accept mode, filter out reference faces with quality <= 0.5
|
||||
if filter_frontal_only:
|
||||
identified_faces = [
|
||||
f for f in identified_faces
|
||||
if f.quality_score is not None and float(f.quality_score) > 0.5
|
||||
]
|
||||
|
||||
if not identified_faces:
|
||||
return []
|
||||
|
||||
|
||||
@@ -264,3 +264,55 @@ def get_photo_face_count(db: Session, photo_id: int) -> int:
|
||||
"""Get face count for a photo."""
|
||||
return db.query(Face).filter(Face.photo_id == photo_id).count()
|
||||
|
||||
|
||||
def get_processed_photos(
|
||||
db: Session,
|
||||
folder_path: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Tuple[List[Photo], int]:
|
||||
"""Get photos that have been processed for face detection.
|
||||
|
||||
Matches desktop behavior exactly.
|
||||
"""
|
||||
query = db.query(Photo).filter(Photo.processed == True) # noqa: E712
|
||||
|
||||
# Apply folder filter if provided
|
||||
if folder_path:
|
||||
folder_path = folder_path.strip()
|
||||
query = query.filter(Photo.path.startswith(folder_path))
|
||||
|
||||
# Total count
|
||||
total = query.count()
|
||||
|
||||
# Pagination and sorting
|
||||
results = query.order_by(Photo.filename).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
return results, total
|
||||
|
||||
|
||||
def get_unprocessed_photos(
|
||||
db: Session,
|
||||
folder_path: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Tuple[List[Photo], int]:
|
||||
"""Get photos that have not been processed for face detection.
|
||||
|
||||
Matches desktop behavior exactly.
|
||||
"""
|
||||
query = db.query(Photo).filter(Photo.processed == False) # noqa: E712
|
||||
|
||||
# Apply folder filter if provided
|
||||
if folder_path:
|
||||
folder_path = folder_path.strip()
|
||||
query = query.filter(Photo.path.startswith(folder_path))
|
||||
|
||||
# Total count
|
||||
total = query.count()
|
||||
|
||||
# Pagination and sorting
|
||||
results = query.order_by(Photo.filename).offset((page - 1) * page_size).limit(page_size).all()
|
||||
|
||||
return results, total
|
||||
|
||||
|
||||
Reference in New Issue
Block a user