feat: Add date filters for face identification and enhance API for improved querying
This commit introduces new date filters for the face identification process, allowing users to filter faces based on the date taken and date processed. The API has been updated to support these new parameters, ensuring backward compatibility with legacy date filters. Additionally, the Identify component has been modified to incorporate these new filters in the user interface, enhancing the overall functionality and user experience. Documentation has been updated to reflect these changes.
This commit is contained in:
+14
-2
@@ -101,8 +101,12 @@ def get_unidentified_faces(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=200),
|
||||
min_quality: float = Query(0.0, ge=0.0, le=1.0),
|
||||
date_from: str | None = Query(None),
|
||||
date_to: str | None = Query(None),
|
||||
date_from: str | None = Query(None, description="Legacy: date from (filters by date_taken or date_added)"),
|
||||
date_to: str | None = Query(None, description="Legacy: date to (filters by date_taken or date_added)"),
|
||||
date_taken_from: str | None = Query(None, description="Date taken from (YYYY-MM-DD)"),
|
||||
date_taken_to: str | None = Query(None, description="Date taken to (YYYY-MM-DD)"),
|
||||
date_processed_from: str | None = Query(None, description="Date processed from (YYYY-MM-DD)"),
|
||||
date_processed_to: str | None = Query(None, description="Date processed to (YYYY-MM-DD)"),
|
||||
sort_by: str = Query("quality"),
|
||||
sort_dir: str = Query("desc"),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -112,6 +116,10 @@ def get_unidentified_faces(
|
||||
|
||||
df = _date.fromisoformat(date_from) if date_from else None
|
||||
dt = _date.fromisoformat(date_to) if date_to else None
|
||||
dtf = _date.fromisoformat(date_taken_from) if date_taken_from else None
|
||||
dtt = _date.fromisoformat(date_taken_to) if date_taken_to else None
|
||||
dpf = _date.fromisoformat(date_processed_from) if date_processed_from else None
|
||||
dpt = _date.fromisoformat(date_processed_to) if date_processed_to else None
|
||||
|
||||
faces, total = list_unidentified_faces(
|
||||
db,
|
||||
@@ -120,6 +128,10 @@ def get_unidentified_faces(
|
||||
min_quality=min_quality,
|
||||
date_from=df,
|
||||
date_to=dt,
|
||||
date_taken_from=dtf,
|
||||
date_taken_to=dtt,
|
||||
date_processed_from=dpf,
|
||||
date_processed_to=dpt,
|
||||
sort_by=sort_by,
|
||||
sort_dir=sort_dir,
|
||||
)
|
||||
|
||||
+21
-2
@@ -96,8 +96,27 @@ async def lifespan(app: FastAPI):
|
||||
db_path = database_url.replace("sqlite:///", "")
|
||||
db_file = Path(db_path)
|
||||
db_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
Base.metadata.create_all(bind=engine)
|
||||
print("✅ Database initialized")
|
||||
|
||||
# Only create tables if they don't already exist (safety check)
|
||||
from sqlalchemy import inspect
|
||||
inspector = inspect(engine)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
# Check if required application tables exist (not just alembic_version)
|
||||
required_tables = {"photos", "people", "faces", "tags", "phototaglinkage", "person_encodings"}
|
||||
missing_tables = required_tables - existing_tables
|
||||
|
||||
if missing_tables:
|
||||
# Some required tables are missing - create all tables
|
||||
# create_all() only creates missing tables, won't drop existing ones
|
||||
Base.metadata.create_all(bind=engine)
|
||||
if len(missing_tables) == len(required_tables):
|
||||
print("✅ Database initialized (first run - tables created)")
|
||||
else:
|
||||
print(f"✅ Database tables created (missing tables: {', '.join(missing_tables)})")
|
||||
else:
|
||||
# All required tables exist - don't recreate (prevents data loss)
|
||||
print(f"✅ Database already initialized ({len(existing_tables)} tables exist)")
|
||||
except Exception as exc:
|
||||
print(f"❌ Database initialization failed: {exc}")
|
||||
raise
|
||||
|
||||
@@ -1204,12 +1204,22 @@ def list_unidentified_faces(
|
||||
min_quality: float = 0.0,
|
||||
date_from: Optional[date] = None,
|
||||
date_to: Optional[date] = None,
|
||||
date_taken_from: Optional[date] = None,
|
||||
date_taken_to: Optional[date] = None,
|
||||
date_processed_from: Optional[date] = None,
|
||||
date_processed_to: Optional[date] = None,
|
||||
sort_by: str = "quality",
|
||||
sort_dir: str = "desc",
|
||||
) -> Tuple[List[Face], int]:
|
||||
"""Return paginated unidentified faces with filters.
|
||||
|
||||
Matches desktop behavior as closely as possible: filter by min quality and date_taken.
|
||||
Supports filtering by:
|
||||
- Min quality
|
||||
- Date taken (date_taken_from, date_taken_to)
|
||||
- Date processed (date_processed_from, date_processed_to) - uses photo.date_added
|
||||
|
||||
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))
|
||||
@@ -1218,7 +1228,20 @@ def list_unidentified_faces(
|
||||
if min_quality is not None:
|
||||
query = query.filter(Face.quality_score >= min_quality)
|
||||
|
||||
# Date range on photo.date_taken when available, else on date_added as fallback
|
||||
# Date taken filters (new separate filters)
|
||||
if date_taken_from is not None:
|
||||
query = query.filter(Photo.date_taken >= date_taken_from)
|
||||
if date_taken_to is not None:
|
||||
query = query.filter(Photo.date_taken <= date_taken_to)
|
||||
|
||||
# Date processed filters (uses photo.date_added)
|
||||
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:
|
||||
query = query.filter(func.date(Photo.date_added) <= date_processed_to)
|
||||
|
||||
# Legacy date filters (backward compatibility)
|
||||
# Filter by date_taken when available, else date_added as fallback
|
||||
if date_from is not None:
|
||||
query = query.filter(
|
||||
(Photo.date_taken.is_not(None) & (Photo.date_taken >= date_from))
|
||||
@@ -1419,7 +1442,7 @@ def _is_acceptable_pose_for_auto_match(pose_mode: str) -> bool:
|
||||
def find_similar_faces(
|
||||
db: Session,
|
||||
face_id: int,
|
||||
limit: int = 20,
|
||||
limit: int = 20000, # Very high default limit - effectively unlimited
|
||||
tolerance: float = 0.6, # DEFAULT_FACE_TOLERANCE from desktop
|
||||
filter_frontal_only: bool = False, # New: Only return frontal or tilted faces (not profile)
|
||||
) -> List[Tuple[Face, float, float]]: # Returns (face, distance, confidence_pct)
|
||||
@@ -1757,7 +1780,7 @@ def find_auto_match_matches(
|
||||
# reference_face_id, tolerance, include_same_photo=False, face_status=None)
|
||||
# This filters by: person_id is None (unidentified), confidence >= 40%, sorts by distance
|
||||
similar_faces = find_similar_faces(
|
||||
db, reference_face_id, limit=1000, tolerance=tolerance,
|
||||
db, reference_face_id, tolerance=tolerance,
|
||||
filter_frontal_only=filter_frontal_only
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user