Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions backend/app/config/settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Model Exports Path
MODEL_EXPORTS_PATH = "app/models/ONNX_Exports"

# Microservice URLs
SYNC_MICROSERVICE_URL = "http://localhost:8001/api/v1"

Comment on lines +4 to +6
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

Make SYNC_MICROSERVICE_URL configurable via environment.

Avoid hardcoding localhost; allow deployments to override and normalize trailing slash.

-# Microservice URLs
-SYNC_MICROSERVICE_URL = "http://localhost:8001/api/v1"
+# Microservice URLs
+import os
+SYNC_MICROSERVICE_URL = os.getenv("SYNC_MICROSERVICE_URL", "http://localhost:8001/api/v1").rstrip("/")
📝 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
# Microservice URLs
SYNC_MICROSERVICE_URL = "http://localhost:8001/api/v1"
# Microservice URLs
import os
SYNC_MICROSERVICE_URL = os.getenv("SYNC_MICROSERVICE_URL", "http://localhost:8001/api/v1").rstrip("/")
🤖 Prompt for AI Agents
In backend/app/config/settings.py around lines 4 to 6, SYNC_MICROSERVICE_URL is
hardcoded; change it to read from an environment variable (e.g.,
os.getenv("SYNC_MICROSERVICE_URL")) with the current value as a safe default,
and normalize the value to remove any trailing slash so the URL is consistent;
ensure os is imported and add a small helper or inline logic to strip a trailing
slash before assigning the constant.

# Object Detection Models:
SMALL_OBJ_DETECTION_MODEL = f"{MODEL_EXPORTS_PATH}/YOLOv11_Small.onnx"
NANO_OBJ_DETECTION_MODEL = f"{MODEL_EXPORTS_PATH}/YOLOv11_Nano.onnx"
Expand Down
115 changes: 102 additions & 13 deletions backend/app/database/face_clusters.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class ClusterData(TypedDict):

cluster_id: ClusterId
cluster_name: Optional[ClusterName]
face_image_base64: Optional[str]


ClusterMap = Dict[ClusterId, ClusterData]
Expand All @@ -25,7 +26,8 @@ def db_create_clusters_table() -> None:
"""
CREATE TABLE IF NOT EXISTS face_clusters (
cluster_id TEXT PRIMARY KEY,
cluster_name TEXT
cluster_name TEXT,
face_image_base64 TEXT
)
"""
)
Expand Down Expand Up @@ -56,14 +58,15 @@ def db_insert_clusters_batch(clusters: List[ClusterData]) -> List[ClusterId]:
for cluster in clusters:
cluster_id = cluster.get("cluster_id")
cluster_name = cluster.get("cluster_name")
face_image_base64 = cluster.get("face_image_base64")

insert_data.append((cluster_id, cluster_name))
insert_data.append((cluster_id, cluster_name, face_image_base64))
cluster_ids.append(cluster_id)

cursor.executemany(
"""
INSERT INTO face_clusters (cluster_id, cluster_name)
VALUES (?, ?)
INSERT INTO face_clusters (cluster_id, cluster_name, face_image_base64)
VALUES (?, ?, ?)
""",
insert_data,
)
Expand All @@ -88,15 +91,17 @@ def db_get_cluster_by_id(cluster_id: ClusterId) -> Optional[ClusterData]:
cursor = conn.cursor()

cursor.execute(
"SELECT cluster_id, cluster_name FROM face_clusters WHERE cluster_id = ?",
"SELECT cluster_id, cluster_name, face_image_base64 FROM face_clusters WHERE cluster_id = ?",
(cluster_id,),
)

row = cursor.fetchone()
conn.close()

if row:
return ClusterData(cluster_id=row[0], cluster_name=row[1])
return ClusterData(
cluster_id=row[0], cluster_name=row[1], face_image_base64=row[2]
)
return None


Expand All @@ -111,15 +116,19 @@ def db_get_all_clusters() -> List[ClusterData]:
cursor = conn.cursor()

