Skip to content

Commit 4d51dbb

Browse files
authored
Merge branch 'main' into logging
2 parents 22a074a + 589d71f commit 4d51dbb

File tree

21 files changed

+1212
-85
lines changed

21 files changed

+1212
-85
lines changed

backend/app/database/face_clusters.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ def db_get_images_by_cluster_id(
275275
rows = cursor.fetchall()
276276
conn.close()
277277

278+
from app.utils.images import image_util_parse_metadata
279+
278280
images = []
279281
for row in rows:
280282
(
@@ -299,7 +301,7 @@ def db_get_images_by_cluster_id(
299301
"image_id": image_id,
300302
"image_path": image_path,
301303
"thumbnail_path": thumbnail_path,
302-
"metadata": metadata,
304+
"metadata": image_util_parse_metadata(metadata),
303305
"face_id": face_id,
304306
"confidence": confidence,
305307
"bbox": bbox,

backend/app/database/faces.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ def get_all_face_embeddings():
159159
)
160160
results = cursor.fetchall()
161161

162+
from app.utils.images import image_util_parse_metadata
163+
162164
images_dict = {}
163165
for (
164166
embeddings,
@@ -184,7 +186,7 @@ def get_all_face_embeddings():
184186
"path": path,
185187
"folder_id": folder_id,
186188
"thumbnailPath": thumbnail_path,
187-
"metadata": metadata,
189+
"metadata": image_util_parse_metadata(metadata),
188190
"isTagged": bool(is_tagged),
189191
"tags": [],
190192
}

backend/app/database/images.py

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Standard library imports
22
import sqlite3
3-
from typing import List, Tuple, TypedDict
3+
from typing import Any, List, Mapping, Tuple, TypedDict, Union
44

55
# App-specific imports
66
from app.config.settings import (
@@ -25,15 +25,32 @@ class ImageRecord(TypedDict):
2525
path: ImagePath
2626
folder_id: FolderId
2727
thumbnailPath: str
28-
metadata: str
28+
metadata: Union[Mapping[str, Any], str]
2929
isTagged: bool
3030

3131

32+
class UntaggedImageRecord(TypedDict):
33+
"""Represents an image record returned for tagging."""
34+
35+
id: ImageId
36+
path: ImagePath
37+
folder_id: FolderId
38+
thumbnailPath: str
39+
metadata: Mapping[str, Any]
40+
41+
3242
ImageClassPair = Tuple[ImageId, ClassId]
3343

3444

35-
def db_create_images_table() -> None:
45+
def _connect() -> sqlite3.Connection:
3646
conn = sqlite3.connect(DATABASE_PATH)
47+
# Ensure ON DELETE CASCADE and other FKs are enforced
48+
conn.execute("PRAGMA foreign_keys = ON")
49+
return conn
50+
51+
52+
def db_create_images_table() -> None:
53+
conn = _connect()
3754
cursor = conn.cursor()
3855

3956
# Create new images table with merged fields
@@ -73,15 +90,23 @@ def db_bulk_insert_images(image_records: List[ImageRecord]) -> bool:
7390
if not image_records:
7491
return True
7592

76-
conn = sqlite3.connect(DATABASE_PATH)
93+
conn = _connect()
7794
cursor = conn.cursor()
7895

7996
try:
8097
cursor.executemany(
8198
"""
82-
INSERT OR IGNORE INTO images (id, path, folder_id, thumbnailPath, metadata, isTagged)
99+
INSERT INTO images (id, path, folder_id, thumbnailPath, metadata, isTagged)
83100
VALUES (:id, :path, :folder_id, :thumbnailPath, :metadata, :isTagged)
84-
""",
101+
ON CONFLICT(path) DO UPDATE SET
102+
folder_id=excluded.folder_id,
103+
thumbnailPath=excluded.thumbnailPath,
104+
metadata=excluded.metadata,
105+
isTagged=CASE
106+
WHEN excluded.isTagged THEN 1
107+
ELSE images.isTagged
108+
END
109+
""",
85110
image_records,
86111
)
87112
conn.commit()
@@ -101,7 +126,7 @@ def db_get_all_images() -> List[dict]:
101126
Returns:
102127
List of dictionaries containing all image data including tags
103128
"""
104-
conn = sqlite3.connect(DATABASE_PATH)
129+
conn = _connect()
105130
cursor = conn.cursor()
106131

107132
try:
@@ -136,18 +161,23 @@ def db_get_all_images() -> List[dict]:
136161
tag_name,
137162
) in results:
138163
if image_id not in images_dict:
164+
# Safely parse metadata JSON -> dict
165+
from app.utils.images import image_util_parse_metadata
166+
167+
metadata_dict = image_util_parse_metadata(metadata)
168+
139169
images_dict[image_id] = {
140170
"id": image_id,
141171
"path": path,
142-
"folder_id": folder_id,
172+
"folder_id": str(folder_id),
143173
"thumbnailPath": thumbnail_path,
144-
"metadata": metadata,
174+
"metadata": metadata_dict,
145175
"isTagged": bool(is_tagged),
146176
"tags": [],
147177
}
148178

149-
# Add tag if it exists
150-
if tag_name:
179+
# Add tag if it exists (avoid duplicates)
180+
if tag_name and tag_name not in images_dict[image_id]["tags"]:
151181
images_dict[image_id]["tags"].append(tag_name)
152182

153183
# Convert to list and set tags to None if empty
@@ -169,7 +199,7 @@ def db_get_all_images() -> List[dict]:
169199
conn.close()
170200

171201

172-
def db_get_untagged_images() -> List[ImageRecord]:
202+
def db_get_untagged_images() -> List[UntaggedImageRecord]:
173203
"""
174204
Find all images that need AI tagging.
175205
Returns images where:
@@ -179,7 +209,7 @@ def db_get_untagged_images() -> List[ImageRecord]:
179209
Returns:
180210
List of dictionaries containing image data: id, path, folder_id, thumbnailPath, metadata
181211
"""
182-
conn = sqlite3.connect(DATABASE_PATH)
212+
conn = _connect()
183213
cursor = conn.cursor()
184214

185215
try:
@@ -197,13 +227,16 @@ def db_get_untagged_images() -> List[ImageRecord]:
197227

198228
untagged_images = []
199229
for image_id, path, folder_id, thumbnail_path, metadata in results:
230+
from app.utils.images import image_util_parse_metadata
231+
232+
md = image_util_parse_metadata(metadata)
200233
untagged_images.append(
201234
{
202235
"id": image_id,
203236
"path": path,
204-
"folder_id": folder_id,
237+
"folder_id": str(folder_id) if folder_id is not None else None,
205238
"thumbnailPath": thumbnail_path,
206-
"metadata": metadata,
239+
"metadata": md,
207240
}
208241
)
209242

@@ -224,7 +257,7 @@ def db_update_image_tagged_status(image_id: ImageId, is_tagged: bool = True) ->
224257
Returns:
225258
True if update was successful, False otherwise
226259
"""
227-
conn = sqlite3.connect(DATABASE_PATH)
260+
conn = _connect()
228261
cursor = conn.cursor()
229262

230263
try:
@@ -255,7 +288,7 @@ def db_insert_image_classes_batch(image_class_pairs: List[ImageClassPair]) -> bo
255288
if not image_class_pairs:
256289
return True
257290

258-
conn = sqlite3.connect(DATABASE_PATH)
291+
conn = _connect()
259292
cursor = conn.cursor()
260293

261294
try:
@@ -291,7 +324,7 @@ def db_get_images_by_folder_ids(
291324
if not folder_ids:
292325
return []
293326

294-
conn = sqlite3.connect(DATABASE_PATH)
327+
conn = _connect()
295328
cursor = conn.cursor()
296329

297330
try:
@@ -327,7 +360,7 @@ def db_delete_images_by_ids(image_ids: List[ImageId]) -> bool:
327360
if not image_ids:
328361
return True
329362

330-
conn = sqlite3.connect(DATABASE_PATH)
363+
conn = _connect()
331364
cursor = conn.cursor()
332365

333366
try:

backend/app/routes/face_clusters.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
import uuid
33
import os
4-
from typing import Optional, List
4+
from typing import Optional, List, Dict, Any
55
from pydantic import BaseModel
66
from app.config.settings import CONFIDENCE_PERCENT, DEFAULT_FACENET_MODEL
77
from fastapi import APIRouter, HTTPException, status
@@ -42,7 +42,7 @@ class ImageData(BaseModel):
4242
path: str
4343
folder_id: str
4444
thumbnailPath: str
45-
metadata: str
45+
metadata: Dict[str, Any]
4646
isTagged: bool
4747
tags: Optional[List[str]] = None
4848
bboxes: BoundingBox

backend/app/routes/images.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,32 @@
22
from typing import List, Optional
33
from app.database.images import db_get_all_images
44
from app.schemas.images import ErrorResponse
5+
from app.utils.images import image_util_parse_metadata
56
from pydantic import BaseModel
67

78
router = APIRouter()
89

910

1011
# Response Models
12+
class MetadataModel(BaseModel):
13+
name: str
14+
date_created: Optional[str]
15+
width: int
16+
height: int
17+
file_location: str
18+
file_size: int
19+
item_type: str
20+
latitude: Optional[float] = None
21+
longitude: Optional[float] = None
22+
location: Optional[str] = None
23+
24+
1125
class ImageData(BaseModel):
1226
id: str
1327
path: str
1428
folder_id: str
1529
thumbnailPath: str
16-
metadata: str
30+
metadata: MetadataModel
1731
isTagged: bool
1832
tags: Optional[List[str]] = None
1933

@@ -42,7 +56,7 @@ def get_all_images():
4256
path=image["path"],
4357
folder_id=image["folder_id"],
4458
thumbnailPath=image["thumbnailPath"],
45-
metadata=image["metadata"],
59+
metadata=image_util_parse_metadata(image["metadata"]),
4660
isTagged=image["isTagged"],
4761
tags=image["tags"],
4862
)

backend/app/schemas/face_clusters.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pydantic import BaseModel
2-
from typing import List, Optional, Dict, Union
2+
from typing import List, Optional, Dict, Union, Any
33

44

55
# Request Models
@@ -50,7 +50,7 @@ class ImageInCluster(BaseModel):
5050
id: str
5151
path: str
5252
thumbnailPath: Optional[str] = None
53-
metadata: Optional[str] = None
53+
metadata: Optional[Dict[str, Any]] = None
5454
face_id: int
5555
confidence: Optional[float] = None
5656
bbox: Optional[Dict[str, Union[int, float]]] = None

0 commit comments

Comments
 (0)