Skip to content
Closed
Show file tree
Hide file tree
Changes from 31 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
6 changes: 3 additions & 3 deletions backend/app/database/face_clusters.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,9 @@ def db_delete_all_clusters() -> int:
return deleted_count


def db_get_all_clusters_with_face_counts() -> List[
Dict[str, Union[str, Optional[str], int]]
]:
def db_get_all_clusters_with_face_counts() -> (
List[Dict[str, Union[str, Optional[str], int]]]
):
"""
Retrieve all clusters with their face counts and stored face images.

Expand Down
8 changes: 4 additions & 4 deletions backend/app/database/faces.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,9 @@ def db_get_faces_unassigned_clusters() -> List[Dict[str, Union[FaceId, FaceEmbed
return faces


def db_get_all_faces_with_cluster_names() -> List[
Dict[str, Union[FaceId, FaceEmbedding, Optional[str]]]
]:
def db_get_all_faces_with_cluster_names() -> (
List[Dict[str, Union[FaceId, FaceEmbedding, Optional[str]]]]
):
"""
Get all faces with their corresponding cluster names.

Expand Down Expand Up @@ -271,7 +271,7 @@ def db_get_all_faces_with_cluster_names() -> List[


def db_update_face_cluster_ids_batch(
face_cluster_mapping: List[Dict[str, Union[FaceId, ClusterId]]]
face_cluster_mapping: List[Dict[str, Union[FaceId, ClusterId]]],
) -> None:
"""
Update cluster IDs for multiple faces in batch.
Expand Down
6 changes: 3 additions & 3 deletions backend/app/database/folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,9 +382,9 @@ def db_get_folder_ids_by_paths(
conn.close()


def db_get_all_folder_details() -> List[
Tuple[str, str, Optional[str], int, bool, Optional[bool]]
]:
def db_get_all_folder_details() -> (
List[Tuple[str, str, Optional[str], int, bool, Optional[bool]]]
):
"""
Get all folder details including folder_id, folder_path, parent_folder_id,
last_modified_time, AI_Tagging, and taggingCompleted.
Expand Down
1 change: 1 addition & 0 deletions backend/app/routes/albums.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

router = APIRouter()


# GET /albums/ - Get all albums
@router.get("/", response_model=GetAlbumsResponse)
def get_albums(show_hidden: bool = Query(False)):
Expand Down
118 changes: 40 additions & 78 deletions backend/app/routes/face_clusters.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import logging
from binascii import Error as Base64Error
import base64
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 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 +22,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 AddSingleBase64ImageRequest, AddSingleImageRequest
from app.utils.faceSearch import perform_face_search


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -247,56 +219,46 @@ def face_tagging(payload: AddSingleImageRequest):
message="The provided path is not a valid file",
).model_dump(),
)
return perform_face_search(image_path)


@router.post("/face-search-base64")
Comment thread
rahulharpal1603 marked this conversation as resolved.
Outdated
def face_search_base64(payload: AddSingleBase64ImageRequest):
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(),
)

fd = FaceDetector()
fn = FaceNet(DEFAULT_FACENET_MODEL)
image_path = None
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=[],
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(),
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
image_id = str(uuid.uuid4())[:8]
temp_dir = "temp_uploads"
os.makedirs(temp_dir, exist_ok=True)
image_path = os.path.join(temp_dir, f"{image_id}.jpeg")

process_face = result["processed_faces"][0]
new_embedding = fn.get_embedding(process_face)
with open(image_path, "wb") as f:
f.write(image_bytes)

images = get_all_face_embeddings()
if len(images) == 0:
return GetAllImagesResponse(
success=True,
message=f"Successfully retrieved {len(matches)} images",
data=[],
)
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"],
)
)
result = perform_face_search(image_path)
return result

return GetAllImagesResponse(
success=True,
message=f"Successfully retrieved {len(matches)} images",
data=matches,
)
finally:
fd.close()
fn.close()
if image_path and os.path.exists(image_path):
os.remove(image_path)
4 changes: 4 additions & 0 deletions backend/app/schemas/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ class AddSingleImageRequest(BaseModel):
path: str


class AddSingleBase64ImageRequest(BaseModel):
base64_data: str


class AddMultipleImagesRequest(BaseModel):
paths: List[str]

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]
Comment on lines +11 to +32
Copy link
Copy Markdown
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.



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=[],
)
Comment on lines +48 to +65
Copy link
Copy Markdown
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.


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()
4 changes: 2 additions & 2 deletions backend/app/utils/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def image_util_process_untagged_images() -> bool:


def image_util_classify_and_face_detect_images(
untagged_images: List[Dict[str, str]]
untagged_images: List[Dict[str, str]],
) -> None:
"""Classify untagged images and detect faces if applicable."""
object_classifier = ObjectClassifier()
Expand Down Expand Up @@ -262,7 +262,7 @@ def image_util_remove_obsolete_images(folder_id_list: List[int]) -> int:


def image_util_create_folder_path_mapping(
folder_ids: List[Tuple[int, str]]
folder_ids: List[Tuple[int, str]],
) -> Dict[str, int]:
"""
Create a dictionary mapping folder paths to their IDs.
Expand Down
Loading