cursor.execute(
"SELECT cluster_id, cluster_name FROM face_clusters ORDER BY cluster_id"
"SELECT cluster_id, cluster_name, face_image_base64 FROM face_clusters ORDER BY cluster_id"
)

rows = cursor.fetchall()
conn.close()

clusters = []
for row in rows:
clusters.append(ClusterData(cluster_id=row[0], cluster_name=row[1]))
clusters.append(
ClusterData(
cluster_id=row[0], cluster_name=row[1], face_image_base64=row[2]
)
)

return clusters

Expand Down Expand Up @@ -190,20 +199,24 @@ def db_get_all_clusters_with_face_counts() -> List[
Dict[str, Union[str, Optional[str], int]]
]:
"""
Retrieve all clusters with their face counts.
Retrieve all clusters with their face counts and stored face images.

Returns:
List of dictionaries containing cluster_id, cluster_name, and face_count
List of dictionaries containing cluster_id, cluster_name, face_count, and face_image_base64
"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

cursor.execute(
"""
SELECT fc.cluster_id, fc.cluster_name, COUNT(f.face_id) as face_count
SELECT
fc.cluster_id,
fc.cluster_name,
COUNT(f.face_id) as face_count,
fc.face_image_base64
FROM face_clusters fc
LEFT JOIN faces f ON fc.cluster_id = f.cluster_id
GROUP BY fc.cluster_id, fc.cluster_name
GROUP BY fc.cluster_id, fc.cluster_name, fc.face_image_base64
ORDER BY fc.cluster_id
"""
)
Expand All @@ -213,8 +226,84 @@ def db_get_all_clusters_with_face_counts() -> List[

clusters = []
for row in rows:
cluster_id, cluster_name, face_count, face_image_base64 = row
clusters.append(
{"cluster_id": row[0], "cluster_name": row[1], "face_count": row[2]}
{
"cluster_id": cluster_id,
"cluster_name": cluster_name,
"face_count": face_count,
"face_image_base64": face_image_base64,
}
)

return clusters


def db_get_images_by_cluster_id(
cluster_id: ClusterId,
) -> List[Dict[str, Union[str, int]]]:
"""
Get all images that contain faces belonging to a specific cluster.

Args:
cluster_id: The ID of the cluster to get images for

Returns:
List of dictionaries containing image data with face information
"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

cursor.execute(
"""
SELECT DISTINCT
i.id as image_id,
i.path as image_path,
i.thumbnailPath as thumbnail_path,
i.metadata,
f.face_id,
f.confidence,
f.bbox
FROM images i
INNER JOIN faces f ON i.id = f.image_id
WHERE f.cluster_id = ?
ORDER BY i.path
""",
(cluster_id,),
)

rows = cursor.fetchall()
conn.close()

images = []
for row in rows:
(
image_id,
image_path,
thumbnail_path,
metadata,
face_id,
confidence,
bbox_json,
) = row

# Parse bbox JSON if it exists
bbox = None
if bbox_json:
import json

bbox = json.loads(bbox_json)

images.append(
{
"image_id": image_id,
"image_path": image_path,
"thumbnail_path": thumbnail_path,
"metadata": metadata,
"face_id": face_id,
"confidence": confidence,
"bbox": bbox,
}
)

return images
24 changes: 24 additions & 0 deletions backend/app/database/folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,30 @@ 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]]
]:
"""
Get all folder details including folder_id, folder_path, parent_folder_id,
last_modified_time, AI_Tagging, and taggingCompleted.
Returns list of tuples with all folder information.
"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

try:
cursor.execute(
"""
SELECT folder_id, folder_path, parent_folder_id, last_modified_time, AI_Tagging, taggingCompleted
FROM folders
ORDER BY folder_path
"""
)
return cursor.fetchall()
finally:
conn.close()


def db_get_direct_child_folders(parent_folder_id: str) -> List[Tuple[str, str]]:
"""
Get all direct child folders (not subfolders) for a given parent folder.
Expand Down
75 changes: 75 additions & 0 deletions backend/app/database/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,81 @@ def db_bulk_insert_images(image_records: List[ImageRecord]) -> bool:
conn.close()


