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
5 changes: 5 additions & 0 deletions .github/workflows/build-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,11 @@ jobs:
unzip -o *.zip
rm *.zip
ls -l
- name: Copy utils folder
shell: bash
run: |
cp -r utils backend/dist/utils
cp -r utils sync-microservice/dist/utils
- name: Setup Node
uses: actions/setup-node@v4
with:
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: "v0.0.241"
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.10
Comment on lines +2 to +3
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

🧩 Analysis chain

🌐 Web query:

latest release astral-sh ruff-pre-commit v0.4.10 current supported

💡 Result:

astral-sh/ruff-pre-commit v0.4.10 is not current.

  • The latest release of astral-sh/ruff-pre-commit is v0.14.14 (Jan 22, 2026). [1]
  • The repository README currently recommends using rev: v0.14.14 in .pre-commit-config.yaml. [2]

So v0.4.10 is outdated; if you want a currently supported/current version, update your rev to v0.14.14. [1][2]

Sources: [1] [2]


🌐 Web query:

latest release psf black 24.4.2 current supported

💡 Result:

  • Latest PSF Black release: 26.1.0 (released Jan 18, 2026). Supported runtime: Python ≥ 3.10 (classifiers include 3.10–3.14). [1][2]
  • Black 24.4.2: released Apr 26, 2024; supported runtime: Python ≥ 3.8 (classifiers 3.8–3.12). It is not the current release. [3]
  • Support changes since 24.4.2: Black dropped Python 3.8 in 24.10.0 and dropped Python 3.9 in 25.12.0 (which is why current Black now requires Python 3.10+). [4]

Sources: [1] [2] [3] [4]


Update pinned tool versions—both are significantly outdated.

  • ruff-pre-commit v0.4.10 is from 2024; the latest release is v0.14.14 (Jan 22, 2026). Update rev to v0.14.14.
  • black 24.4.2 is from April 2024; the latest release is 26.1.0 (Jan 18, 2026). However, note that Black 25.12.0+ requires Python ≥ 3.10 (it dropped Python 3.8 in v24.10.0 and Python 3.9 in v25.12.0). If your project must support Python 3.8 or 3.9, keep 24.4.2; otherwise, update to 26.1.0.

Also applies to: 8-9

