Skip to content

Commit da15d93

Browse files
committed
fix: incorrect tagging on memory cards for Dates & Location
- Fix location_name nullable semantics for date vs location classification - Add Weekends memory clustering (backend + frontend) - Add GET /api/memories/weekly-memories endpoint - Update OpenAPI docs
1 parent 9caa450 commit da15d93

8 files changed

Lines changed: 751 additions & 132 deletions

File tree

backend/app/routes/memories.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@
2525
db_get_images_by_date_range,
2626
db_get_images_by_year_month,
2727
)
28-
from app.utils.memory_clustering import MemoryClustering
28+
from app.utils.memory_clustering import (
29+
MemoryClustering,
30+
find_total_location_memories,
31+
generate_clusters_for_weekends,
32+
)
2933
from app.logging.setup_logging import get_logger
3034

3135
# Initialize router and logger
@@ -112,6 +116,12 @@ class LocationsResponse(BaseModel):
112116
locations: List[LocationCluster]
113117

114118

119+
class WeeklyMemoriesResponse(BaseModel):
120+
success: bool
121+
message: str
122+
weekly_memories: List[Dict]
123+
124+
115125
# API Endpoints
116126

117127

@@ -163,7 +173,7 @@ def generate_memories(
163173
)
164174

165175
memories = clustering.cluster_memories(images)
166-
176+
tlm = find_total_location_memories(memories)
167177
# Calculate breakdown
168178
location_count = sum(1 for m in memories if m.get("type") == "location")
169179
date_count = sum(1 for m in memories if m.get("type") == "date")
@@ -177,6 +187,7 @@ def generate_memories(
177187
"memory_count": len(memories),
178188
"image_count": len(images),
179189
"memories": memories,
190+
"total_location": tlm,
180191
},
181192
"success": True,
182193
"message": f"{len(memories)} memories ({location_count} location, {date_count} date)",
@@ -466,3 +477,24 @@ def get_locations(
466477
except Exception:
467478
logger.error("Error getting locations", exc_info=True)
468479
raise HTTPException(status_code=500, detail="Failed to get locations")
480+
481+
482+
@router.get("/weekly-memories", response_model=WeeklyMemoriesResponse)
483+
def get_weekly_memories():
484+
try:
485+
weekly_clusters = generate_clusters_for_weekends()
486+
if weekly_clusters:
487+
return WeeklyMemoriesResponse(
488+
success=True,
489+
message="Weekly cluster created.",
490+
weekly_memories=weekly_clusters,
491+
)
492+
else:
493+
return WeeklyMemoriesResponse(
494+
success=True,
495+
message="Please add images.",
496+
weekly_memories=weekly_clusters,
497+
)
498+
except Exception:
499+
logger.error("Failed to create weekly memories", exc_info=True)
500+
raise HTTPException(status_code=500, detail="Failed to create weekly memories")

backend/app/utils/memory_clustering.py

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515
from typing import List, Dict, Any, Optional
1616
from collections import defaultdict
1717
import hashlib
18-
18+
from math import radians, cos, sin, asin, sqrt
1919
import numpy as np
2020
from sklearn.cluster import DBSCAN
21-
21+
import uuid
2222
from app.logging.setup_logging import get_logger
23+
from app.database.images import db_get_all_images
2324

2425
# Initialize logger
2526
logger = get_logger(__name__)
@@ -80,7 +81,6 @@ def find_nearest_city(
8081
Returns:
8182
City name if within range, None otherwise
8283
"""
83-
from math import radians, cos, sin, asin, sqrt
8484

8585
def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
8686
"""Calculate distance between two points in km using Haversine formula."""
@@ -104,6 +104,16 @@ def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> fl
104104
return nearest_city
105105

106106

107+
# function to count total memories which has location
108+
# this can also be done in tsx.
109+
def find_total_location_memories(data: list) -> int:
110+
tlm = 0 # total location memories
111+
for memory in data:
112+
if memory["location_name"] is not None:
113+
tlm += 1
114+
return tlm
115+
116+
107117
class MemoryClustering:
108118
"""
109119
Clusters images into memories based on location and time proximity.
@@ -385,7 +395,7 @@ def _create_simple_memory(
385395
title = date_obj.strftime("%B %Y")
386396
else:
387397
title = "Undated Photos"
388-
location_name = ""
398+
location_name = None
389399
center_lat = 0
390400
center_lon = 0
391401

@@ -944,3 +954,46 @@ def _generate_memory_id(
944954
hash_input = f"lat:{lat_rounded}|lon:{lon_rounded}"
945955
hash_digest = hashlib.sha256(hash_input.encode()).hexdigest()[:8]
946956
return f"mem_nodate_{hash_digest}"
957+
958+
959+
def generate_clusters_for_weekends() -> List[Dict]:
960+
images = db_get_all_images()
961+
962+
# sort by date
963+
images.sort(key=lambda x: x["metadata"]["date_created"], reverse=True)
964+
965+
weekend_memories = {}
966+
967+
for img in images:
968+
metadata = img.get("metadata")
969+
if not metadata:
970+
continue
971+
date_str = metadata.get("date_created")
972+
if not date_str:
973+
continue
974+
try:
975+
dt = datetime.fromisoformat(date_str)
976+
except (ValueError, TypeError):
977+
continue
978+
979+
# get year and week number
980+
year, week, _ = dt.isocalendar()
981+
982+
week_key = f"{year}-W{week}"
983+
984+
if week_key not in weekend_memories:
985+
weekend_memories[week_key] = {
986+
"mem_id": str(uuid.uuid4()),
987+
"images": [],
988+
"end_date": date_str.split("T")[0],
989+
"start_date": "",
990+
}
991+
image_info = {
992+
"id": img["id"],
993+
"path": img["path"],
994+
"thumbnailPath": img["thumbnailPath"],
995+
}
996+
weekend_memories[week_key]["start_date"] = date_str.split("T")[0]
997+
weekend_memories[week_key]["images"].append(image_info)
998+
999+
return list(weekend_memories.values())

docs/backend/backend_python/openapi.json

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3422,6 +3422,168 @@
34223422
],
34233423
"title": "ErrorResponse"
34243424
},
3425+
"app__schemas__user_preferences__ErrorResponse": {
3426+
"properties": {
3427+
"success": {
3428+
"type": "boolean",
3429+
"title": "Success"
3430+
},
3431+
"error": {
3432+
"type": "string",
3433+
"title": "Error"
3434+
},
3435+
"message": {
3436+
"type": "string",
3437+
"title": "Message"
3438+
}
3439+
},
3440+
"type": "object",
3441+
"required": [
3442+
"success",
3443+
"error",
3444+
"message"
3445+
],
3446+
"title": "ErrorResponse",
3447+
"description": "Error response model"
3448+
},
3449+
"WeeklyMemoriesResponse": {
3450+
"properties": {
3451+
"success": {
3452+
"type": "boolean",
3453+
"title": "Success"
3454+
},
3455+
"message": {
3456+
"type": "string",
3457+
"title": "Message"
3458+
},
3459+
"weekly_memories": {
3460+
"items": {
3461+
"items": { "$ref": "`#/components/schemas/WeeklyMemoryImage`" }
3462+
},
3463+
"type": "array",
3464+
"title": "Weekly Memories"
3465+
}
3466+
},
3467+
"type": "object",
3468+
"required": [
3469+
"success",
3470+
"message",
3471+
"weekly_memories"
3472+
],
3473+
"title": "WeeklyMemoriesResponse"
3474+
},
3475+
"WeeklyMemory": {
3476+
"type": "object",
3477+
"required": ["mem_id", "images", "start_date", "end_date"],
3478+
"properties": {
3479+
"mem_id": { "type": "string", "title": "Memory ID" },
3480+
"images": {
3481+
"type": "array",
3482+
"items": { "$ref": "`#/components/schemas/WeeklyMemoryImage`" }
3483+
},
3484+
"start_date": { "type": "string", "title": "Start Date" },
3485+
"end_date": { "type": "string", "title": "End Date" }
3486+
},
3487+
"title": "WeeklyMemory"
3488+
},
3489+
"WeeklyMemoryImage": {
3490+
"type": "object",
3491+
"required": ["asset_id", "thumbnail_path"],
3492+
"properties": {
3493+
"asset_id": { "type": "string", "title": "Asset ID" },
3494+
"thumbnail_path": { "type": "string", "title": "Thumbnail Path" }
3495+
},
3496+
"title": "WeeklyMemoryImage"
3497+
},
3498+
"app__schemas__face_clusters__ErrorResponse": {
3499+
"properties": {
3500+
"success": {
3501+
"type": "boolean",
3502+
"title": "Success",
3503+
"default": false
3504+
},
3505+
"message": {
3506+
"anyOf": [
3507+
{
3508+
"type": "string"
3509+
},
3510+
{
3511+
"type": "null"
3512+
}
3513+
],
3514+
"title": "Message"
3515+
},
3516+
"error": {
3517+
"anyOf": [
3518+
{
3519+
"type": "string"
3520+
},
3521+
{
3522+
"type": "null"
3523+
}
3524+
],
3525+
"title": "Error"
3526+
}
3527+
},
3528+
"type": "object",
3529+
"title": "ErrorResponse"
3530+
},
3531+
"app__schemas__folders__ErrorResponse": {
3532+
"properties": {
3533+
"success": {
3534+
"type": "boolean",
3535+
"title": "Success",
3536+
"default": false
3537+
},
3538+
"message": {
3539+
"anyOf": [
3540+
{
3541+
"type": "string"
3542+
},
3543+
{
3544+
"type": "null"
3545+
}
3546+
],
3547+
"title": "Message"
3548+
},
3549+
"error": {
3550+
"anyOf": [
3551+
{
3552+
"type": "string"
3553+
},
3554+
{
3555+
"type": "null"
3556+
}
3557+
],
3558+
"title": "Error"
3559+
}
3560+
},
3561+
"type": "object",
3562+
"title": "ErrorResponse"
3563+
},
3564+
"app__schemas__images__ErrorResponse": {
3565+
"properties": {
3566+
"success": {
3567+
"type": "boolean",
3568+
"title": "Success",
3569+
"default": false
3570+
},
3571+
"message": {
3572+
"type": "string",
3573+
"title": "Message"
3574+
},
3575+
"error": {
3576+
"type": "string",
3577+
"title": "Error"
3578+
}
3579+
},
3580+
"type": "object",
3581+
"required": [
3582+
"message",
3583+
"error"
3584+
],
3585+
"title": "ErrorResponse"
3586+
},
34253587
"app__schemas__user_preferences__ErrorResponse": {
34263588
"properties": {
34273589
"success": {

frontend/src/api/api-functions/memories.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export interface Memory {
3030
memory_id: string;
3131
title: string;
3232
description: string;
33-
location_name: string;
33+
location_name: string | null;
3434
date_start: string | null;
3535
date_end: string | null;
3636
image_count: number;
@@ -44,7 +44,7 @@ export interface Memory {
4444
* Location cluster with sample images
4545
*/
4646
export interface LocationCluster {
47-
location_name: string;
47+
location_name: string | null;
4848
center_lat: number;
4949
center_lon: number;
5050
image_count: number;
@@ -127,3 +127,41 @@ export const getLocations = async (options?: {
127127
const response = await apiClient.get<APIResponse>(url);
128128
return response.data;
129129
};
130+
131+
/**
132+
* A single image within a weekend memory cluster
133+
*/
134+
export interface WeeklyMemoryImage {
135+
id: string;
136+
path: string;
137+
thumbnailPath: string;
138+
}
139+
140+
/**
141+
* A single weekend memory cluster returned by the backend
142+
*/
143+
export interface WeeklyMemory {
144+
mem_id: string;
145+
images: WeeklyMemoryImage[];
146+
start_date: string;
147+
end_date: string;
148+
}
149+
150+
/**
151+
* Full response shape from GET /api/memories/weekly-memories
152+
*/
153+
export interface WeeklyMemoriesResponse {
154+
success: boolean;
155+
message: string;
156+
weekly_memories: WeeklyMemory[];
157+
}
158+
159+
/**
160+
* Fetch weekend memory clusters from the backend
161+
*/
162+
export const getWeeklyMemories = async (): Promise<WeeklyMemoriesResponse> => {
163+
const response = await apiClient.get<WeeklyMemoriesResponse>(
164+
memoriesEndpoints.weeklyMemories,
165+
);
166+
return response.data;
167+
};

frontend/src/api/apiEndpoints.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ export const memoriesEndpoints = {
3737
timeline: '/api/memories/timeline',
3838
onThisDay: '/api/memories/on-this-day',
3939
locations: '/api/memories/locations',
40+
weeklyMemories: '/api/memories/weekly-memories',
4041
};

0 commit comments

Comments
 (0)