def db_get_all_images() -> List[dict]:
"""
Get all images from the database with their tags.

Returns:
List of dictionaries containing all image data including tags
"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

try:
cursor.execute(
"""
SELECT
i.id,
i.path,
i.folder_id,
i.thumbnailPath,
i.metadata,
i.isTagged,
m.name as tag_name
FROM images i
LEFT JOIN image_classes ic ON i.id = ic.image_id
LEFT JOIN mappings m ON ic.class_id = m.class_id
ORDER BY i.path, m.name
"""
)

results = cursor.fetchall()

# Group results by image ID
images_dict = {}
for (
image_id,
path,
folder_id,
thumbnail_path,
metadata,
is_tagged,
tag_name,
) in results:
if image_id not in images_dict:
images_dict[image_id] = {
"id": image_id,
"path": path,
"folder_id": folder_id,
"thumbnailPath": thumbnail_path,
"metadata": metadata,
"isTagged": bool(is_tagged),
"tags": [],
}

# Add tag if it exists
if tag_name:
images_dict[image_id]["tags"].append(tag_name)

Comment on lines +145 to +148
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

Deduplicate tags to avoid duplicates from joins

If image_classes has duplicate rows or mappings alias the same name, this can add duplicates.

Apply:

-            if tag_name:
-                images_dict[image_id]["tags"].append(tag_name)
+            if tag_name and tag_name not in images_dict[image_id]["tags"]:
+                images_dict[image_id]["tags"].append(tag_name)
📝 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
# Add tag if it exists
if tag_name:
images_dict[image_id]["tags"].append(tag_name)
# Add tag if it exists (and avoid duplicates)
- if tag_name:
if tag_name and tag_name not in images_dict[image_id]["tags"]:
images_dict[image_id]["tags"].append(tag_name)
🤖 Prompt for AI Agents
In backend/app/database/images.py around lines 129 to 132, tag values may be
appended multiple times due to duplicate join rows; change the logic so tags are
deduplicated before adding them — e.g., only append tag_name if it's truthy and
not already present in images_dict[image_id]["tags"], or collect tags into a set
per image (then convert to list) to ensure no duplicates are stored while
preserving existing behavior.

# Convert to list and set tags to None if empty
images = []
for image_data in images_dict.values():
if not image_data["tags"]:
image_data["tags"] = None
images.append(image_data)

# Sort by path
images.sort(key=lambda x: x["path"])

return images

except Exception as e:
print(f"Error getting all images: {e}")
return []
finally:
conn.close()


def db_get_untagged_images() -> List[ImageRecord]:
"""
Find all images that need AI tagging.
Expand Down
13 changes: 11 additions & 2 deletions backend/app/models/FaceDetector.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,20 @@ def detect_faces(self, image_id: int, image_path: str):
return None

boxes, scores, class_ids = self.yolo_detector(img)
print(boxes)
print(f"Detected {len(boxes)} faces in image {image_id}.")

processed_faces, embeddings = [], []
processed_faces, embeddings, bboxes, confidences = [], [], [], []

for box, score in zip(boxes, scores):
if score > self.yolo_detector.conf_threshold:
x1, y1, x2, y2 = map(int, box)

# Create bounding box dictionary in JSON format
bbox = {"x": x1, "y": y1, "width": x2 - x1, "height": y2 - y1}
bboxes.append(bbox)
confidences.append(float(score))

padding = 20
face_img = img[
max(0, y1 - padding) : min(img.shape[0], y2 + padding),
Expand All @@ -45,7 +52,9 @@ def detect_faces(self, image_id: int, image_path: str):
embeddings.append(embedding)

if embeddings:
db_insert_face_embeddings_by_image_id(image_id, embeddings)
db_insert_face_embeddings_by_image_id(
image_id, embeddings, confidence=confidences, bbox=bboxes
)

return {
"ids": f"{class_ids}",
Expand Down
Loading
Loading