Skip to content
Open
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
32 changes: 32 additions & 0 deletions backend/app/database/folders.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,3 +438,35 @@ def db_get_direct_child_folders(parent_folder_id: str) -> List[Tuple[str, str]]:
return cursor.fetchall()
finally:
conn.close()


def db_update_folder_tagging_completed(
folder_id: FolderId, completed: bool = True
) -> bool:
"""
Update the taggingCompleted status for a folder.

Args:
folder_id: The folder ID to update
completed: The boolean value to set (default: True)

Returns:
bool: True if update was successful, False otherwise
"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

try:
cursor.execute(
"UPDATE folders SET taggingCompleted = ? WHERE folder_id = ?",
(completed, folder_id),
)

updated = cursor.rowcount > 0
conn.commit()
return updated
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
100 changes: 75 additions & 25 deletions backend/app/utils/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
import uuid
import datetime
import json
import logging
from typing import List, Tuple, Dict, Any, Mapping
from PIL import Image, ExifTags
from pathlib import Path
from collections import defaultdict
from app.utils.webSocket.webSocket import publish_progress_from_thread

from app.config.settings import THUMBNAIL_IMAGES_PATH
from app.database.images import (
Expand All @@ -26,8 +27,6 @@
# GPS EXIF tag constant
GPS_INFO_TAG = 34853

logger = logging.getLogger(__name__)


def image_util_process_folder_images(folder_data: List[Tuple[str, int, bool]]) -> bool:
"""Main function to process images in multiple folders based on provided folder data.
Expand Down Expand Up @@ -81,6 +80,7 @@ def image_util_process_folder_images(folder_data: List[Tuple[str, int, bool]]) -
return True # No images to process is not an error
except Exception as e:
logger.error(f"Error processing folders: {e}")

return False


Expand All @@ -104,36 +104,86 @@ def image_util_process_untagged_images() -> bool:
def image_util_classify_and_face_detect_images(
untagged_images: List[Dict[str, str]],
) -> None:
"""Classify untagged images and detect faces if applicable."""
"""Classify untagged images and detect faces if applicable.

Progress updates are sent via publish_progress_from_thread using folder_id as job_id.
"""
object_classifier = ObjectClassifier()
face_detector = FaceDetector()
try:
for image in untagged_images:
image_path = image["path"]
image_id = image["id"]

# Step 1: Get classes
classes = object_classifier.get_classes(image_path)
# Group images by folder_id (folder_id is a string)
images_by_folder = defaultdict(list)
for image in untagged_images:
folder_id = image.get("folder_id")
if folder_id: # Skip images without folder_id
images_by_folder[folder_id].append(image)

# Step 2: Insert class-image pairs if classes were detected
if len(classes) > 0:
# Create image-class pairs
image_class_pairs = [(image_id, class_id) for class_id in classes]
logger.debug(f"Image-class pairs: {image_class_pairs}")
try:
for folder_id, folder_images in images_by_folder.items():
total = len(folder_images)
last_bucket = -1

for idx, image in enumerate(folder_images, start=1):
image_path = image["path"]
image_id = image["id"]

# Step 1: Get classes
classes = object_classifier.get_classes(image_path)

# Step 2: Insert class-image pairs if classes were detected
if classes:
image_class_pairs = [(image_id, class_id) for class_id in classes]
logger.debug(f"Image-class pairs: {image_class_pairs}")
db_insert_image_classes_batch(image_class_pairs)

# Step 3: Detect faces if "person" class is present
# (assuming class id 0 denotes "person")
if classes and 0 in classes and 0 < classes.count(0) < 7:
face_detector.detect_faces(image_id, image_path)

# Step 4: Update the image status in the database
db_update_image_tagged_status(image_id, True)

percentage = (idx / total) * 100
bucket = int(percentage // 2)

if bucket != last_bucket or idx == total:
publish_progress_from_thread(
{
"job_id": str(folder_id),
"processed": idx,
"total": total,
"percent": round(percentage, 2),
"status": "running",
}
)
last_bucket = bucket

# Insert the pairs into the database
db_insert_image_classes_batch(image_class_pairs)
# Step 5: Mark folder tagging as completed after processing all images
from app.database.folders import db_update_folder_tagging_completed

# Step 3: Detect faces if "person" class is present
if classes and 0 in classes and 0 < classes.count(0) < 7:
face_detector.detect_faces(image_id, image_path)
db_update_folder_tagging_completed(folder_id, True)
logger.info(f"Folder {folder_id} tagging completed")
# Publish final done event after database update
publish_progress_from_thread(
{
"job_id": str(folder_id),
"processed": total,
"total": total,
"percent": 100.0,
"status": "done",
}
)
Comment on lines +165 to +176
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

Add exception handling to prevent one folder's failure from blocking others.

If db_update_folder_tagging_completed fails for one folder, the exception propagates and stops processing of all remaining folders. This cascading failure means that one folder's database issue prevents other (potentially healthy) folders from being tagged and marked complete.

Apply this diff to continue processing remaining folders even if one fails:

-            # Step 5: Mark folder tagging as completed after processing all images
-            from app.database.folders import db_update_folder_tagging_completed
-
-            db_update_folder_tagging_completed(folder_id, True)
-            logger.info(f"Folder {folder_id} tagging completed")
-            # Publish final done event after database update
-            publish_progress_from_thread(
-                {
-                    "job_id": str(folder_id),
-                    "processed": total,
-                    "total": total,
-                    "percent": 100.0,
-                    "status": "done",
-                }
-            )
+            # Step 5: Mark folder tagging as completed after processing all images
+            from app.database.folders import db_update_folder_tagging_completed
+
+            try:
+                db_update_folder_tagging_completed(folder_id, True)
+                logger.info(f"Folder {folder_id} tagging completed")
+                # Publish final done event after database update
+                publish_progress_from_thread(
+                    {
+                        "job_id": str(folder_id),
+                        "processed": total,
+                        "total": total,
+                        "percent": 100.0,
+                        "status": "done",
+                    }
+                )
+            except Exception as e:
+                logger.error(f"Failed to mark folder {folder_id} as completed: {e}")
+                # Publish error status so frontend knows this folder failed
+                publish_progress_from_thread(
+                    {
+                        "job_id": str(folder_id),
+                        "processed": total,
+                        "total": total,
+                        "percent": 100.0,
+                        "status": "error",
+                    }
+                )

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

🤖 Prompt for AI Agents
In backend/app/utils/images.py around lines 165 to 176, wrap the
db_update_folder_tagging_completed and the logger.info call in a try/except so a
failure updating one folder doesn't stop the loop; on exception catch Exception
as e and call logger.exception or logger.error with the folder_id and exception,
then continue processing; ensure publish_progress_from_thread is still invoked
for this folder (use different status like "done" on success and "error" on
failure) so downstream systems get a final event and the loop moves on.


# Step 4: Update the image status in the database
db_update_image_tagged_status(image_id, True)
finally:
# Ensure resources are cleaned up
object_classifier.close()
face_detector.close()
try:
object_classifier.close()
except Exception:
logger.exception("Error closing object_classifier")
try:
face_detector.close()
except Exception:
logger.exception("Error closing face_detector")


def image_util_prepare_image_records(
Expand Down
Loading