Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
8b2c12b
Fixed navigation to home
tushar1977 Sep 23, 2025
0129f7c
Merge branch 'AOSSIE-Org:main' into main
tushar1977 Sep 23, 2025
52a8a57
Removed console logs
tushar1977 Sep 23, 2025
7b73b34
Testing webcam permissions
tushar1977 Sep 25, 2025
4fc47c7
testing for permissions
tushar1977 Sep 25, 2025
5c05baf
Testing webcam capture on windows
tushar1977 Sep 26, 2025
1e4b900
Added dialog box for those device that dont support webcam
tushar1977 Oct 5, 2025
5d06b19
Merge branch 'AOSSIE-Org:main' into main
tushar1977 Oct 5, 2025
b9a36dd
Revert "Removed console logs"
tushar1977 Oct 5, 2025
2c037c8
Reverted some changes to 7b73b34 commit and fixed the closing of dial…
tushar1977 Oct 13, 2025
79c9b67
testing on windows
tushar1977 Oct 14, 2025
f655d50
Merge branch 'AOSSIE-Org:main' into test
tushar1977 Oct 14, 2025
994b943
Implemented Base64 to image route
tushar1977 Oct 16, 2025
ed5168b
Added routes in frontend
tushar1977 Oct 16, 2025
b083616
Merge test branch
tushar1977 Oct 16, 2025
4fba2f2
Fixed mutate function to fetch images from fetchSearchedFacesBase64
tushar1977 Oct 16, 2025
ec95615
Merge branch 'main' of https://github.com/tushar1977/PictoPy
tushar1977 Oct 16, 2025
14f55fc
Fixed bugs in backend
tushar1977 Oct 16, 2025
efe79db
Fixed frontend
tushar1977 Oct 16, 2025
fd18420
Fixed linting
tushar1977 Oct 16, 2025
dbc0820
Removed redundant import
tushar1977 Oct 16, 2025
5b216a6
Reverted main.rs file
tushar1977 Oct 16, 2025
1f7113d
Reverted files
tushar1977 Oct 16, 2025
eb99b0e
Fixed bugs
tushar1977 Oct 16, 2025
7c8bef7
Fixed grammatical mistake
tushar1977 Oct 16, 2025
7297b59
Fixed critical and major bugs
tushar1977 Oct 16, 2025
a12a7c9
Fixed tauri config file
tushar1977 Oct 16, 2025
340b1e9
Fixed package json
tushar1977 Oct 16, 2025
aed41b4
Reformated using black
tushar1977 Oct 16, 2025
aa01490
Fixed webcam bugs
tushar1977 Oct 16, 2025
8967c03
Fixed cleanup
tushar1977 Oct 16, 2025
e74e37e
Fixed default image to appear when searching
tushar1977 Oct 19, 2025
06f3c53
Fixed thumbnail image and webcam onclose
tushar1977 Oct 19, 2025
66b22a5
Fixed merge conflicts
tushar1977 Oct 19, 2025
7c3eeb0
Fixed linting
tushar1977 Oct 19, 2025
f7c4a97
Removed duplicate photo.jpeg
tushar1977 Oct 19, 2025
ac41b8b
Fixing major bugs
tushar1977 Oct 19, 2025
8a1a930
Merge branch 'main' into main
tushar1977 Oct 19, 2025
8d7a082
Implemented plist file for macOs
tushar1977 Oct 22, 2025
e39321e
Fixed plist file
tushar1977 Oct 22, 2025
6fbb39d
Fix plist
tushar1977 Oct 22, 2025
d1c9131
Fixed plist file for macOs
tushar1977 Oct 22, 2025
acadd54
Added info.plist
tushar1977 Oct 22, 2025
5372fbe
Refractord backend code
tushar1977 Oct 23, 2025
19d11d3
Fixed bugs
tushar1977 Oct 23, 2025
2dfa314
Linted files
tushar1977 Oct 23, 2025
73278fc
Merge branch 'main' into main
tushar1977 Oct 23, 2025
df38c90
Fixed to have limit on base64
tushar1977 Oct 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 88 additions & 91 deletions backend/app/routes/face_clusters.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
import logging
from binascii import Error as Base64Error
import base64
from typing import Annotated
import uuid
import os
from typing import Optional, List, Dict, Any
from pydantic import BaseModel
from app.config.settings import CONFIDENCE_PERCENT, DEFAULT_FACENET_MODEL
from fastapi import APIRouter, HTTPException, status
from fastapi import APIRouter, HTTPException, Query, status
from app.database.face_clusters import (
db_get_cluster_by_id,
db_update_cluster,
db_get_all_clusters_with_face_counts,
db_get_images_by_cluster_id, # Add this import
)
from app.database.faces import get_all_face_embeddings
from app.models.FaceDetector import FaceDetector
from app.models.FaceNet import FaceNet
from app.schemas.face_clusters import (
RenameClusterRequest,
RenameClusterResponse,
Expand All @@ -26,32 +23,8 @@
GetClusterImagesData,
ImageInCluster,
)
from app.schemas.images import AddSingleImageRequest
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]
from app.schemas.images import FaceSearchRequest, InputType
from app.utils.faceSearch import perform_face_search


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -236,67 +209,91 @@ def get_cluster_images(cluster_id: str):
"/face-search",
responses={code: {"model": ErrorResponse} for code in [400, 500]},
)
def face_tagging(payload: AddSingleImageRequest):
image_path = payload.path
if not os.path.isfile(image_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(),
)

