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
36 changes: 34 additions & 2 deletions backend/app/routes/memories.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@
db_get_images_by_date_range,
db_get_images_by_year_month,
)
from app.utils.memory_clustering import MemoryClustering
from app.utils.memory_clustering import (
MemoryClustering,
find_total_location_memories,
generate_clusters_for_weekends,
)
from app.logging.setup_logging import get_logger

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


class WeeklyMemoriesResponse(BaseModel):
success: bool
message: str
weekly_memories: List[Dict]


# API Endpoints


Expand Down Expand Up @@ -163,7 +173,7 @@ def generate_memories(
)

memories = clustering.cluster_memories(images)

tlm = find_total_location_memories(memories)
# Calculate breakdown
location_count = sum(1 for m in memories if m.get("type") == "location")
date_count = sum(1 for m in memories if m.get("type") == "date")
Expand All @@ -177,6 +187,7 @@ def generate_memories(
"memory_count": len(memories),
"image_count": len(images),
"memories": memories,
"total_location": tlm,
},
"success": True,
"message": f"{len(memories)} memories ({location_count} location, {date_count} date)",
Expand Down Expand Up @@ -466,3 +477,24 @@ def get_locations(
except Exception:
logger.error("Error getting locations", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to get locations")


@router.get("/weekly-memories", response_model=WeeklyMemoriesResponse)
def get_weekly_memories():
try:
weekly_clusters = generate_clusters_for_weekends()
if weekly_clusters:
return WeeklyMemoriesResponse(
success=True,
message="Weekly cluster created.",
weekly_memories=weekly_clusters,
)
else:
return WeeklyMemoriesResponse(
success=True,
message="Please add images.",
weekly_memories=weekly_clusters,
)
except Exception:
logger.error("Failed to create weekly memories", exc_info=True)
raise HTTPException(status_code=500, detail="Failed to create weekly memories")
61 changes: 57 additions & 4 deletions backend/app/utils/memory_clustering.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
from typing import List, Dict, Any, Optional
from collections import defaultdict
import hashlib

from math import radians, cos, sin, asin, sqrt
import numpy as np
from sklearn.cluster import DBSCAN

import uuid
from app.logging.setup_logging import get_logger
from app.database.images import db_get_all_images

# Initialize logger
logger = get_logger(__name__)
Expand Down Expand Up @@ -80,7 +81,6 @@ def find_nearest_city(
Returns:
City name if within range, None otherwise
"""
from math import radians, cos, sin, asin, sqrt

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


# function to count total memories which has location
# this can also be done in tsx.
def find_total_location_memories(data: list) -> int:
tlm = 0 # total location memories
for memory in data:
if memory["location_name"] is not None:
tlm += 1
return tlm


class MemoryClustering:
"""
Clusters images into memories based on location and time proximity.
Expand Down Expand Up @@ -385,7 +395,7 @@ def _create_simple_memory(
title = date_obj.strftime("%B %Y")
else:
title = "Undated Photos"
location_name = ""
location_name = None
center_lat = 0
center_lon = 0

Expand Down Expand Up @@ -944,3 +954,46 @@ def _generate_memory_id(
hash_input = f"lat:{lat_rounded}|lon:{lon_rounded}"
hash_digest = hashlib.sha256(hash_input.encode()).hexdigest()[:8]
return f"mem_nodate_{hash_digest}"


def generate_clusters_for_weekends() -> List[Dict]:
images = db_get_all_images()

# sort by date
images.sort(key=lambda x: x["metadata"]["date_created"], reverse=True)

weekend_memories = {}

for img in images:
metadata = img.get("metadata")
if not metadata:
continue
date_str = metadata.get("date_created")
if not date_str:
continue
try:
dt = datetime.fromisoformat(date_str)
except (ValueError, TypeError):
continue

# get year and week number
year, week, _ = dt.isocalendar()

week_key = f"{year}-W{week}"

if week_key not in weekend_memories:
weekend_memories[week_key] = {
"mem_id": str(uuid.uuid4()),
"images": [],
"end_date": date_str.split("T")[0],
"start_date": "",
}
image_info = {
"id": img["id"],
"path": img["path"],
"thumbnailPath": img["thumbnailPath"],
}
weekend_memories[week_key]["start_date"] = date_str.split("T")[0]
weekend_memories[week_key]["images"].append(image_info)

return list(weekend_memories.values())
162 changes: 162 additions & 0 deletions docs/backend/backend_python/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3422,6 +3422,168 @@
],
"title": "ErrorResponse"
},
"app__schemas__user_preferences__ErrorResponse": {
"properties": {
"success": {
"type": "boolean",
"title": "Success"
},
"error": {
"type": "string",
"title": "Error"
},
"message": {
"type": "string",
"title": "Message"
}
},
"type": "object",
"required": [
"success",
"error",
"message"
],
"title": "ErrorResponse",
"description": "Error response model"
},
"WeeklyMemoriesResponse": {
"properties": {
"success": {
"type": "boolean",
"title": "Success"
},
"message": {
"type": "string",
"title": "Message"
},
"weekly_memories": {
"items": {
"items": { "$ref": "`#/components/schemas/WeeklyMemoryImage`" }
},
"type": "array",
"title": "Weekly Memories"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
"type": "object",
"required": [
"success",
"message",
"weekly_memories"
],
"title": "WeeklyMemoriesResponse"
},
"WeeklyMemory": {
"type": "object",
"required": ["mem_id", "images", "start_date", "end_date"],
"properties": {
"mem_id": { "type": "string", "title": "Memory ID" },
"images": {
"type": "array",
"items": { "$ref": "`#/components/schemas/WeeklyMemoryImage`" }
},
"start_date": { "type": "string", "title": "Start Date" },
"end_date": { "type": "string", "title": "End Date" }
},
"title": "WeeklyMemory"
},
"WeeklyMemoryImage": {
"type": "object",
"required": ["asset_id", "thumbnail_path"],
"properties": {
"asset_id": { "type": "string", "title": "Asset ID" },
"thumbnail_path": { "type": "string", "title": "Thumbnail Path" }
},
"title": "WeeklyMemoryImage"
},
"app__schemas__face_clusters__ErrorResponse": {
"properties": {
"success": {
"type": "boolean",
"title": "Success",
"default": false
},
"message": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Message"
},
"error": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Error"
}
},
"type": "object",
"title": "ErrorResponse"
},
"app__schemas__folders__ErrorResponse": {
"properties": {
"success": {
"type": "boolean",
"title": "Success",
"default": false
},
"message": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Message"
},
"error": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Error"
}
},
"type": "object",
"title": "ErrorResponse"
},
"app__schemas__images__ErrorResponse": {
"properties": {
"success": {
"type": "boolean",
"title": "Success",
"default": false
},
"message": {
"type": "string",
"title": "Message"
},
"error": {
"type": "string",
"title": "Error"
}
},
"type": "object",
"required": [
"message",
"error"
],
"title": "ErrorResponse"
},
"app__schemas__user_preferences__ErrorResponse": {
"properties": {
"success": {
Expand Down
42 changes: 40 additions & 2 deletions frontend/src/api/api-functions/memories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface Memory {
memory_id: string;
title: string;
description: string;
location_name: string;
location_name: string | null;
date_start: string | null;
date_end: string | null;
image_count: number;
Expand All @@ -44,7 +44,7 @@ export interface Memory {
* Location cluster with sample images
*/
export interface LocationCluster {
location_name: string;
location_name: string | null;
center_lat: number;
center_lon: number;
image_count: number;
Expand Down Expand Up @@ -127,3 +127,41 @@ export const getLocations = async (options?: {
const response = await apiClient.get<APIResponse>(url);
return response.data;
};

/**
* A single image within a weekend memory cluster
*/
export interface WeeklyMemoryImage {
id: string;
path: string;
thumbnailPath: string;
}

/**
* A single weekend memory cluster returned by the backend
*/
export interface WeeklyMemory {
mem_id: string;
images: WeeklyMemoryImage[];
start_date: string;
end_date: string;
}

/**
* Full response shape from GET /api/memories/weekly-memories
*/
export interface WeeklyMemoriesResponse {
success: boolean;
message: string;
weekly_memories: WeeklyMemory[];
}

/**
* Fetch weekend memory clusters from the backend
*/
export const getWeeklyMemories = async (): Promise<WeeklyMemoriesResponse> => {
const response = await apiClient.get<WeeklyMemoriesResponse>(
memoriesEndpoints.weeklyMemories,
);
return response.data;
};
1 change: 1 addition & 0 deletions frontend/src/api/apiEndpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ export const memoriesEndpoints = {
timeline: '/api/memories/timeline',
onThisDay: '/api/memories/on-this-day',
locations: '/api/memories/locations',
weeklyMemories: '/api/memories/weekly-memories',
};
Loading
Loading