🤖 Prompt for AI Agents
In @.pre-commit-config.yaml around lines 2 - 3, Update the pinned pre-commit
tool versions: change the ruff-pre-commit entry's rev from v0.4.10 to v0.14.14
and update the black entry's rev to v26.1.0 unless the project must support
Python 3.8 or 3.9, in which case keep the existing black rev (24.4.2); ensure
you update the rev values for the two repo entries referenced
(https://github.com/astral-sh/ruff-pre-commit and the black pre-commit repo) so
the pre-commit config reflects the latest compatible releases.

hooks:
- id: ruff
args: [--fix]

- repo: https://github.com/psf/black
rev: "22.3.0"
rev: 24.4.2
hooks:
- id: black
language_version: python3
8 changes: 8 additions & 0 deletions COPYRIGHT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Copyright © 2025 AOSSIE <br />
All rights reserved.

All works in this repository may be used according to the conditions
stated in the LICENSE.md file available in this repository.

These works are WITHOUT ANY WARRANTY, without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
595 changes: 595 additions & 0 deletions LICENSE.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# ...existing code...
from utils.cache import invalidate_cache


# Add cache reset option when application starts
def initialize_app():
# ...existing code...
Expand Down
2 changes: 1 addition & 1 deletion backend/app/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
MODEL_EXPORTS_PATH = "app/models/ONNX_Exports"

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

CONFIDENCE_PERCENT = 0.6
# Object Detection Models:
Expand Down
31 changes: 30 additions & 1 deletion backend/app/database/face_clusters.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import sqlite3
from typing import Optional, List, Dict, TypedDict, Union
from typing import Dict, List, Optional, TypedDict, Union

from app.config.settings import DATABASE_PATH

from app.logging.setup_logging import get_logger

logger = get_logger(__name__)
# Type definitions
ClusterId = str
ClusterName = str
Expand Down Expand Up @@ -349,3 +353,28 @@ def db_get_images_by_cluster_id(
return images
finally:
conn.close()


def db_delete_empty_clusters() -> int:
"""
Delete all clusters that have no faces associated with them.
"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

try:
cursor.execute(
"""
DELETE FROM face_clusters
WHERE cluster_id NOT IN (
SELECT DISTINCT cluster_id FROM faces WHERE cluster_id IS NOT NULL
)
"""
)

deleted_count = cursor.rowcount
conn.commit()
logger.info(f"Deleted {deleted_count} empty clusters.")
return deleted_count
finally:
conn.close()
Comment on lines +358 to +380
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

Optimize the subquery and add error handling.

The current implementation has two issues:

  1. Performance concern: The NOT IN subquery can be slow with large datasets because it doesn't leverage indexes efficiently. With many clusters and faces, this could block the database.

  2. Missing error handling: If the query fails, no rollback is performed, potentially leaving the transaction in an inconsistent state.

Apply this diff to improve performance and add error handling:

 def db_delete_empty_clusters() -> int:
     """
     Delete all clusters that have no faces associated with them.
     """
     conn = sqlite3.connect(DATABASE_PATH)
     cursor = conn.cursor()
 
     try:
         cursor.execute(
             """
             DELETE FROM face_clusters
-            WHERE cluster_id NOT IN (
-                SELECT DISTINCT cluster_id FROM faces WHERE cluster_id IS NOT NULL
+            WHERE NOT EXISTS (
+                SELECT 1 FROM faces 
+                WHERE faces.cluster_id = face_clusters.cluster_id
             )
             """
         )
 
         deleted_count = cursor.rowcount
         conn.commit()
         logger.info(f"Deleted {deleted_count} empty clusters.")
         return deleted_count
+    except Exception as e:
+        logger.error(f"Error deleting empty clusters: {e}")
+        conn.rollback()
+        raise
     finally:
         conn.close()

The NOT EXISTS correlated subquery allows the database to use indexes efficiently and short-circuit as soon as a matching face is found.

📝 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
def db_delete_empty_clusters() -> int:
"""
Delete all clusters that have no faces associated with them.
"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
try:
cursor.execute(
"""
DELETE FROM face_clusters
WHERE cluster_id NOT IN (
SELECT DISTINCT cluster_id FROM faces WHERE cluster_id IS NOT NULL
)
"""
)
deleted_count = cursor.rowcount
conn.commit()
logger.info(f"Deleted {deleted_count} empty clusters.")
return deleted_count
finally:
conn.close()
def db_delete_empty_clusters() -> int:
"""
Delete all clusters that have no faces associated with them.
"""
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
try:
cursor.execute(
"""
DELETE FROM face_clusters
WHERE NOT EXISTS (
SELECT 1 FROM faces
WHERE faces.cluster_id = face_clusters.cluster_id
)
"""
)
deleted_count = cursor.rowcount
conn.commit()
logger.info(f"Deleted {deleted_count} empty clusters.")
return deleted_count
except Exception as e:
logger.error(f"Error deleting empty clusters: {e}")
conn.rollback()
raise
finally:
conn.close()
🤖 Prompt for AI Agents
In backend/app/database/face_clusters.py around lines 330 to 352, replace the
current DELETE ... WHERE cluster_id NOT IN (...) with a correlated NOT EXISTS
subquery to improve query planning and index usage (e.g., DELETE FROM
face_clusters fc WHERE NOT EXISTS (SELECT 1 FROM faces f WHERE f.cluster_id =
fc.cluster_id)); also wrap the execute/commit in a try/except block that calls
conn.rollback() on exception, logs the exception details, and re-raises the
error (ensure conn.close() remains in the finally block).

15 changes: 10 additions & 5 deletions backend/app/logging/setup_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,16 @@ def emit(self, record: logging.LogRecord) -> None:
# Create a message that includes the original module in the format
msg = record.getMessage()

# Find the appropriate logger
logger = get_logger(module_name)

# Log the message with our custom formatting
logger.log(record.levelno, f"[uvicorn] {msg}")
record.msg = f"[{module_name}] {msg}"
record.args = ()
# Clear exception / stack info to avoid duplicate traces
record.exc_info = None
record.stack_info = None

root_logger = logging.getLogger()
for handler in root_logger.handlers:
if handler is not self:
handler.handle(record)
Comment on lines +246 to +255
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

Respect handler log levels when re-dispatching records.

Calling handler.handle(record) directly bypasses handler-level filtering, so DEBUG logs can leak into INFO+ handlers. Add a record.levelno >= handler.level guard (or delegate through Logger.callHandlers) to keep log levels consistent.

🛠️ Proposed fix
-        root_logger = logging.getLogger()
-        for handler in root_logger.handlers:
-            if handler is not self:
-                handler.handle(record)
+        root_logger = logging.getLogger()
+        for handler in root_logger.handlers:
+            if handler is self:
+                continue
+            if record.levelno >= handler.level:
+                handler.handle(record)
🤖 Prompt for AI Agents
In `@backend/app/logging/setup_logging.py` around lines 246 - 255, When
re-dispatching the modified LogRecord to other handlers in setup_logging.py,
respect each handler's level instead of calling handler.handle(record)
unconditionally; update the loop over root_logger.handlers to skip self and only
invoke handler.handle(record) when record.levelno >= handler.level (or
equivalently call handler.filter(record) to apply filters), e.g., in the block
iterating root_logger.handlers, add a guard using handler.level and/or
handler.filter before calling handler.handle(record) so handlers don't receive
records below their configured level.



def configure_uvicorn_logging(component_name: str) -> None:
Expand Down
55 changes: 32 additions & 23 deletions backend/app/routes/folders.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,54 @@
from fastapi import APIRouter, HTTPException, status, Depends, Request
import os
from concurrent.futures import ProcessPoolExecutor
from typing import List, Tuple

from fastapi import APIRouter, Depends, HTTPException, Request, status

from app.database.folders import (
db_update_parent_ids_for_subtree,
db_folder_exists,
db_find_parent_folder_id,
db_enable_ai_tagging_batch,
db_disable_ai_tagging_batch,
db_delete_folders_batch,
db_disable_ai_tagging_batch,
db_enable_ai_tagging_batch,
db_find_parent_folder_id,
db_folder_exists,
db_get_all_folder_details,
db_get_direct_child_folders,
db_get_folder_ids_by_path_prefix,
db_get_all_folder_details,
db_update_parent_ids_for_subtree,
)
from app.logging.setup_logging import get_logger
from app.schemas.folders import (
AddFolderData,
AddFolderRequest,
AddFolderResponse,
AddFolderData,
ErrorResponse,
UpdateAITaggingRequest,
UpdateAITaggingResponse,
UpdateAITaggingData,
DeleteFoldersData,
DeleteFoldersRequest,
DeleteFoldersResponse,
DeleteFoldersData,
ErrorResponse,
FolderDetails,
GetAllFoldersData,
GetAllFoldersResponse,
SyncFolderData,
SyncFolderRequest,
SyncFolderResponse,
SyncFolderData,
GetAllFoldersResponse,
GetAllFoldersData,
FolderDetails,
UpdateAITaggingData,
UpdateAITaggingRequest,
UpdateAITaggingResponse,
)
from app.utils.API import API_util_restart_sync_microservice_watcher
from app.utils.face_clusters import (
cluster_util_delete_empty_clusters,
cluster_util_face_clusters_sync,
)
import os
from app.utils.folders import (
folder_util_add_folder_tree,
folder_util_add_multiple_folder_trees,
folder_util_delete_obsolete_folders,
folder_util_get_filesystem_direct_child_folders,
)
from concurrent.futures import ProcessPoolExecutor
from app.utils.images import (
image_util_process_folder_images,
image_util_process_untagged_images,
)
from app.utils.face_clusters import cluster_util_face_clusters_sync
from app.utils.API import API_util_restart_sync_microservice_watcher

# Initialize logger
logger = get_logger(__name__)
Expand Down Expand Up @@ -72,7 +77,8 @@ def post_folder_add_sequence(folder_path: str, folder_id: int):

# Restart sync microservice watcher after processing images
API_util_restart_sync_microservice_watcher()

# Delete empty clusters after folder addition
cluster_util_delete_empty_clusters()
except Exception as e:
logger.error(
f"Error in post processing after folder {folder_path} was added: {e}"
Expand Down Expand Up @@ -121,6 +127,9 @@ def post_sync_folder_sequence(

# Restart sync microservice watcher after processing images
API_util_restart_sync_microservice_watcher()

# Delete empty clusters after folder sync
cluster_util_delete_empty_clusters()
except Exception as e:
logger.error(
f"Error in post processing after folder {folder_path} was synced: {e}"
Expand Down Expand Up @@ -322,7 +331,7 @@ def delete_folders(request: DeleteFoldersRequest):
raise ValueError("No folder IDs provided")

deleted_count = db_delete_folders_batch(request.folder_ids)

cluster_util_delete_empty_clusters()
return DeleteFoldersResponse(
data=DeleteFoldersData(
deleted_count=deleted_count, folder_ids=request.folder_ids
Expand Down
35 changes: 35 additions & 0 deletions backend/app/utils/face_clusters.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,3 +862,38 @@ def _determine_cluster_name(faces_in_cluster: List[Dict]) -> Optional[str]:
most_common_name, _ = name_counts.most_common(1)[0]

return most_common_name


def cluster_util_delete_empty_clusters() -> int:
"""
Delete all clusters that have no faces associated with them.

Returns:
int: Number of clusters deleted
"""
import sqlite3
from app.config.settings import DATABASE_PATH

conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()

try:
cursor.execute(
"""
DELETE FROM face_clusters
WHERE cluster_id NOT IN (
SELECT DISTINCT cluster_id FROM faces WHERE cluster_id IS NOT NULL
)
"""
)

deleted_count = cursor.rowcount
conn.commit()
logger.info(f"Deleted {deleted_count} empty clusters.")
return deleted_count
except Exception as e:
logger.error(f"Error deleting empty clusters: {e}")
conn.rollback()
return 0
finally:
conn.close()
Loading