fd = FaceDetector()
fn = FaceNet(DEFAULT_FACENET_MODEL)
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=f"Successfully retrieved {len(matches)} images",
data=[],
def face_tagging(
payload: FaceSearchRequest,
input_type: Annotated[
InputType, Query(description="Choose input type: 'path' or 'base64'")
] = InputType.path,
):
image_path = None

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 ",
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).

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
Comment on lines +220 to +241
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.


process_face = result["processed_faces"][0]
new_embedding = fn.get_embedding(process_face)

images = get_all_face_embeddings()
if len(images) == 0:
return GetAllImagesResponse(
success=True,
message=f"Successfully retrieved {len(matches)} images",
data=[],
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,
MAX_B64_LEN = 14_000_000 # 10MB
if len(base64_data) > MAX_B64_LEN:
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(),
)
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(),
)

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}")

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

image_path = local_image_path

try:
return perform_face_search(image_path)
finally:
fd.close()
fn.close()
if input_type == InputType.base64 and image_path and os.path.exists(image_path):
os.remove(image_path)
12 changes: 9 additions & 3 deletions backend/app/schemas/images.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
from enum import Enum
from pydantic import BaseModel
from typing import Optional, List, Union


# Request Model
class AddSingleImageRequest(BaseModel):
path: str
class InputType(str, Enum):
path = "path"
base64 = "base64"


class FaceSearchRequest(BaseModel):
path: Optional[str] = None
base64_data: Optional[str] = None


class AddMultipleImagesRequest(BaseModel):
Expand Down
106 changes: 106 additions & 0 deletions backend/app/utils/faceSearch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import uuid
from typing import Optional, List, Dict, Any
from pydantic import BaseModel
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]


def perform_face_search(image_path: str) -> GetAllImagesResponse:
"""
Performs face detection, embedding generation, and similarity search.

Args:
image_path (str): Path to the image file to process.

Returns:
GetAllImagesResponse: Search result containing matched images.
"""
fd = FaceDetector()
fn = FaceNet(DEFAULT_FACENET_MODEL)

try:
matches = []
image_id = str(uuid.uuid4())

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=[],
)

process_face = result["processed_faces"][0]
new_embedding = fn.get_embedding(process_face)

images = get_all_face_embeddings()
if not images:
return GetAllImagesResponse(
success=True,
message="No face embeddings available for comparison.",
data=[],
)

for image in images:
similarity = FaceNet_util_cosine_similarity(
new_embedding, image["embeddings"]
)
if 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)} matching images.",
data=matches,
)

finally:
if "fd" in locals() and fd is not None:
fd.close()
if "fn" in locals() and fn is not None:
fn.close()
Loading
Loading