Skip to content

Conversation

@tushar1977
Copy link
Contributor

@tushar1977 tushar1977 commented Oct 16, 2025

Issue - #502

  • Added a backend API route to handle face searches using Base64-encoded image data.
  • Integrated frontend support for capturing images directly from the webcam and sending them to the backend for processing.

Video Ref :- https://drive.google.com/file/d/1hjGRv23Jhul-N_f9bcG4MfUmCyuWt3kT/view?usp=sharing

Summary by CodeRabbit

  • New Features

    • Face search accepts either file path or base64 image input; base64 searches (including webcam captures) supported.
    • Added a webcam capture component and a base64-based face-search API call.
  • User Experience

    • Face search dialog redesigned with "Upload Photo" and "Use Webcam", including capture review and retake.
    • Image rendering now correctly handles data URL images.
  • Security & Configuration

    • macOS camera entitlement and tightened content-security policy for camera/media access.

@github-actions
Copy link
Contributor

⚠️ No issue was linked in the PR description.
Please make sure to link an issue (e.g., 'Fixes #issue_number')

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

Walkthrough

Replaces single-input face-tagging with a dual-input POST /face-clusters/face-search (path or base64 via query input_type); adds perform_face_search util, temp-file handling for base64, frontend webcam capture and base64 API wiring, Tauri macOS entitlements/CSP updates, and new react-webcam dependency.

Changes

Cohort / File(s) Summary
Backend: route, request schemas & search util
backend/app/routes/face_clusters.py, backend/app/schemas/images.py, backend/app/utils/faceSearch.py
face_tagging signature changed to accept FaceSearchRequest with an input_type query (path
Docs: OpenAPI
docs/backend/backend_python/openapi.json
OpenAPI updated to add input_type query param (enum), replace AddSingleImageRequest with FaceSearchRequest (path/base64_data), and adjust face-search request/response schemas and image metadata allowances.
Frontend: API endpoints & functions
frontend/src/api/apiEndpoints.ts, frontend/src/api/api-functions/face_clusters.ts
searchForFaces endpoint constant updated to include ?input_type=path; new searchForFacesBase64 for ?input_type=base64. Added FetchSearchedFacesBase64Request and fetchSearchedFacesBase64() to POST base64 payloads.
Frontend: webcam UI & dialog
frontend/src/components/WebCam/WebCamComponent.tsx, frontend/src/components/Dialog/FaceSearchDialog.tsx, frontend/src/components/Navigation/Navbar/Navbar.tsx
New WebcamComponent for capture/review/search using base64; FaceSearchDialog updated to present Upload or Use Webcam flows and open webcam; Navbar adjusted to use data URLs directly for image src when appropriate.
Tauri / macOS config & deps
frontend/src-tauri/tauri.conf.json, frontend/src-tauri/Entitlements.plist, frontend/src-tauri/Info.plist, frontend/package.json
Added Entitlements.plist reference in tauri config, new Entitlements.plist (app sandbox) and Info.plist (NSCameraUsageDescription), updated CSP to allow media/blob/connect sources, and added react-webcam dependency.
Misc
package.json
Trailing newline added (no functional change).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Frontend UI (Dialog / Webcam)
    participant FE as Frontend API
    participant BE as Backend / face_tagging
    participant Search as perform_face_search

    rect rgb(220,235,255)
    Note over User,UI: Upload / Path flow (input_type=path)
    User->>UI: Select file (path)
    UI->>FE: fetchSearchedFaces(filePath)  -- ?input_type=path
    FE->>BE: POST /face-clusters/face-search?input_type=path (body: {path})
    BE->>Search: perform_face_search(image_path)
    Search-->>BE: GetAllImagesResponse
    BE-->>FE: Results
    FE-->>UI: Render matches
    end

    rect rgb(235,255,235)
    Note over User,UI: Webcam / Base64 flow (input_type=base64)
    User->>UI: Capture photo → base64
    UI->>FE: fetchSearchedFacesBase64({base64_data})  -- ?input_type=base64
    FE->>BE: POST /face-clusters/face-search?input_type=base64 (body: {base64_data})
    BE->>BE: validate & decode base64 → write temp file
    BE->>Search: perform_face_search(temp_path)
    Search-->>BE: GetAllImagesResponse
    BE->>BE: cleanup temp file
    BE-->>FE: Results
    FE-->>UI: Render matches
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement, frontend

Suggested reviewers

  • rahulharpal1603

Poem

🐰 I hopped and blinked beside the screen,
A flash, a frame, a tiny glean.
I turned base64 into a file so neat,
Then chased the faces, quick and fleet.
🥕📸

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: WebCam feature" is directly related to a significant aspect of the changeset—the addition of webcam capture functionality on the frontend. However, the PR encompasses more than just the frontend feature; it also includes substantial backend refactoring to support base64-encoded image inputs for face searches, new schemas (FaceSearchRequest and InputType), and a new face search utility module. The title captures the user-facing webcam feature but omits the backend infrastructure changes that enable base64 input handling, making it a partial rather than comprehensive summary of the work.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

⚠️ No issue was linked in the PR description.
Please make sure to link an issue (e.g., 'Fixes #issue_number')

@github-actions github-actions bot added UI backend bug Something isn't working labels Oct 16, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (6)
frontend/src-tauri/src/main.rs (2)

17-23: Consider removing or documenting the debug print statement.

The setup closure resolves the backend resource path but only prints it without further use. If this is temporary debugging code, consider removing it. If it's intentional for logging during startup, consider using proper logging instead of println!.


4-4: Remove unused services module
The services module only defines get_server_path but isn’t invoked by Tauri or referenced anywhere else. Remove mod services; in main.rs, pub mod services; in lib.rs, and delete frontend/src-tauri/src/services/.

backend/app/routes/face_clusters.py (1)

264-274: Add input validation for base64 data.

The endpoint should validate:

  1. The base64 data can be successfully decoded
  2. The decoded data represents a valid image format
  3. The data size is within acceptable limits to prevent DoS attacks via extremely large payloads

Consider wrapping the base64 decode and file operations in a try-except block to return a proper error response instead of letting exceptions propagate.

+    MAX_IMAGE_SIZE = 10 * 1024 * 1024  # 10MB limit
+
     image_path = None
     try:
+        # Validate and decode base64
+        try:
+            image_bytes = base64.b64decode(base64_data.split(",")[-1])
+        except Exception:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ErrorResponse(
+                    success=False,
+                    error="Invalid base64 data",
+                    message="The provided base64 data could not be decoded.",
+                ).model_dump(),
+            )
+        
+        # Validate size
+        if len(image_bytes) > MAX_IMAGE_SIZE:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ErrorResponse(
+                    success=False,
+                    error="Image too large",
+                    message=f"Image size exceeds maximum allowed size of {MAX_IMAGE_SIZE} bytes.",
+                ).model_dump(),
+            )
+        
-        image_bytes = base64.b64decode(base64_data.split(",")[-1])
         image_id = str(uuid.uuid4())[:8]
docs/backend/backend_python/openapi.json (1)

2058-2058: Confirm cluster metadata flexibility is intentional

OpenAPI’s additionalProperties: true for ImageInCluster.metadata matches Python’s Optional[Dict[str, Any]] and its dynamic use in face_clusters, YOLO, ONNX, etc. If you require stricter contracts, define or reuse a dedicated metadata schema (e.g. MetadataModel) and document expected keys.

backend/app/utils/faceSearch.py (2)

45-46: Consider reusing detector instances to avoid initialization overhead.

Creating new FaceDetector and FaceNet instances on every search call incurs significant overhead—loading YOLO and FaceNet models from disk, allocating ONNX sessions, etc. For a webcam-based search feature where users may perform multiple consecutive searches, this becomes a performance bottleneck.

Refactor to use shared instances or dependency injection:

# Option 1: Module-level singleton (if thread-safe)
_detector_instance = None
_facenet_instance = None

def get_detector_instances():
    global _detector_instance, _facenet_instance
    if _detector_instance is None:
        _detector_instance = FaceDetector()
    if _facenet_instance is None:
        _facenet_instance = FaceNet(DEFAULT_FACENET_MODEL)
    return _detector_instance, _facenet_instance

def perform_face_search(image_path: str) -> GetAllImagesResponse:
    fd, fn = get_detector_instances()
    # Remove the finally block that closes instances
    ...
# Option 2: Pass instances as parameters (better for testing)
def perform_face_search(
    image_path: str,
    fd: FaceDetector,
    fn: FaceNet
) -> GetAllImagesResponse:
    # No initialization or cleanup needed
    ...

If you choose Option 2, update the calling route to manage the lifecycle of these instances (e.g., create once per worker process or use FastAPI dependencies).


50-50: Remove unused UUID generation.

The image_id variable (line 50) is generated but only used for logging within detect_faces. Since this is a search operation and the ID isn't persisted or returned, generating a UUID adds unnecessary overhead.

