Skip to content

Commit 03e384b

Browse files
committed
refactor: optimize to single averaged vector, remove unused models, and rename delete route
- Use Cases (attendees.py): Modified EncodeAttendeeUseCase to average 9 augmented encodings using numpy.mean(axis=0) and return a single List[float]. Updated SortAttendeeUseCase to receive and pass the single attendee_encoding. - Repository: Simplified find_matches to accept encoding: List[float] and perform a direct pgvector <=> comparison. Removed the dead get_closest_matches_debug method entirely. - Routers & Schemas (attendees.py, schemas.py): Updated FastAPI endpoints and DTOs to return/receive List[float] (single vector) instead of nested arrays. - Domain: Removed the unused AttendeeProfile model that referenced List[List[float]]. - Events API: Renamed /delete-event-table route to /delete-event-data. Propagated the naming shift across the application by renaming DeleteEventTableUseCase to DeleteEventDataUseCase and updating the corresponding schemas and DI container. - Chore: Added missing type hints to completed_tasks in background_tasks.py. - Docs: Corrected Markdown formatting and updated data flow diagrams in workflows.md.
1 parent 1c98743 commit 03e384b

13 files changed

Lines changed: 194 additions & 175 deletions

File tree

‎main_api/application/dtos.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class EncodedCountDTO:
2929

3030

3131
@dataclass
32-
class DeleteTableDTO:
32+
class DeleteDataDTO:
3333
success: bool
3434
message: Optional[str] = None
3535
table_name: Optional[str] = None

‎main_api/application/ports/repository.py‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List, Set, Dict, Any, Protocol
1+
from typing import List, Set, Protocol
22
from application.dtos import EventEncodingDTO
33

44

@@ -21,13 +21,7 @@ async def delete_event_data(self, event_code: str) -> None:
2121
async def find_matches(
2222
self,
2323
event_code: str,
24-
encodings: List[List[float]],
24+
encoding: List[float],
2525
threshold: float,
26-
min_matches: int,
2726
) -> List[str]:
2827
pass
29-
30-
async def get_closest_matches_debug(
31-
self, event_code: str, encodings: List[List[float]], limit: int = 5
32-
) -> List[Dict[str, Any]]:
33-
pass

