|
10 | 10 | db_update_cluster, |
11 | 11 | db_get_all_clusters_with_face_counts, |
12 | 12 | db_get_images_by_cluster_id, |
| 13 | + db_get_images_by_face_clusters, |
13 | 14 | ) |
14 | 15 | from app.utils.face_clusters import cluster_util_face_clusters_sync |
15 | 16 | from app.schemas.face_clusters import ( |
|
25 | 26 | GetClusterImagesResponse, |
26 | 27 | GetClusterImagesData, |
27 | 28 | ImageInCluster, |
| 29 | + MultiPersonSearchRequest, |
| 30 | + MultiPersonSearchResponse, |
| 31 | + MultiPersonSearchData, |
| 32 | + MultiPersonSearchImage, |
28 | 33 | ) |
29 | 34 | from app.schemas.images import FaceSearchRequest, InputType |
30 | 35 | from app.utils.faceSearch import perform_face_search |
@@ -347,3 +352,65 @@ def trigger_global_reclustering(): |
347 | 352 | message=f"Global reclustering failed: {str(e)}", |
348 | 353 | ).model_dump(), |
349 | 354 | ) |
| 355 | + |
| 356 | + |
| 357 | +@router.post( |
| 358 | + "/multi-search", |
| 359 | + response_model=MultiPersonSearchResponse, |
| 360 | + responses={code: {"model": ErrorResponse} for code in [400, 404, 500]}, |
| 361 | +) |
| 362 | +def search_images_by_multiple_faces(body: MultiPersonSearchRequest): |
| 363 | + """Search for images containing multiple face identities, ranked by match count.""" |
| 364 | + try: |
| 365 | + if not body.cluster_ids: |
| 366 | + raise HTTPException( |
| 367 | + status_code=status.HTTP_400_BAD_REQUEST, |
| 368 | + detail=ErrorResponse( |
| 369 | + success=False, |
| 370 | + error="Validation Error", |
| 371 | + message="cluster_ids cannot be empty.", |
| 372 | + ).model_dump(), |
| 373 | + ) |
| 374 | + if body.match_mode not in ("match_any", "match_all"): |
| 375 | + raise HTTPException( |
| 376 | + status_code=status.HTTP_400_BAD_REQUEST, |
| 377 | + detail=ErrorResponse( |
| 378 | + success=False, |
| 379 | + error="Validation Error", |
| 380 | + message="match_mode must be 'match_any' or 'match_all'.", |
| 381 | + ).model_dump(), |
| 382 | + ) |
| 383 | + |
| 384 | + rows = db_get_images_by_face_clusters(body.cluster_ids, body.match_mode) |
| 385 | + |
| 386 | + images = [ |
| 387 | + MultiPersonSearchImage( |
| 388 | + id=row["image_id"], |
| 389 | + path=row["image_path"], |
| 390 | + thumbnailPath=row["thumbnail_path"], |
| 391 | + metadata=row["metadata"], |
| 392 | + match_count=row["match_count"], |
| 393 | + ) |
| 394 | + for row in rows |
| 395 | + ] |
| 396 | + |
| 397 | + return MultiPersonSearchResponse( |
| 398 | + success=True, |
| 399 | + message=f"Found {len(images)} image(s) matching the selected people.", |
| 400 | + data=MultiPersonSearchData( |
| 401 | + images=images, |
| 402 | + total=len(images), |
| 403 | + match_mode=body.match_mode, |
| 404 | + ), |
| 405 | + ) |
| 406 | + except HTTPException: |
| 407 | + raise |
| 408 | + except Exception as e: |
| 409 | + raise HTTPException( |
| 410 | + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| 411 | + detail=ErrorResponse( |
| 412 | + success=False, |
| 413 | + error="Internal server error", |
| 414 | + message=f"Multi-person search failed: {str(e)}", |
| 415 | + ).model_dump(), |
| 416 | + ) |
0 commit comments