Consider removing it or passing a constant placeholder:

-        image_id = str(uuid.uuid4())
-        result = fd.detect_faces(image_id, image_path, forSearch=True)
+        result = fd.detect_faces("search_temp", image_path, forSearch=True)

Or if logging requires unique IDs, document why it's necessary.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 589d71f and fd18420.

⛔ Files ignored due to path filters (2)
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • frontend/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • backend/app/routes/face_clusters.py (3 hunks)
  • backend/app/schemas/images.py (1 hunks)
  • backend/app/utils/faceSearch.py (1 hunks)
  • docs/backend/backend_python/openapi.json (3 hunks)
  • frontend/package.json (1 hunks)
  • frontend/src-tauri/src/main.rs (1 hunks)
  • frontend/src-tauri/tauri.conf.json (1 hunks)
  • frontend/src/api/api-functions/face_clusters.ts (2 hunks)
  • frontend/src/api/apiEndpoints.ts (1 hunks)
  • frontend/src/components/Dialog/FaceSearchDialog.tsx (2 hunks)
  • frontend/src/components/WebCam/WebCamComponent.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
backend/app/routes/face_clusters.py (3)
backend/app/schemas/images.py (3)
  • AddSingleBase64ImageRequest (10-11)
  • AddSingleImageRequest (6-7)
  • ErrorResponse (57-60)
backend/app/utils/faceSearch.py (1)
  • perform_face_search (35-95)
backend/app/schemas/face_clusters.py (1)
  • ErrorResponse (23-26)
frontend/src/api/api-functions/face_clusters.ts (3)
frontend/src/types/API.ts (1)
  • APIResponse (1-8)
frontend/src/api/axiosConfig.ts (1)
  • apiClient (5-12)
frontend/src/api/apiEndpoints.ts (1)
  • faceClustersEndpoints (5-11)
backend/app/utils/faceSearch.py (4)
backend/app/models/FaceDetector.py (2)
  • FaceDetector (11-75)
  • detect_faces (22-63)
backend/app/models/FaceNet.py (2)
  • FaceNet (8-25)
  • get_embedding (16-21)
backend/app/utils/FaceNet.py (1)
  • FaceNet_util_cosine_similarity (20-23)
backend/app/routes/face_clusters.py (3)
  • BoundingBox (30-34)
  • ImageData (37-45)
  • GetAllImagesResponse (48-51)
frontend/src/components/WebCam/WebCamComponent.tsx (6)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/face_clusters.ts (1)
  • fetchSearchedFacesBase64 (58-67)
frontend/src/types/Media.ts (1)
  • Image (13-22)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/searchSlice.ts (3)
  • setResults (24-26)
  • clearSearch (27-31)
  • startSearch (20-23)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/components/Dialog/FaceSearchDialog.tsx (4)
frontend/src/constants/routes.ts (1)
  • ROUTES (1-11)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/features/searchSlice.ts (1)
  • startSearch (20-23)