‎main_api/application/use_cases/attendees.py‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import numpy as np
12
from typing import List
23
import asyncio
34
from application.ports.inference import IInferenceService
@@ -21,7 +22,7 @@ def __init__(
2122
self.inference_service = inference_service
2223
self.augmenter = augmenter
2324

24-
async def execute(self, attendee_images_base64: List[str]) -> List[List[float]]:
25+
async def execute(self, attendee_images_base64: List[str]) -> List[float]:
2526
if len(attendee_images_base64) != 3:
2627
raise InvalidReferenceImagesError(
2728
"Must provide exactly 3 attendee images (front, left, right)."
@@ -43,20 +44,19 @@ async def execute(self, attendee_images_base64: List[str]) -> List[List[float]]:
4344
"Could not detect clear faces in the provided and augmented reference images."
4445
)
4546

46-
return embeddings_list
47+
avg_embedding = np.mean(embeddings_list, axis=0).tolist()
48+
return avg_embedding
4749

4850

4951
class SortAttendeeUseCase:
5052
def __init__(self, uow: IUnitOfWork):
5153
self.uow = uow
5254

5355
async def execute(
54-
self, event_code: str, attendee_encodings: List[List[float]]
56+
self, event_code: str, attendee_encoding: List[float]
5557
) -> AttendeeSortDTO:
56-
if len(attendee_encodings) == 0:
57-
raise InvalidReferenceImagesError(
58-
"Must provide at least one attendee encoding."
59-
)
58+
if not attendee_encoding:
59+
raise InvalidReferenceImagesError("Must provide a valid attendee encoding.")
6060

6161
async with self.uow as uow:
6262
has_data = await uow.event_repo.check_event_has_data(event_code)
@@ -67,9 +67,8 @@ async def execute(
6767

6868
matched_paths = await uow.event_repo.find_matches(
6969
event_code,
70-
attendee_encodings,
70+
attendee_encoding,
7171
settings.SIMILARITY_THRESHOLD,
72-
settings.MIN_MATCHES,
7372
)
7473

7574
if not matched_paths:

‎main_api/application/use_cases/background_tasks.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ async def execute(
158158
task_ids.append(tid)
159159

160160
import asyncio
161-
completed_tasks = set()
161+
162+
completed_tasks: set[str] = set()
162163
while len(completed_tasks) < len(task_ids):
163164
for tid in task_ids:
164165
if tid not in completed_tasks:
@@ -168,8 +169,12 @@ async def execute(
168169

169170
processed_batches = len(completed_tasks)
170171
processed_images = min(processed_batches * batch_size, total_images)
171-
pct = int((processed_images / total_images) * 100) if total_images > 0 else 100
172-
172+
pct = (
173+
int((processed_images / total_images) * 100)
174+
if total_images > 0
175+
else 100
176+
)
177+
173178
update_state_cb(
174179
"PROCESSING",
175180
{

‎main_api/application/use_cases/events.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from application.ports.queue import ITaskQueueService
22
from application.ports.uow import IUnitOfWork
3-
from application.dtos import EncodedCountDTO, DeleteTableDTO
3+
from application.dtos import EncodedCountDTO, DeleteDataDTO
44

55

66
class StartEventEncodingUseCase:
@@ -40,15 +40,15 @@ async def execute(self, event_code: str) -> EncodedCountDTO:
4040
return EncodedCountDTO(encoded_count=count, table_exists=True)
4141

4242

43-
class DeleteEventTableUseCase:
43+
class DeleteEventDataUseCase:
4444
def __init__(self, queue_service: ITaskQueueService):
4545
self.queue_service = queue_service
4646

4747
async def execute(
4848
self, event_code: str, event_id: str | None = None
49-
) -> DeleteTableDTO:
49+
) -> DeleteDataDTO:
5050
self.queue_service.enqueue_delete_event(event_code, event_id)
51-
return DeleteTableDTO(
51+
return DeleteDataDTO(
5252
success=True,
5353
message=f"Enqueued deletion task for event '{event_code}'.",
5454
)

‎main_api/domain/models.py‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@ class MatchResult:
1515
best_distance: float
1616

1717

18-
@dataclass
19-
class AttendeeProfile:
20-
encodings: List[List[float]]
21-
22-
2318
@dataclass
2419
class Event:
2520
folder_path: str

‎main_api/infrastructure/database/repository.py‎

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,11 @@
88
delete,
99
func,
1010
literal_column,
11-
values,
12-
column,
13-
Integer,
14-
String,
1511
cast,
1612
Float,
1713
)
1814
from pgvector.sqlalchemy import Vector
19-
from typing import List, Set, Dict, Any
15+
from typing import List, Set
2016
import dataclasses
2117

2218

@@ -64,72 +60,23 @@ async def delete_event_data(self, event_code: str) -> None:
6460
async def find_matches(
6561
self,
6662
event_code: str,
67-
encodings: List[List[float]],
63+
encoding: List[float],
6864
threshold: float,
69-
min_matches: int,
7065
) -> List[str]:
71-
ref_encodings = (
72-
values(
73-
column("id", Integer), column("embedding", String), name="ref_encodings"
74-
)
75-
.data([(i + 1, str(emb)) for i, emb in enumerate(encodings)])
76-
.cte("ref_encodings")
77-
)
78-
7966
distance_op = EventEncodingModel.embedding.op("<=>", return_type=Float())(
80-
cast(ref_encodings.c.embedding, Vector(512))
67+
cast(str(encoding), Vector(512))
8168
)
8269

8370
stmt = (
8471
select(
8572
EventEncodingModel.image_path,
86-
func.count(ref_encodings.c.id).label("match_count"),
8773
func.min(distance_op).label("best_distance"),
8874
)
89-
.join(ref_encodings, literal_column("true"))
9075
.where(EventEncodingModel.event_code == event_code, distance_op < threshold)
9176
.group_by(EventEncodingModel.image_path)
92-
.having(func.count(ref_encodings.c.id) >= min_matches)
93-
.order_by(
94-
literal_column("match_count").desc(),
95-
literal_column("best_distance").asc(),
96-
)
77+
.order_by(literal_column("best_distance").asc())
9778
)
9879

9980
result = await self.session.execute(stmt)
10081
rows = result.all()
10182
return [row[0] for row in rows]
102-
103-
async def get_closest_matches_debug(
104-
self, event_code: str, encodings: List[List[float]], limit: int = 5
105-
) -> List[Dict[str, Any]]:
106-
ref_encodings = (
107-
values(
108-
column("id", Integer), column("embedding", String), name="ref_encodings"
109-
)
110-
.data([(i + 1, str(emb)) for i, emb in enumerate(encodings)])
111-
.cte("ref_encodings")
112-
)
113-
114-
distance_op = EventEncodingModel.embedding.op("<=>", return_type=Float())(
115-
cast(ref_encodings.c.embedding, Vector(512))
116-
)
117-
118-
stmt = (
119-
select(
120-
EventEncodingModel.image_path,
121-
func.count(ref_encodings.c.id).label("match_count"),
122-
func.min(distance_op).label("best_distance"),
123-
)
124-
.join(ref_encodings, literal_column("true"))
125-
.where(EventEncodingModel.event_code == event_code)
126-
.group_by(EventEncodingModel.image_path)
127-
.order_by(literal_column("best_distance").asc())
128-
.limit(limit)
129-
)
130-
131-
result = await self.session.execute(stmt)
132-
return [
133-
{"image_path": row[0], "match_count": row[1], "best_distance": row[2]}
134-
for row in result.all()
135-
]

‎main_api/infrastructure/di_container.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
StartEventEncodingUseCase,
44
CheckEncodingStatusUseCase,
55
GetEncodedCountUseCase,
6-
DeleteEventTableUseCase,
6+
DeleteEventDataUseCase,
77
)
88
from application.use_cases.attendees import (
99
EncodeAttendeeUseCase,
@@ -84,8 +84,8 @@ class Container(containers.DeclarativeContainer):
8484

8585
get_encoded_count_use_case = providers.Factory(GetEncodedCountUseCase, uow=uow)
8686

87-
delete_event_table_use_case = providers.Factory(
88-
DeleteEventTableUseCase, queue_service=queue_service
87+
delete_event_data_use_case = providers.Factory(
88+
DeleteEventDataUseCase, queue_service=queue_service
8989
)
9090

9191
encode_attendee_use_case = providers.Factory(

‎main_api/infrastructure/storage/minio_service.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ async def create_zip_from_images(
7676
) -> None:
7777
session = self._get_session()
7878
total = len(image_paths)
79-
s3_config = Config(signature_version="s3v4", max_pool_connections=10, retries={"max_attempts": 0})
79+
s3_config = Config(
80+
signature_version="s3v4",
81+
max_pool_connections=10,
82+
retries={"max_attempts": 0},
83+
)
8084

8185
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
8286
tmp_path = tmp.name

‎main_api/presentation/api/routers/attendees.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ async def encode_attendee(
2929
Provide[Container.encode_attendee_use_case]
3030
),
3131
):
32-
embeddings_list = await use_case.execute(request.attendee_images_base64)
32+
encoding = await use_case.execute(request.attendee_images_base64)
3333
return EncodeAttendeeResponse(
34-
message=f"Successfully generated {len(embeddings_list)} encodings from 3 reference images.",
35-
encodings=embeddings_list,
34+
message="Successfully generated 1 averaged encoding from 3 reference images.",
35+
encoding=encoding,
3636
)
3737

3838

@@ -42,7 +42,7 @@ async def sort_event_attendee(
4242
request: SortAttendeeRequest,
4343
use_case: SortAttendeeUseCase = Depends(Provide[Container.sort_attendee_use_case]),
4444
):
45-
dto = await use_case.execute(request.event_code, request.attendee_encodings)
45+
dto = await use_case.execute(request.event_code, request.attendee_encoding)
4646
return AttendeeSortResponse(**dataclasses.asdict(dto))
4747

4848

0 commit comments

Comments
 (0)