frontend/src/features/loaderSlice.ts (1)
  • showLoader (17-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Backend Tests
  • GitHub Check: Tauri Build Check (ubuntu-22.04)
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
  • GitHub Check: Tauri Build Check (windows-latest)
🔇 Additional comments (11)
frontend/package.json (1)

64-64: LGTM! Dependency addition aligns with webcam feature.

The react-webcam dependency at version ^7.2.0 is correctly added to support the new webcam capture functionality introduced in this PR.

backend/app/schemas/images.py (1)

10-12: LGTM! Schema follows established patterns.

The new AddSingleBase64ImageRequest model correctly mirrors the structure of AddSingleImageRequest and appropriately supports the base64 image search endpoint.

frontend/src/api/apiEndpoints.ts (1)

8-8: LGTM! Endpoint mapping is correct.

The new endpoint path correctly aligns with the backend route /face-clusters/face-search-base64.

docs/backend/backend_python/openapi.json (2)

1097-1135: LGTM! New endpoint definition is complete and correct.

The /face-clusters/face-search-base64 endpoint definition properly specifies the request schema, response codes, and follows the existing API patterns.


1330-1342: LGTM! Schema definition matches backend model.

The AddSingleBase64ImageRequest schema correctly defines the required base64_data field and aligns with the backend Pydantic model.

frontend/src/api/api-functions/face_clusters.ts (1)

18-20: LGTM! Interface correctly defines the base64 request structure.

The FetchSearchedFacesBase64Request interface appropriately mirrors FetchSearchedFacesRequest with the base64-specific field.

frontend/src/components/WebCam/WebCamComponent.tsx (1)

29-166: LGTM! Webcam component implementation is well-structured.

The component correctly implements the capture-review-search flow with appropriate state management, error handling via info dialogs, and proper cleanup of state on close. The integration with Redux slices and the mutation hook follows established patterns in the codebase.

backend/app/routes/face_clusters.py (1)

247-247: LGTM! Refactored to use centralized search logic.

The refactoring to delegate to perform_face_search improves code maintainability by centralizing the face detection and search logic.

frontend/src/components/Dialog/FaceSearchDialog.tsx (1)

92-153: LGTM! Dialog redesign enhances user experience.

The refactored dialog structure with dual action buttons (Upload Photo and Use Webcam) provides clear user choices. The webcam permission handling with appropriate error feedback and the integration of WebcamComponent are well implemented.

backend/app/utils/faceSearch.py (2)

60-61: Clarify behavior: only the first detected face is processed.

The code only processes result["processed_faces"][0], discarding all other detected faces in the image. This may be intentional for the webcam use case (search for one person at a time), but it's not documented and could be surprising behavior.

If this is intentional, add a comment explaining why:

-        process_face = result["processed_faces"][0]
-        new_embedding = fn.get_embedding(process_face)
+        # For webcam search, we only match the first detected face
+        # Users should ensure only one face is visible in the frame
+        process_face = result["processed_faces"][0]
+        new_embedding = fn.get_embedding(process_face)

If multiple faces should be supported, consider processing all faces and returning the union of matches, or providing an option to specify which face to search for.


71-85: Remove this review comment. The concern is incorrect.

The code at faceSearch.py:72 is correct. image["embeddings"] is a JSON-parsed list (returned from get_all_face_embeddings at line 180 of faces.py), and FaceNet_util_cosine_similarity handles lists without issue—numpy operations automatically accept list inputs.

The real underlying issue is in get_all_face_embeddings (line 171), which only stores the first face per image (if image_id not in images_dict:), causing silent data loss when multiple faces exist in the same image. However, this is a separate concern from the faceSearch.py code structure.

Likely an incorrect or invalid review comment.

Comment on lines +11 to +32
class BoundingBox(BaseModel):
x: float
y: float
width: float
height: float


class ImageData(BaseModel):
id: str
path: str
folder_id: str
thumbnailPath: str
metadata: Dict[str, Any]
isTagged: bool
tags: Optional[List[str]] = None
bboxes: BoundingBox


class GetAllImagesResponse(BaseModel):
success: bool
message: str
data: List[ImageData]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Eliminate duplicate model definitions.

The BoundingBox, ImageData, and GetAllImagesResponse models are already defined identically in backend/app/routes/face_clusters.py (lines 29-50). This code duplication violates DRY principles and creates a maintenance burden—future changes must be synchronized across both locations.

Move these models to a shared location (e.g., backend/app/schemas/face_search.py or backend/app/models/schemas.py) and import them in both files:

-from typing import Optional, List, Dict, Any
-from pydantic import BaseModel
+from app.schemas.face_search import BoundingBox, ImageData, GetAllImagesResponse
 from app.config.settings import CONFIDENCE_PERCENT, DEFAULT_FACENET_MODEL
 from app.database.faces import get_all_face_embeddings
 from app.models.FaceDetector import FaceDetector
 from app.models.FaceNet import FaceNet
 from app.utils.FaceNet import FaceNet_util_cosine_similarity
-
-
-class BoundingBox(BaseModel):
-    x: float
-    y: float
-    width: float
-    height: float
-
-
-class ImageData(BaseModel):
-    id: str
-    path: str
-    folder_id: str
-    thumbnailPath: str
-    metadata: Dict[str, Any]
-    isTagged: bool
-    tags: Optional[List[str]] = None
-    bboxes: BoundingBox
-
-
-class GetAllImagesResponse(BaseModel):
-    success: bool
-    message: str
-    data: List[ImageData]

Then update backend/app/routes/face_clusters.py to import from the same shared location.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 48 to 58
try:
matches = []
image_id = str(uuid.uuid4())

result = fd.detect_faces(image_id, image_path, forSearch=True)
if not result or result["num_faces"] == 0:
return GetAllImagesResponse(
success=True,
message="No faces detected in the image.",
data=[],
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add error handling for image loading and detection failures.

The function does not handle potential failures such as:

  • Invalid or corrupted image files
  • Model inference errors
  • File system access issues

Without explicit error handling, exceptions will propagate to the caller with potentially unclear error messages.

Wrap the detection logic in a try-except block:

     try:
         matches = []
         image_id = str(uuid.uuid4())
 
-        result = fd.detect_faces(image_id, image_path, forSearch=True)
+        try:
+            result = fd.detect_faces(image_id, image_path, forSearch=True)
+        except Exception as e:
+            return GetAllImagesResponse(
+                success=False,
+                message=f"Failed to process image: {str(e)}",
+                data=[],
+            )
+
         if not result or result["num_faces"] == 0:
             return GetAllImagesResponse(
                 success=True,
                 message="No faces detected in the image.",
                 data=[],
             )

Also consider validating that image_path exists before passing it to detect_faces:

import os

if not os.path.exists(image_path):
    return GetAllImagesResponse(
        success=False,
        message="Image file not found.",
        data=[],
    )
🤖 Prompt for AI Agents
In backend/app/utils/faceSearch.py around lines 48 to 58, the detection block
lacks validation and error handling for missing/corrupt files and runtime
errors; first check os.path.exists(image_path) and return a GetAllImagesResponse
with success=False and a clear "Image file not found." message if missing, then
wrap the call to fd.detect_faces(...) and subsequent processing in a try/except
that catches broad exceptions (or more specific ones if known), logs the
exception detail, and returns a GetAllImagesResponse with success=False, a
descriptive error message and empty data so callers receive a clear failure
response instead of an unhandled exception.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
frontend/src/components/Dialog/FaceSearchDialog.tsx (3)

62-79: Consider enhancing error handling.

The previous resource leak and UX flow issues have been correctly addressed. However, consider these improvements:

  1. Error logging: The caught error (line 69) is not logged, which could make debugging permission issues harder.
  2. Error guidance: The error message doesn't guide users on how to enable camera permissions if they were previously denied.

Apply this diff to add error logging:

   } catch (error) {
+    console.error('Webcam access error:', error);
     dispatch(
       showInfoDialog({
         title: 'Webcam Not Supported',

95-95: Remove redundant onClick handler.

The DialogTrigger component automatically handles opening the dialog when clicked, so the explicit onClick={() => setIsDialogOpen(true)} is redundant and can be removed.

Apply this diff:

 <Button
-  onClick={() => setIsDialogOpen(true)}
   variant="ghost"

116-116: Remove redundant disabled={false} props.

The disabled={false} props on lines 116 and 129 are redundant since buttons are enabled by default.

Apply this diff:

 <Button
   onClick={handlePickFile}
-  disabled={false}
   className="flex h-32 flex-col items-center justify-center gap-2 p-0"
 <Button
   onClick={handleWebCam}
-  disabled={false}
   className="flex h-32 flex-col items-center justify-center gap-2 p-0"

Also applies to: 129-129

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aed41b4 and 8967c03.

📒 Files selected for processing (2)
  • backend/app/utils/faceSearch.py (1 hunks)
  • frontend/src/components/Dialog/FaceSearchDialog.tsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/utils/faceSearch.py
🔇 Additional comments (3)
frontend/src/components/Dialog/FaceSearchDialog.tsx (3)

22-22: LGTM! Clean integration of webcam functionality.

The WebcamComponent import and showCamera state are properly integrated to support the new webcam capture feature.

Also applies to: 26-26


80-89: LGTM! Proper dialog close timing.

Closing the dialog after file selection (line 84) ensures the dialog remains open if the user cancels the file picker, which is the correct UX flow.


148-151: Consider conditional rendering for WebcamComponent.

The WebcamComponent is always rendered regardless of the showCamera state. If this component has expensive initialization (e.g., loading camera libraries, setting up media streams), consider conditional rendering for better performance.

Run the following script to check if WebcamComponent has expensive initialization:

If the component does perform expensive operations on mount, apply this diff:

-<WebcamComponent
-  isOpen={showCamera}
-  onClose={() => setShowCamera(false)}
-/>
+{showCamera && (
+  <WebcamComponent
+    isOpen={showCamera}
+    onClose={() => setShowCamera(false)}
+  />
+)}

@tushar1977 tushar1977 changed the title feat: Implemented WebCam feat: WebCam feature Oct 16, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
frontend/src/components/WebCam/WebCamComponent.tsx (4)

35-66: Simplify by removing redundant handleClose() call.

The handleClose() is called in two places: line 82 (before search starts) and line 41 (after search completes). Since the dialog closes immediately when search begins (line 82), the second call in onSuccess is redundant.

Apply this diff:

    onSuccess: (data) => {
      const result = data?.data as Image[];
      dispatch(hideLoader());
-     handleClose();
      if (result && result.length > 0) {

81-96: Add error handling for camera permissions and initialization.

There's no error handling if the user denies camera access or if the camera fails to initialize. This could leave users confused with a black screen or no feedback.

Consider adding an error handler to the Webcam component and displaying appropriate feedback:

const [cameraError, setCameraError] = useState<string | null>(null);

const handleUserMediaError = (error: string | DOMException) => {
  console.error('Camera error:', error);
  const message = typeof error === 'string' 
    ? error 
    : error.name === 'NotAllowedError'
    ? 'Camera access was denied. Please allow camera access and try again.'
    : 'Failed to access camera. Please check your device settings.';
  
  dispatch(
    showInfoDialog({
      title: 'Camera Error',
      message,
      variant: 'error',
    }),
  );
  setCameraError(message);
};

Then pass it to the Webcam component:

<Webcam
  audio={false}
  height={500}
  ref={webcamRef}
  screenshotFormat="image/jpeg"
  width={500}
  videoConstraints={videoConstraints}
  className="rounded-lg border"
  onUserMediaError={handleUserMediaError}
/>

119-167: Simplify conditional rendering logic.

The condition showCamera && !capturedImageUrl on line 119 includes a redundant check. When capturedImageUrl is set (line 71), showCamera is immediately set to false (line 72), so !capturedImageUrl is always true when showCamera is true.

Apply this diff to simplify:

- {showCamera && !capturedImageUrl ? (
+ {showCamera ? (
    <div className="flex flex-col items-center gap-4">

Alternatively, you could simplify the entire conditional structure by just using capturedImageUrl as the single source of truth:

{!capturedImageUrl ? (
  // Show camera
  <div className="flex flex-col items-center gap-4">
    <Webcam ... />
    <Button onClick={capture}>Capture Photo</Button>
  </div>
) : (
  // Show captured image
  <div className="flex flex-col items-center gap-4">
    <img src={capturedImageUrl} ... />
    <div className="flex gap-3">...</div>
  </div>
)}

This eliminates the need for the showCamera state entirely.


121-129: Add accessibility labels for screen readers.

The webcam video stream and captured image lack proper ARIA labels, which could make the component less accessible to users with screen readers.

Apply these diffs:

  <Webcam
    audio={false}
    ref={webcamRef}
    screenshotFormat="image/jpeg"
    videoConstraints={videoConstraints}
    className="rounded-lg border w-full max-w-md"
+   aria-label="Webcam video stream for face capture"
  />
  <img
    src={capturedImageUrl}
    alt="Captured"
+   role="img"
+   aria-label="Captured photo for face search"
    className="max-h-96 rounded-lg border"
  />

Also applies to: 136-140

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8967c03 and e74e37e.

⛔ Files ignored due to path filters (1)
  • frontend/public/photo.jpeg is excluded by !**/*.jpeg
📒 Files selected for processing (2)
  • frontend/src/components/Navigation/Navbar/Navbar.tsx (1 hunks)
  • frontend/src/components/WebCam/WebCamComponent.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • frontend/src/components/Navigation/Navbar/Navbar.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/components/WebCam/WebCamComponent.tsx (7)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/face_clusters.ts (1)
  • fetchSearchedFacesBase64 (58-66)
frontend/src/types/Media.ts (1)
  • Image (13-22)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/searchSlice.ts (3)
  • setResults (24-26)
  • clearSearch (27-31)
  • startSearch (20-23)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/components/ui/dialog.tsx (5)
  • Dialog (131-131)
  • DialogContent (133-133)
  • DialogHeader (136-136)
  • DialogTitle (139-139)
  • DialogDescription (134-134)

Comment on lines +68 to +74
const capture = useCallback(() => {
if (webcamRef.current) {
const imageSrc = webcamRef.current.getScreenshot();
setCapturedImageUrl(imageSrc);
setShowCamera(false);
}
}, [webcamRef]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add null check for getScreenshot() result.

The getScreenshot() method can return null if the webcam stream isn't ready or if there's an error. This would cause capturedImageUrl to be set to null, and the user would see nothing after clicking "Capture Photo".

Apply this diff:

  const capture = useCallback(() => {
    if (webcamRef.current) {
      const imageSrc = webcamRef.current.getScreenshot();
-     setCapturedImageUrl(imageSrc);
-     setShowCamera(false);
+     if (imageSrc) {
+       setCapturedImageUrl(imageSrc);
+       setShowCamera(false);
+     } else {
+       dispatch(
+         showInfoDialog({
+           title: 'Capture Failed',
+           message: 'Unable to capture image. Please try again.',
+           variant: 'error',
+         }),
+       );
+     }
    }
  }, [webcamRef]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const capture = useCallback(() => {
if (webcamRef.current) {
const imageSrc = webcamRef.current.getScreenshot();
setCapturedImageUrl(imageSrc);
setShowCamera(false);
}
}, [webcamRef]);
const capture = useCallback(() => {
if (webcamRef.current) {
const imageSrc = webcamRef.current.getScreenshot();
if (imageSrc) {
setCapturedImageUrl(imageSrc);
setShowCamera(false);
}
}
}, [webcamRef]);
🤖 Prompt for AI Agents
In frontend/src/components/WebCam/WebCamComponent.tsx around lines 68 to 74, the
capture callback calls webcamRef.current.getScreenshot() without checking its
return value which can be null; modify the function to store the result in a
variable, check if the result is non-null before calling setCapturedImageUrl and
setShowCamera(false), and handle the null case (e.g., keep camera open and
optionally set an error/notification state) so we never set capturedImageUrl to
null.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
frontend/src/components/WebCam/WebCamComponent.tsx (3)

123-131: Make the webcam responsive and handle permission errors; optionally reduce image size.

Fixed 500×500 breaks on small screens; add responsive classes and error handler. Also consider screenshotQuality to reduce payload.

   <Webcam
     audio={false}
-    height={500}
     ref={webcamRef}
     screenshotFormat="image/jpeg"
-    width={500}
+    screenshotQuality={0.8}
     videoConstraints={videoConstraints}
-    className="rounded-lg border"
+    className="rounded-lg border w-full max-w-md h-auto"
+    onUserMediaError={() =>
+      dispatch(
+        showInfoDialog({
+          title: 'Camera Access Denied',
+          message:
+            'We could not access your camera. Please grant permission or use file upload.',
+          variant: 'error',
+        }),
+      )
+    }
   />

69-75: Null-check getScreenshot() to avoid null state and user confusion.

getScreenshot() may return null; handle it and keep the camera open with an error hint. Also add dispatch to the hook deps.

-  const capture = useCallback(() => {
-    if (webcamRef.current) {
-      const imageSrc = webcamRef.current.getScreenshot();
-      setCapturedImageUrl(imageSrc);
-      setShowCamera(false);
-    }
-  }, [webcamRef]);
+  const capture = useCallback(() => {
+    const cam = webcamRef.current;
+    if (!cam) return;
+    const imageSrc = cam.getScreenshot();
+    if (imageSrc) {
+      setCapturedImageUrl(imageSrc);
+      setShowCamera(false);
+    } else {
+      dispatch(
+        showInfoDialog({
+          title: 'Capture Failed',
+          message: 'Unable to capture image. Please try again.',
+          variant: 'error',
+        }),
+      );
+    }
+  }, [webcamRef, dispatch]);

82-98: Don’t store large base64 in Redux; also send raw base64 (strip data URL prefix) and only close dialog after kicking off search.

Avoid putting data URLs into Redux; compute payload locally and pass to mutate. Close only when a valid capture exists.

-  const handleSearchCapturedImage = () => {
-    onClose();
-    if (capturedImageUrl) {
-      dispatch(startSearch(capturedImageUrl));
-      dispatch(showLoader('Searching faces...'));
-      getSearchImagesBase64(capturedImageUrl);
-    } else {
-      dispatch(
-        showInfoDialog({
-          title: 'Capture Failed',
-          message: 'An unexpected error occurred during capture.',
-          variant: 'error',
-        }),
-      );
-      handleClose();
-    }
-  };
+  const handleSearchCapturedImage = () => {
+    if (!capturedImageUrl) {
+      dispatch(
+        showInfoDialog({
+          title: 'Capture Failed',
+          message: 'An unexpected error occurred during capture.',
+          variant: 'error',
+        }),
+      );
+      return;
+    }
+    // Strip "data:image/*;base64," if present
+    const payload =
+      capturedImageUrl.includes(',')
+        ? capturedImageUrl.split(',')[1]
+        : capturedImageUrl;
+    dispatch(showLoader('Searching faces...'));
+    getSearchImagesBase64(payload);
+    onClose();
+  };

If other UI depends on searchSlice.active, consider a lightweight action (e.g., startSearchFromCamera()) that sets active without storing the base64.

🧹 Nitpick comments (1)
frontend/src/components/WebCam/WebCamComponent.tsx (1)

35-36: Prevent duplicate submissions: disable actions while the mutation is running.

Expose isLoading from the mutation and disable Capture/Retake/Search buttons during requests.

-  const { mutate: getSearchImagesBase64 } = usePictoMutation({
+  const { mutate: getSearchImagesBase64, isLoading } = usePictoMutation({
     mutationFn: async (base64_data: string) =>
       fetchSearchedFacesBase64({ base64_data }),
-  <Button onClick={capture} className="w-40">
+  <Button onClick={capture} className="w-40" disabled={isLoading}>
     Capture Photo
   </Button>
-  <Button
+  <Button
     onClick={handleRetake}
     variant="outline"
-    className="flex items-center gap-2"
+    className="flex items-center gap-2"
+    disabled={isLoading}
   >
-  <Button
+  <Button
     onClick={handleSearchCapturedImage}
-    className="flex items-center gap-2"
+    className="flex items-center gap-2"
+    disabled={isLoading}
   >

Also applies to: 132-135, 144-158

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e74e37e and 06f3c53.

📒 Files selected for processing (2)
  • frontend/src/components/Navigation/Navbar/Navbar.tsx (1 hunks)
  • frontend/src/components/WebCam/WebCamComponent.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/components/Navigation/Navbar/Navbar.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/components/WebCam/WebCamComponent.tsx (7)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/face_clusters.ts (1)
  • fetchSearchedFacesBase64 (58-66)
frontend/src/types/Media.ts (1)
  • Image (13-22)
frontend/src/features/loaderSlice.ts (2)
  • hideLoader (21-24)
  • showLoader (17-20)
frontend/src/features/searchSlice.ts (3)
  • setResults (24-26)
  • clearSearch (27-31)
  • startSearch (20-23)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
frontend/src/components/ui/dialog.tsx (5)
  • Dialog (131-131)
  • DialogContent (133-133)
  • DialogHeader (136-136)
  • DialogTitle (139-139)
  • DialogDescription (134-134)
🔇 Additional comments (1)
frontend/src/components/WebCam/WebCamComponent.tsx (1)

17-18: <!-- [scratchpad_start]
[task verify_barrel_export 2/2]
[observations]

  • Barrel index file: frontend/src/api/api-functions/index.ts
  • Contains: export * from './face_clusters';
  • fetchSearchedFacesBase64 is exported from face_clusters.ts:58 with export const
  • The barrel correctly re-exports via wildcard export from face_clusters
    [analysis]
  • fetchSearchedFacesBase64 IS properly re-exported from the barrel '@/api/api-functions'
  • The import in WebCamComponent.tsx:17 is correct and will resolve successfully
  • The original review comment's concern is unfounded
  • This is not an issue; the code is correct as-is
    [done]
  • Verification complete: the barrel export is correct and the import will work as expected
    [scratchpad_start] -->

No action needed — the barrel export is correctly configured.

The barrel index file (frontend/src/api/api-functions/index.ts) includes export * from './face_clusters';, which re-exports fetchSearchedFacesBase64. The import in WebCamComponent.tsx is correct and will resolve as expected at build time.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/Dialog/FaceSearchDialog.tsx (1)

80-89: Navigation occurs before file selection completes.

Similar to the issue previously fixed in handleWebCam, the current flow navigates to the home page before the user selects a file. If the user cancels the file picker, they end up on the home page without having accomplished anything.

Apply this diff to improve the UX flow:

 const handlePickFile = async () => {
-  navigate(`/${ROUTES.HOME}`);
   const filePath = await pickSingleFile();
   if (filePath) {
+    navigate(`/${ROUTES.HOME}`);
     setIsDialogOpen(false);
     dispatch(startSearch(filePath));
     dispatch(showLoader('Searching faces...'));
     getSearchImages(filePath);
   }
 };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06f3c53 and 7c3eeb0.

📒 Files selected for processing (2)
  • frontend/src/components/Dialog/FaceSearchDialog.tsx (6 hunks)
  • frontend/src/components/Navigation/Navbar/Navbar.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/components/Navigation/Navbar/Navbar.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/components/Dialog/FaceSearchDialog.tsx (2)
frontend/src/constants/routes.ts (1)
  • ROUTES (1-11)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tauri Build Check (windows-latest)
🔇 Additional comments (1)
frontend/src/components/Dialog/FaceSearchDialog.tsx (1)

62-79: Past review feedback properly addressed.

The resource leak and UX flow issues flagged in the previous review have been correctly resolved. The implementation now:

  • Stops stream tracks immediately after permission check (preventing resource leak)
  • Only navigates and shows camera on successful permission grant
  • Keeps user on current page and shows error dialog if permission denied

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac41b8b and 8d7a082.

📒 Files selected for processing (3)
  • frontend/src-tauri/Entitlements.plist (1 hunks)
  • frontend/src-tauri/tauri.conf.json (2 hunks)
  • frontend/src/components/Navigation/Navbar/Navbar.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • frontend/src-tauri/Entitlements.plist
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src-tauri/tauri.conf.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
  • GitHub Check: Tauri Build Check (ubuntu-22.04)
  • GitHub Check: Tauri Build Check (windows-latest)
🔇 Additional comments (1)
frontend/src/components/Navigation/Navbar/Navbar.tsx (1)

37-41: Verify the removal of the fallback image is intentional.

The previous implementation used || 'photo.png' as a fallback when convertFileSrc(queryImage) returned a falsy value. This fallback has been removed. Ensure that convertFileSrc(queryImage) always returns a valid string or that displaying an image with an empty/undefined src is acceptable in your use case.

If a fallback is still needed, consider adding it back to the non-base64 branch:

                src={
                  queryImage?.startsWith('data:')
                    ? queryImage
-                    : convertFileSrc(queryImage)
+                    : (convertFileSrc(queryImage) || 'photo.png')
                }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1c9131 and acadd54.

📒 Files selected for processing (2)
  • frontend/src-tauri/Entitlements.plist (1 hunks)
  • frontend/src-tauri/Info.plist (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src-tauri/Entitlements.plist
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Backend Tests
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
  • GitHub Check: Tauri Build Check (ubuntu-22.04)
  • GitHub Check: Tauri Build Check (windows-latest)

return perform_face_search(image_path)


@router.post("/face-search-base64")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to create a separate route for this. Just use FastAPI's query parameters to define what is the input format, whether it is a path or a base64 string. Use the /face-search route only.

Accordingly update the Request and Response Schemas.

Comment on lines 38 to 67
onSuccess: (data) => {
const result = data?.data as Image[];
dispatch(hideLoader());
handleClose();
if (result && result.length > 0) {
dispatch(setResults(result));
} else {
dispatch(clearSearch());
dispatch(
showInfoDialog({
title: 'No Matches Found',
message:
"We couldn't find any matching faces in your gallery for this photo.",
variant: 'info',
}),
);
}
},
onError: () => {
dispatch(hideLoader());
handleClose();
dispatch(
showInfoDialog({
title: 'Search Failed',
message: 'There was an error while searching for faces.',
variant: 'error',
}),
);
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of these functions, use this hook: hooks/useMutationFeedback.ts To display messages and show a loader. This keeps the code cleaner. Refer to src/hooks/useFolderOperations.tsx for an example.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
frontend/src/components/Navigation/Navbar/Navbar.tsx (1)

37-41: Previous issue resolved correctly.

The change from includes('base64') to startsWith('data:') properly addresses the past review concern. This is the correct and robust way to distinguish data URLs from filesystem paths, eliminating false positives from paths containing the substring "base64".

🧹 Nitpick comments (1)
frontend/src/api/apiEndpoints.ts (1)

7-8: Refactor to use a function pattern for better maintainability.

Hardcoding query parameters directly in endpoint URLs creates duplication and violates the DRY principle. This is inconsistent with the existing pattern in this file (lines 9-10), which uses functions to handle dynamic parameters.

Apply this diff to refactor using a function pattern:

-  searchForFaces: '/face-clusters/face-search?input_type=path',
-  searchForFacesBase64: '/face-clusters/face-search?input_type=base64',
+  searchForFaces: (inputType: 'path' | 'base64') => `/face-clusters/face-search?input_type=${inputType}`,

Then update call sites to pass the input type:

  • searchForFaces('path') for filesystem paths
  • searchForFaces('base64') for base64 data

This approach:

  • Eliminates duplication of the base URL
  • Provides type safety for the input_type parameter
  • Maintains consistency with the existing function-based endpoint pattern
  • Simplifies maintenance if the base URL changes
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between acadd54 and 5372fbe.

📒 Files selected for processing (5)
  • backend/app/routes/face_clusters.py (3 hunks)
  • backend/app/schemas/images.py (1 hunks)
  • docs/backend/backend_python/openapi.json (5 hunks)
  • frontend/src/api/apiEndpoints.ts (1 hunks)
  • frontend/src/components/Navigation/Navbar/Navbar.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/app/schemas/images.py
  • docs/backend/backend_python/openapi.json
🧰 Additional context used
🧬 Code graph analysis (1)
backend/app/routes/face_clusters.py (2)
backend/app/schemas/images.py (3)
  • AddSingleBase64ImageRequest (16-17)
  • AddSingleImageRequest (12-13)
  • InputType (6-8)
backend/app/utils/faceSearch.py (1)
  • perform_face_search (35-106)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Tauri Build Check (ubuntu-22.04)
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
  • GitHub Check: Tauri Build Check (windows-latest)
  • GitHub Check: Backend Tests
🔇 Additional comments (3)
backend/app/routes/face_clusters.py (3)

1-31: LGTM: Imports are appropriate.

All new imports are necessary for the consolidated face-search functionality.


215-225: LGTM: Path validation is correct.

File existence check and error handling are appropriate.


259-259: LGTM: Clean delegation to shared utility.

The return statement appropriately delegates face search logic to the centralized perform_face_search utility. The Enum type for input_type ensures only valid values are accepted, so no additional branch handling is needed.

Comment on lines 212 to 213
def face_tagging(payload: AddSingleImageRequest | AddSingleBase64ImageRequest, input_type: Annotated[InputType, Query(description="Choose input type: 'path' or 'base64'")] = InputType.path):
image_path = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix payload validation: union type with query parameter discriminator will fail.

The function accepts a union type AddSingleImageRequest | AddSingleBase64ImageRequest, but the discriminator is the input_type query parameter. This creates a validation mismatch where FastAPI validates the body against both schemas independently of the query parameter, but runtime code assumes the payload type matches input_type.

Problem scenario:

  • Request: ?input_type=base64 with body {"path": "/some/path"}
  • FastAPI validation: ✅ Passes (matches AddSingleImageRequest)
  • Runtime line 228: ❌ AttributeError when accessing payload.base64_data on an AddSingleImageRequest instance

Solutions:

  1. Preferred: Use discriminated union with input_type field in payload body:
-def face_tagging(payload: AddSingleImageRequest | AddSingleBase64ImageRequest, input_type: Annotated[InputType, Query(description="Choose input type: 'path' or 'base64'")] = InputType.path):
+from typing import Union
+from pydantic import Field
+
+# Update schemas to include discriminator field
+class AddSingleImageRequest(BaseModel):
+    input_type: Literal["path"] = Field(default="path")
+    path: str
+
+class AddSingleBase64ImageRequest(BaseModel):
+    input_type: Literal["base64"] = Field(default="base64")
+    base64_data: str
+
+def face_tagging(payload: Annotated[Union[AddSingleImageRequest, AddSingleBase64ImageRequest], Field(discriminator="input_type")]):
+    input_type = payload.input_type
     image_path = None
  1. Alternative: Use a single unified schema with optional fields:
+class FaceSearchRequest(BaseModel):
+    path: Optional[str] = None
+    base64_data: Optional[str] = None
+
-def face_tagging(payload: AddSingleImageRequest | AddSingleBase64ImageRequest, input_type: Annotated[InputType, Query(description="Choose input type: 'path' or 'base64'")] = InputType.path):
+def face_tagging(payload: FaceSearchRequest, input_type: Annotated[InputType, Query(description="Choose input type: 'path' or 'base64'")] = InputType.path):

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/app/routes/face_clusters.py around lines 212-213, the handler accepts
a union body type but uses the query param input_type as discriminator which
causes FastAPI to validate the body against either schema regardless of
input_type and leads to AttributeError at runtime; fix by making the payload a
discriminated union with an explicit input_type field inside the request body
(or remove the query param and rely on the body discriminator), updating the
Pydantic models to include a literal input_type field and use
typing.Annotated/Field(discriminator="input_type") so FastAPI validates the
correct branch; alternatively, replace the union with a single unified schema
containing optional path and base64_data fields and perform manual validation at
the start of the handler based on the query input_type, returning 400 on
mismatch.

Comment on lines 227 to 257
elif input_type == InputType.base64:
base64_data = payload.base64_data
if not base64_data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorResponse(
success=False,
error="No base64 data",
message="Base64 image data is required.",
).model_dump(),
)
else:
for image in images:
max_similarity = 0
similarity = FaceNet_util_cosine_similarity(
new_embedding, image["embeddings"]
)
max_similarity = max(max_similarity, similarity)
if max_similarity >= CONFIDENCE_PERCENT:
matches.append(
ImageData(
id=image["id"],
path=image["path"],
folder_id=image["folder_id"],
thumbnailPath=image["thumbnailPath"],
metadata=image["metadata"],
isTagged=image["isTagged"],
tags=image["tags"],
bboxes=image["bbox"],
)
)

return GetAllImagesResponse(
success=True,
message=f"Successfully retrieved {len(matches)} images",
data=matches,
try:
image_bytes = base64.b64decode(base64_data.split(",")[-1])
except (Base64Error, ValueError):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorResponse(
success=False,
error="Invalid base64 data",
message="The provided base64 image data is malformed or invalid.",
).model_dump(),
)
finally:
fd.close()
fn.close()
image_id = str(uuid.uuid4())[:8]
temp_dir = "temp_uploads"
os.makedirs(temp_dir, exist_ok=True)
local_image_path = os.path.join(temp_dir, f"{image_id}.jpeg")

with open(local_image_path, "wb") as f:
f.write(image_bytes)

image_path = local_image_path
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Resource leak—temporary files are never cleaned up.

The base64 flow writes a temporary file to disk (line 252-255) but never deletes it. Previous versions had a finally block that handled cleanup, but this refactored version omits it entirely, causing a resource leak where temp files accumulate indefinitely.

Wrap the perform_face_search call with cleanup logic:

         image_path = local_image_path

-    return perform_face_search(image_path)
+    try:
+        return perform_face_search(image_path)
+    finally:
+        # Cleanup temporary base64 file
+        if input_type == InputType.base64 and image_path and os.path.exists(image_path):
+            os.remove(image_path)

Major: Hardcoded .jpeg extension may not match actual image format.

Line 252 uses .jpeg regardless of the actual image format. The base64 data URL prefix typically indicates the format (e.g., data:image/png;base64,...). Saving with the wrong extension can cause issues with libraries that rely on file extensions.

Detect the format from the data URL prefix:

+        # Extract format from data URL (e.g., "data:image/png;base64,...")
+        format_match = base64_data.split(";")[0].split("/")[-1] if ";" in base64_data else "jpeg"
+        extension = format_match if format_match in ["jpeg", "jpg", "png", "gif", "webp"] else "jpeg"
         image_id = str(uuid.uuid4())[:8]
         temp_dir = "temp_uploads"
         os.makedirs(temp_dir, exist_ok=True)
-        local_image_path = os.path.join(temp_dir, f"{image_id}.jpeg")
+        local_image_path = os.path.join(temp_dir, f"{image_id}.{extension}")

Major: Hardcoded temp directory path.

Line 250 hardcodes "temp_uploads". This should be configurable via environment variables or a config module for flexibility across environments (dev, staging, production).

Move to configuration:

# In config.py or settings
TEMP_UPLOAD_DIR = os.getenv("TEMP_UPLOAD_DIR", "temp_uploads")

# In route
temp_dir = TEMP_UPLOAD_DIR

Optional: 8-character UUID increases collision risk.

Line 249 uses only 8 characters of a UUID. While collisions are unlikely in typical usage, high-volume scenarios or long-running services could encounter duplicates. Consider using the full UUID or at least 16 characters.

-        image_id = str(uuid.uuid4())[:8]
+        image_id = str(uuid.uuid4())  # or [:16] for a balance
🤖 Prompt for AI Agents
In backend/app/routes/face_clusters.py around lines 227 to 257, the base64
handling writes a temp file but never cleans it up, always uses a hardcoded
".jpeg" extension, and uses a hardcoded temp dir and short 8-char UUID; fix by
(1) deriving image format from the base64 data URL prefix and choose an
appropriate extension, (2) use a configured temp directory (read from config or
env var like TEMP_UPLOAD_DIR) instead of the literal "temp_uploads", (3) wrap
the perform_face_search call (or the surrounding logic that uses the temp file)
in try/finally so the local_image_path is removed in the finally block to
prevent leaks, and (4) replace the 8-character slice with a less collision-prone
identifier (full UUID or at least 16 chars).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/app/routes/face_clusters.py (1)

208-214: Add a response_model to document the 200 payload.

The handler returns GetAllImagesResponse, but the route lacks response_model, producing an empty 200 schema in OpenAPI. Add the model to keep clients typed.

-@router.post(
-    "/face-search",
-    responses={code: {"model": ErrorResponse} for code in [400, 500]},
-)
-def face_tagging(payload: FaceSearchRequest, input_type: Annotated[InputType, Query(description="Choose input type: 'path' or 'base64'")] = InputType.path):
+@router.post(
+    "/face-search",
+    response_model=GetAllImagesResponse,
+    responses={code: {"model": ErrorResponse} for code in [400, 500]},
+)
+def face_tagging(
+    payload: FaceSearchRequest,
+    input_type: Annotated[InputType, Query(description="Choose input type: 'path' or 'base64'")] = InputType.path,
+):
docs/backend/backend_python/openapi.json (1)

1069-1076: Add response_model=GetAllImagesResponse to the face-search route decorator.

The face_tagging() function returns perform_face_search(image_path), which returns GetAllImagesResponse. The @router.post() decorator at lines 208–210 lacks response_model, causing the OpenAPI 200 response schema to be empty {}. Add response_model=GetAllImagesResponse to the decorator and regenerate the OpenAPI spec.

♻️ Duplicate comments (2)
frontend/src/components/WebCam/WebCamComponent.tsx (2)

80-85: Don’t store base64 image in Redux.

Large data URLs bloat state/DevTools. Pass base64 directly to the mutation and keep it in component state.

   const handleSearchCapturedImage = () => {
     onClose();
     if (capturedImageUrl) {
-      dispatch(startSearch(capturedImageUrl));
-      getSearchImagesBase64.mutate(capturedImageUrl);
+      // Option A: lightweight start flag
+      dispatch(startSearch('camera')); // or omit entirely
+      getSearchImagesBase64.mutate(capturedImageUrl);
     } else {

67-73: Null-check getScreenshot() result.

getScreenshot() may return null; handle it to avoid setting null into state and toggling UI incorrectly.

   const capture = useCallback(() => {
     if (webcamRef.current) {
-      const imageSrc = webcamRef.current.getScreenshot();
-      setCapturedImageUrl(imageSrc);
-      setShowCamera(false);
+      const imageSrc = webcamRef.current.getScreenshot();
+      if (imageSrc) {
+        setCapturedImageUrl(imageSrc);
+        setShowCamera(false);
+      } else {
+        dispatch(
+          showInfoDialog({
+            title: 'Capture Failed',
+            message: 'Unable to capture image. Please try again.',
+            variant: 'error',
+          }),
+        );
+      }
     }
   }, [webcamRef]);
🧹 Nitpick comments (6)
backend/app/routes/face_clusters.py (2)

261-267: Temp file handling: prefer tempfile + configurable dir + stronger IDs.

  • Hardcoded "temp_uploads" and 8-char UUID increase collision/config risk.
  • Use tempfile in a configured dir; use full UUID.
-        format_match = base64_data.split(";")[0].split("/")[-1] if ";" in base64_data else "jpeg"
-        extension = format_match if format_match in ["jpeg", "jpg", "png", "gif", "webp"] else "jpeg"
-        image_id = str(uuid.uuid4())[:8]
-        temp_dir = "temp_uploads"
-        os.makedirs(temp_dir, exist_ok=True)
-        local_image_path = os.path.join(temp_dir, f"{image_id}.{extension}")
+        format_match = base64_data.split(";")[0].split("/")[-1] if ";" in base64_data else "jpeg"
+        extension = format_match if format_match in {"jpeg", "jpg", "png", "gif", "webp"} else "jpeg"
+        image_id = str(uuid.uuid4())
+        temp_dir = os.getenv("TEMP_UPLOAD_DIR", "temp_uploads")
+        os.makedirs(temp_dir, exist_ok=True)
+        import tempfile
+        fd, local_image_path = tempfile.mkstemp(prefix=f"{image_id}_", suffix=f".{extension}", dir=temp_dir)
+        os.close(fd)

261-269: Validate actual image type from bytes, not just data URL.

Headers can lie. Probe bytes (e.g., Pillow or imghdr) and normalize extension accordingly.

If Pillow is available:

+        from PIL import Image
+        from io import BytesIO
+        try:
+            fmt = Image.open(BytesIO(image_bytes)).format  # e.g., 'JPEG', 'PNG'
+            extension = fmt.lower() if fmt else extension
+        except Exception:
+            pass  # keep derived extension
docs/backend/backend_python/openapi.json (3)

1045-1056: Minor: Parameter description duplication.

description appears both on the parameter and inside its schema. Keep only at parameter level for clarity.


1516-1543: Consider schema constraints for FaceSearchRequest.

If input_type=path then path should be required; if base64 then base64_data should be required. FastAPI can’t express cross-parameter requirements here; document with examples or switch to a discriminated body schema.


1039-1046: Rename summary to “Face Search”.

Align resource name and UI (“Face Search” vs “Face Tagging”).

frontend/src/components/WebCam/WebCamComponent.tsx (1)

125-131: Optional: reduce screenshot size to improve UX/perf.

Set screenshotQuality (e.g., 0.8) to reduce payload size; faster upload and less memory.

   <Webcam
     audio={false}
     ref={webcamRef}
-    screenshotFormat="image/jpeg"
+    screenshotFormat="image/jpeg"
+    screenshotQuality={0.8}
     videoConstraints={videoConstraints}
     className="w-full max-w-md rounded-lg border"
   />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5372fbe and 19d11d3.

📒 Files selected for processing (4)
  • backend/app/routes/face_clusters.py (3 hunks)
  • backend/app/schemas/images.py (1 hunks)
  • docs/backend/backend_python/openapi.json (5 hunks)
  • frontend/src/components/WebCam/WebCamComponent.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/schemas/images.py
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/src/components/WebCam/WebCamComponent.tsx (6)
frontend/src/hooks/useQueryExtension.ts (1)
  • usePictoMutation (26-78)
frontend/src/api/api-functions/face_clusters.ts (1)
  • fetchSearchedFacesBase64 (58-66)
frontend/src/hooks/useMutationFeedback.tsx (1)
  • useMutationFeedback (60-135)
frontend/src/types/Media.ts (1)
  • Image (13-22)
frontend/src/features/searchSlice.ts (3)
  • setResults (24-26)
  • clearSearch (27-31)
  • startSearch (20-23)
frontend/src/features/infoDialogSlice.ts (1)
  • showInfoDialog (16-30)
backend/app/routes/face_clusters.py (2)
backend/app/schemas/images.py (2)
  • FaceSearchRequest (11-13)
  • InputType (6-8)
backend/app/utils/faceSearch.py (1)
  • perform_face_search (35-106)
🔇 Additional comments (2)
backend/app/routes/face_clusters.py (1)

273-277: Cleanup looks good.

Base64 temp file is removed in finally; path inputs are preserved. LGTM.

Optional: add best-effort logging on cleanup failures.

frontend/src/components/WebCam/WebCamComponent.tsx (1)

33-38: Import path is correctly configured.

The barrel index file at frontend/src/api/api-functions/index.ts properly re-exports fetchSearchedFacesBase64 from face_clusters.ts via export * from './face_clusters', making the import from @/api/api-functions valid and functional. No changes needed.

Comment on lines +35 to +38
const getSearchImagesBase64 = usePictoMutation({
mutationFn: async (base64_data: string) =>
fetchSearchedFacesBase64({ base64_data }),
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Endpoint mismatch: use unified /face-search with input_type=base64.

Backend now exposes POST /face-clusters/face-search with query input_type. Calling fetchSearchedFacesBase64 (likely hitting a removed base64-specific route) will 404.

-  const getSearchImagesBase64 = usePictoMutation({
-    mutationFn: async (base64_data: string) =>
-      fetchSearchedFacesBase64({ base64_data }),
-  });
+  const getSearchImagesBase64 = usePictoMutation({
+    mutationFn: async (base64_data: string) =>
+      // Prefer a dedicated api function: fetchFaceSearch({ base64_data }, { input_type: 'base64' })
+      apiClient.post('/face-clusters/face-search?input_type=base64', {
+        base64_data,
+      }).then(r => r.data),
+  });

Follow-up: update api-functions/face_clusters.ts to export a generic fetchFaceSearch(body, params) and change callers. I can draft that if helpful.

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
backend/app/routes/face_clusters.py (2)

243-253: Add size limit validation for base64 payloads.

The code doesn't validate the size of base64_data before decoding, which could allow memory exhaustion attacks with oversized payloads.

Apply this diff to add size validation:

+    # Define max size (e.g., 10MB base64 ≈ 14MB encoded)
+    MAX_BASE64_SIZE = int(os.getenv("MAX_BASE64_IMAGE_SIZE", 14_000_000))
+    
     elif input_type == InputType.base64:
         base64_data = payload.base64_data
         if not base64_data:
             raise HTTPException(
                 status_code=status.HTTP_400_BAD_REQUEST,
                 detail=ErrorResponse(
                     success=False,
                     error="No base64 data",
                     message="Base64 image data is required.",
                 ).model_dump(),
             )
+        
+        # Validate size before decoding
+        if len(base64_data) > MAX_BASE64_SIZE:
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ErrorResponse(
+                    success=False,
+                    error="Payload too large",
+                    message="Base64 image exceeds maximum allowed size.",
+                ).model_dump(),
+            )

Based on learnings


220-241: Critical: Validate file paths against an allowlist to prevent path traversal.

The code accepts arbitrary filesystem paths without validation, allowing potential access to sensitive files outside the intended media directory. An attacker could provide paths like /etc/passwd or use ../ to escape the intended directory.

Apply this diff to add path validation:

+from pathlib import Path
+
+# At the top of the function or in config
+MEDIA_ROOT = os.getenv("MEDIA_ROOT", os.path.abspath("uploads"))
+
 if input_type == InputType.path:
     local_file_path = payload.path
 
     if not local_file_path:
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ErrorResponse(
                 success=False,
                 error="No Image path provided ",
                 message="image path is required.",
             ).model_dump(),
         )
+    
+    # Resolve path and validate against allowlist
+    try:
+        resolved_path = Path(local_file_path).resolve()
+        media_root = Path(MEDIA_ROOT).resolve()
+        
+        # Check if path is within allowed directory
+        if not resolved_path.is_relative_to(media_root):
+            raise HTTPException(
+                status_code=status.HTTP_400_BAD_REQUEST,
+                detail=ErrorResponse(
+                    success=False,
+                    error="Path not allowed",
+                    message="The provided path is outside the allowed media directory.",
+                ).model_dump(),
+            )
+    except ValueError:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ErrorResponse(
+                success=False,
+                error="Invalid path",
+                message="The provided path is invalid.",
+            ).model_dump(),
+        )
+    
-    if not os.path.isfile(local_file_path):
+    if not resolved_path.is_file():
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=ErrorResponse(
                 success=False,
                 error="Invalid file path",
                 message="The provided path is not a valid file",
             ).model_dump(),
         )
-    image_path = payload.path
+    
+    # Check file is readable
+    if not os.access(resolved_path, os.R_OK):
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ErrorResponse(
+                success=False,
+                error="Unreadable file",
+                message="The server cannot read the provided file.",
+            ).model_dump(),
+        )
+    
+    image_path = str(resolved_path)

Based on learnings

🧹 Nitpick comments (3)
frontend/src/api/apiEndpoints.ts (1)

5-11: Consider adding JSDoc documentation for the face cluster endpoints.

Adding documentation would improve maintainability by clarifying the expected usage, input types, and any request format requirements for each endpoint.

Apply this diff to add documentation:

+/**
+ * Face clustering and search endpoints.
+ */
 export const faceClustersEndpoints = {
+  /** Retrieves all face clusters */
   getAllClusters: '/face-clusters/',
+  /** Searches for faces using file path input */
   searchForFaces: '/face-clusters/face-search?input_type=path',
+  /** Searches for faces using base64-encoded image data */
   searchForFacesBase64: '/face-clusters/face-search?input_type=base64',
+  /** Updates the name of a specific cluster */
   renameCluster: (clusterId: string) => `/face-clusters/${clusterId}`,
+  /** Retrieves all images belonging to a specific cluster */
   getClusterImages: (clusterId: string) => `/face-clusters/${clusterId}/images`,
 };
backend/app/routes/face_clusters.py (2)

274-277: Use configurable temp directory path.

The temp directory is hardcoded to "temp_uploads", which reduces flexibility across different environments (dev, staging, production).

Apply this diff to use a configurable path:

-        image_id = str(uuid.uuid4())[:8]
-        temp_dir = "temp_uploads"
+        image_id = str(uuid.uuid4())[:8]
+        temp_dir = os.getenv("TEMP_UPLOAD_DIR", "temp_uploads")
         os.makedirs(temp_dir, exist_ok=True)
         local_image_path = os.path.join(temp_dir, f"{image_id}.{extension}")

Based on learnings


274-274: Consider using full UUID for temp file names.

The 8-character UUID prefix increases collision risk in high-volume scenarios. While unlikely for temporary files with immediate cleanup, using the full UUID provides stronger guarantees.

-        image_id = str(uuid.uuid4())[:8]
+        image_id = str(uuid.uuid4())

Based on learnings

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19d11d3 and 73278fc.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • backend/app/routes/face_clusters.py (3 hunks)
  • frontend/package.json (1 hunks)
  • frontend/src/api/apiEndpoints.ts (1 hunks)
  • frontend/src/components/Navigation/Navbar/Navbar.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/package.json
  • frontend/src/components/Navigation/Navbar/Navbar.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
backend/app/routes/face_clusters.py (2)
backend/app/schemas/images.py (3)
  • FaceSearchRequest (11-13)
  • InputType (6-8)
  • ErrorResponse (59-62)
backend/app/utils/faceSearch.py (1)
  • perform_face_search (35-106)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Tauri Build Check (windows-latest)
  • GitHub Check: Tauri Build Check (macos-latest, --target aarch64-apple-darwin)
🔇 Additional comments (4)
frontend/src/api/apiEndpoints.ts (1)

7-8: LGTM! Backend correctly implements dual input-type handling.

Verified that the backend /face-search endpoint properly handles both input_type query parameters:

  • path: Validates the file exists locally and passes to face search
  • base64: Decodes base64 data, saves to temp file, processes it, and cleans up after

The frontend endpoint constants align with the backend implementation, and the approach maintains backward compatibility with the default path-based input type.

backend/app/routes/face_clusters.py (3)

2-7: LGTM! Imports are appropriate.

The new imports support the dual-input functionality (path/base64) and proper error handling for base64 decoding.

Also applies to: 26-27


212-218: LGTM! Unified schema approach with query discriminator.

The function uses a unified FaceSearchRequest schema with optional fields and relies on the input_type query parameter to determine which field to use. This is a valid approach that addresses previous concerns about payload validation.


284-288: LGTM! Cleanup logic properly prevents resource leaks.

The finally block correctly removes temporary files created during base64 processing, with appropriate guards to prevent errors.

status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorResponse(
success=False,
error="No Image path provided ",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix typo in error message.

There's an extra space at the end of the error message.

Apply this diff:

-                    error="No Image path provided ",
+                    error="No Image path provided",
🤖 Prompt for AI Agents
In backend/app/routes/face_clusters.py around line 228, the error message string
"No Image path provided " has an extra trailing space; remove the trailing space
so the message reads "No Image path provided" (update the string literal in that
location).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
backend/app/routes/face_clusters.py (1)

228-228: Fix: Remove trailing space in error message.

-                    error="No Image path provided ",
+                    error="No Image path provided",
🧹 Nitpick comments (1)
backend/app/routes/face_clusters.py (1)

243-293: Strong base64 handling with room for configuration improvements.

The implementation correctly:

  • Validates payload presence before processing
  • Enforces size limits to prevent memory exhaustion attacks
  • Handles decoding errors gracefully with appropriate HTTP status codes
  • Detects image format from the data URL prefix

However, two minor configuration issues remain from previous reviews:

  1. Line 286: The temp_uploads directory is still hardcoded. Consider moving to environment configuration:

    temp_dir = os.getenv("TEMP_UPLOAD_DIR", "temp_uploads")
  2. Line 285: The 8-character UUID prefix has low (but non-zero) collision risk in high-volume scenarios. Using the full UUID or at least 16 characters would be safer.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73278fc and df38c90.

📒 Files selected for processing (1)
  • backend/app/routes/face_clusters.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/app/routes/face_clusters.py (2)
backend/app/schemas/images.py (3)
  • FaceSearchRequest (11-13)
  • InputType (6-8)
  • ErrorResponse (59-62)
backend/app/utils/faceSearch.py (1)
  • perform_face_search (35-106)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Tauri Build Check (windows-latest)
  • GitHub Check: Backend Tests
🔇 Additional comments (3)
backend/app/routes/face_clusters.py (3)

1-31: LGTM: Imports are well-organized and necessary.

All new imports (Base64Error, base64, Annotated, Query, FaceSearchRequest, InputType, perform_face_search) are appropriately used in the face search implementation below.


212-217: Good solution: Single route with query parameter discriminator.

The implementation correctly addresses the maintainer's feedback to use a single /face-search route with a query parameter to distinguish between path and base64 inputs, rather than separate endpoints.


295-299: Excellent cleanup implementation.

The try/finally pattern correctly ensures temporary files are removed even when exceptions occur. The conditional check if input_type == InputType.base64 and image_path and os.path.exists(image_path) properly guards against cleanup errors and only removes files created by the base64 path.

Comment on lines +220 to +241
if input_type == InputType.path:
local_file_path = payload.path

if not local_file_path:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorResponse(
success=False,
error="No Image path provided ",
message="image path is required.",
).model_dump(),
)
if not os.path.isfile(local_file_path):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=ErrorResponse(
success=False,
error="Invalid file path",
message="The provided path is not a valid file",
).model_dump(),
)
image_path = payload.path
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Validate that only the expected field is provided.

When input_type=path, the code doesn't verify that payload.base64_data is None. A client could send both fields, leading to ambiguous behavior where base64_data is silently ignored. This makes debugging harder and violates the principle of early validation.

Add mutual exclusivity validation at the start of the function:

 def face_tagging(
     payload: FaceSearchRequest,
     input_type: Annotated[
         InputType, Query(description="Choose input type: 'path' or 'base64'")
     ] = InputType.path,
 ):
     image_path = None
+    
+    # Validate mutual exclusivity
+    if input_type == InputType.path and payload.base64_data is not None:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ErrorResponse(
+                success=False,
+                error="Invalid request",
+                message="Cannot provide base64_data when input_type is 'path'.",
+            ).model_dump(),
+        )
+    
+    if input_type == InputType.base64 and payload.path is not None:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ErrorResponse(
+                success=False,
+                error="Invalid request",
+                message="Cannot provide path when input_type is 'base64'.",
+            ).model_dump(),
+        )
 
     if input_type == InputType.path:

Previous security concerns remain unaddressed.

As noted in earlier reviews, the path input still lacks validation against an allowlist/configured media root, which could allow reading arbitrary filesystem paths. Additionally, line 228 has a trailing space in the error message.


🏁 Script executed:

#!/bin/bash
# Examine the face_tagging function and surrounding context
cat backend/app/routes/face_clusters.py | sed -n '200,260p'

Length of output: 2047


🏁 Script executed:

#!/bin/bash
# Find the FaceSearchRequest model definition
rg -n "class FaceSearchRequest" --type=py -A 10

Length of output: 586


🏁 Script executed:

#!/bin/bash
# Check for any path validation patterns or allowlist usage in the codebase
rg -n "allowlist|MEDIA_ROOT|secure_path|safe.*path" --type=py -C 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for the InputType enum definition
rg -n "class InputType|InputType\s*=" --type=py -A 5

Length of output: 299


🏁 Script executed:

#!/bin/bash
# Check for any global path validation or filesystem configuration
rg -n "os\.path\.|file.*path|image.*path" --type=py -B 2 -A 2 | head -50

Length of output: 3003


🏁 Script executed:

#!/bin/bash
# Look for configuration files that might define allowed paths
fd -type f -name "*.env*" -o -name "*config*" -o -name "settings*" | grep -E "\.(py|env|yaml|toml|json)$"

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Check if there's any validation in middleware or app initialization
rg -n "mount|middleware|startup|app\." --type=py backend/app/ | grep -E "(middleware|mount|startup)" | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Verify the actual behavior: does base64_data get processed if both are sent?
cat backend/app/routes/face_clusters.py | sed -n '210,300p' | grep -A 5 "elif input_type"

Length of output: 238


Validate that only the expected field is provided.

When input_type=path, the code doesn't verify that payload.base64_data is None. A client could send both fields, leading to ambiguous behavior where base64_data is silently ignored. This makes debugging harder and violates the principle of early validation.

Add mutual exclusivity validation at the start of the function:

 def face_tagging(
     payload: FaceSearchRequest,
     input_type: Annotated[
         InputType, Query(description="Choose input type: 'path' or 'base64'")
     ] = InputType.path,
 ):
     image_path = None
+    
+    # Validate mutual exclusivity
+    if input_type == InputType.path and payload.base64_data is not None:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ErrorResponse(
+                success=False,
+                error="Invalid request",
+                message="Cannot provide base64_data when input_type is 'path'.",
+            ).model_dump(),
+        )
+    
+    if input_type == InputType.base64 and payload.path is not None:
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail=ErrorResponse(
+                success=False,
+                error="Invalid request",
+                message="Cannot provide path when input_type is 'base64'.",
+            ).model_dump(),
+        )
 
     if input_type == InputType.path:

Fix trailing space in error message and address remaining path security concerns.

Line 228 contains a trailing space: error="No Image path provided ". Additionally, the path input still lacks validation against an allowlist or configured media root, which could allow reading arbitrary filesystem paths.

Committable suggestion skipped: line range outside the PR's diff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend bug Something isn't working UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants