AdarshDRC commited on
Commit
8c901de
·
verified ·
1 Parent(s): 79ad95a

Delete src/db.py

Browse files
Files changed (1) hide show
  1. src/db.py +0 -356
src/db.py DELETED
@@ -1,356 +0,0 @@
1
- """
2
- src/db.py — All external service I/O lives here.
3
-
4
- Two responsibilities:
5
- 1. PineconePool — LRU cache of Pinecone client objects (avoids per-request TCP overhead)
6
- 2. Cloudinary helpers — thin wrappers that always inject credentials explicitly
7
- (stateless; no global cloudinary.config() call needed)
8
-
9
- Search helpers (search_faces, search_objects) live here too so main.py
10
- stays thin and the query logic is testable in isolation.
11
- """
12
-
13
- import io
14
- from collections import OrderedDict
15
- from typing import Optional
16
-
17
- import cloudinary
18
- import cloudinary.api
19
- import cloudinary.uploader
20
- from pinecone import Pinecone, ServerlessSpec
21
-
22
- from .config import (
23
- PINECONE_POOL_MAX,
24
- IDX_FACES, IDX_OBJECTS,
25
- IDX_FACES_DIM, IDX_OBJECTS_DIM,
26
- FACE_THRESHOLD_HIGH, FACE_THRESHOLD_LOW, FACE_DET_SCORE_HQ_SPLIT,
27
- FACE_TOP_K_FETCH,
28
- OBJECT_SCORE_THRESHOLD, OBJECT_TOP_K,
29
- )
30
- from .utils import to_list, face_ui_score
31
-
32
-
33
- # ════════════════════════════════════════════════════════════════════
34
- # PINECONE LRU CONNECTION POOL
35
- # ════════════════════════════════════════════════════════════════════
36
-
37
- class PineconePool:
38
- """
39
- LRU cache of Pinecone() client objects keyed by API key.
40
-
41
- Why pool? Creating a Pinecone() client opens a TCP connection and
42
- authenticates — adds ~200 ms on first use. Re-using clients across
43
- requests eliminates that overhead for repeat users.
44
-
45
- Implementation: OrderedDict + move_to_end = O(1) LRU without any
46
- extra dependency. On overflow, popitem(last=False) evicts the
47
- least-recently-used entry.
48
- """
49
-
50
- def __init__(self, maxsize: int = PINECONE_POOL_MAX):
51
- self._pool: OrderedDict[str, Pinecone] = OrderedDict()
52
- self._maxsize = maxsize
53
-
54
- def get(self, api_key: str) -> Pinecone:
55
- if api_key not in self._pool:
56
- if len(self._pool) >= self._maxsize:
57
- self._pool.popitem(last=False)
58
- self._pool[api_key] = Pinecone(api_key=api_key)
59
- self._pool.move_to_end(api_key)
60
- return self._pool[api_key]
61
-
62
-
63
- # Module-level singleton — shared across all requests
64
- pinecone_pool = PineconePool()
65
-
66
-
67
- # ════════════════════════════════════════════════════════════════════
68
- # CLOUDINARY WRAPPERS
69
- # All functions accept a `creds` dict — no global state.
70
- # Call via asyncio.to_thread() from async endpoints.
71
- # ════════════════════════════════════════════════════════════════════
72
-
73
- def cld_upload(file_obj: io.BytesIO, folder: str, creds: dict) -> dict:
74
- """Upload an in-memory file to Cloudinary. Returns the full API response."""
75
- return cloudinary.uploader.upload(
76
- file_obj, folder=folder,
77
- api_key=creds["api_key"], api_secret=creds["api_secret"],
78
- cloud_name=creds["cloud_name"],
79
- )
80
-
81
-
82
- def cld_ping(creds: dict) -> dict:
83
- """Lightweight connectivity check — verifies credentials are valid."""
84
- return cloudinary.api.ping(
85
- api_key=creds["api_key"], api_secret=creds["api_secret"],
86
- cloud_name=creds["cloud_name"],
87
- )
88
-
89
-
90
- def cld_root_folders(creds: dict) -> dict:
91
- """List all top-level folders in the account."""
92
- return cloudinary.api.root_folders(
93
- api_key=creds["api_key"], api_secret=creds["api_secret"],
94
- cloud_name=creds["cloud_name"],
95
- )
96
-
97
-
98
- def cld_list_folder_images(
99
- folder: str,
100
- creds: dict,
101
- next_cursor: Optional[str] = None,
102
- max_results: int = 100,
103
- ) -> dict:
104
- """Paginated listing of resources under a folder prefix."""
105
- kwargs = dict(
106
- type="upload",
107
- prefix=f"{folder}/",
108
- max_results=min(max_results, 100), # Cloudinary hard-caps at 100
109
- api_key=creds["api_key"],
110
- api_secret=creds["api_secret"],
111
- cloud_name=creds["cloud_name"],
112
- )
113
- if next_cursor:
114
- kwargs["next_cursor"] = next_cursor
115
- return cloudinary.api.resources(**kwargs)
116
-
117
-
118
- def cld_delete_resource(public_id: str, creds: dict) -> dict:
119
- """Delete a single resource by public_id."""
120
- return cloudinary.uploader.destroy(
121
- public_id,
122
- api_key=creds["api_key"], api_secret=creds["api_secret"],
123
- cloud_name=creds["cloud_name"],
124
- )
125
-
126
-
127
- def cld_delete_folder_resources(folder: str, creds: dict) -> dict:
128
- """Delete all resources under a folder prefix (not the folder record itself)."""
129
- return cloudinary.api.delete_resources_by_prefix(
130
- f"{folder}/",
131
- api_key=creds["api_key"], api_secret=creds["api_secret"],
132
- cloud_name=creds["cloud_name"],
133
- )
134
-
135
-
136
- def cld_remove_folder(folder: str, creds: dict):
137
- """Delete the folder record itself. Silently ignores errors (already gone)."""
138
- try:
139
- return cloudinary.api.delete_folder(
140
- folder,
141
- api_key=creds["api_key"], api_secret=creds["api_secret"],
142
- cloud_name=creds["cloud_name"],
143
- )
144
- except Exception:
145
- pass
146
-
147
-
148
- def cld_delete_all_paginated(creds: dict) -> int:
149
- """
150
- Delete ALL resources in a Cloudinary account, paginating in batches of 100.
151
- Returns the total number of resources deleted.
152
- Used only by the destructive /api/reset-database and /api/delete-account endpoints.
153
- """
154
- deleted = 0
155
- while True:
156
- try:
157
- res = cloudinary.api.resources(
158
- type="upload", max_results=100,
159
- api_key=creds["api_key"], api_secret=creds["api_secret"],
160
- cloud_name=creds["cloud_name"],
161
- )
162
- resources = res.get("resources", [])
163
- if not resources:
164
- break
165
- public_ids = [r["public_id"] for r in resources]
166
- cloudinary.api.delete_resources(
167
- public_ids,
168
- api_key=creds["api_key"], api_secret=creds["api_secret"],
169
- cloud_name=creds["cloud_name"],
170
- )
171
- deleted += len(public_ids)
172
- if not res.get("next_cursor"):
173
- break
174
- except Exception:
175
- break
176
- return deleted
177
-
178
-
179
- # ════════════════════════════════════════════════════════════════════
180
- # PINECONE INDEX MANAGEMENT
181
- # ════════════════════════════════════════════════════════════════════
182
-
183
- def ensure_indexes(pc: Pinecone) -> list[str]:
184
- """
185
- Create enterprise-faces and enterprise-objects indexes if they don't exist.
186
- Returns a list of index names that were freshly created (empty list if both existed).
187
- """
188
- existing = {idx.name for idx in pc.list_indexes()}
189
- created = []
190
- spec = ServerlessSpec(cloud="aws", region="us-east-1")
191
-
192
- if IDX_OBJECTS not in existing:
193
- pc.create_index(name=IDX_OBJECTS, dimension=IDX_OBJECTS_DIM, metric="cosine", spec=spec)
194
- created.append(IDX_OBJECTS)
195
-
196
- if IDX_FACES not in existing:
197
- pc.create_index(name=IDX_FACES, dimension=IDX_FACES_DIM, metric="cosine", spec=spec)
198
- created.append(IDX_FACES)
199
-
200
- return created
201
-
202
-
203
- def delete_and_recreate_indexes(pc: Pinecone):
204
- """
205
- Destroy both indexes and recreate them empty.
206
- Used by /api/reset-database. Caller is responsible for ensuring
207
- this is not called on the shared demo database.
208
- """
209
- import asyncio, time
210
- existing = {idx.name for idx in pc.list_indexes()}
211
- for name in [IDX_OBJECTS, IDX_FACES]:
212
- if name in existing:
213
- pc.delete_index(name)
214
- time.sleep(3) # Pinecone needs a moment to fully delete before recreating
215
- spec = ServerlessSpec(cloud="aws", region="us-east-1")
216
- pc.create_index(name=IDX_OBJECTS, dimension=IDX_OBJECTS_DIM, metric="cosine", spec=spec)
217
- pc.create_index(name=IDX_FACES, dimension=IDX_FACES_DIM, metric="cosine", spec=spec)
218
-
219
-
220
- # ════════════════════════════════════════════════════════════════════
221
- # PINECONE SEARCH HELPERS
222
- # ════════════════════════════════════════════════════════════════════
223
-
224
- def search_faces(idx_face, vec: list, det_score: float) -> dict:
225
- """
226
- Query the faces index for one face vector.
227
-
228
- Threshold selection:
229
- - High-quality face (det_score >= FACE_DET_SCORE_HQ_SPLIT) → FACE_THRESHOLD_HIGH
230
- - Lower-quality face → FACE_THRESHOLD_LOW
231
- Adaptive thresholding prevents low-quality query faces from flooding results
232
- with false positives while still matching when confidence warrants it.
233
-
234
- Returns: { image_url → { raw_score, face_crop, folder, face_width_px } }
235
- De-duplicated per image URL — only the highest-scoring face match per image
236
- is retained (multiple face vectors can be stored per source image during upload).
237
- """
238
- threshold = (
239
- FACE_THRESHOLD_HIGH
240
- if det_score >= FACE_DET_SCORE_HQ_SPLIT
241
- else FACE_THRESHOLD_LOW
242
- )
243
- response = idx_face.query(vector=vec, top_k=FACE_TOP_K_FETCH, include_metadata=True)
244
- image_map = {}
245
-
246
- for match in response.get("matches", []):
247
- raw = match["score"]
248
- if raw < threshold:
249
- continue
250
- url = match["metadata"].get("url", "")
251
- if not url:
252
- continue
253
- if url not in image_map or raw > image_map[url]["raw_score"]:
254
- image_map[url] = {
255
- "raw_score": raw,
256
- "face_crop": match["metadata"].get("face_crop", ""),
257
- "folder": match["metadata"].get("folder", ""),
258
- "face_width_px": int(match["metadata"].get("face_width_px", 0)),
259
- }
260
- return image_map
261
-
262
-
263
- def search_objects(idx_obj, vec: list) -> list:
264
- """
265
- Query the objects index for one object vector.
266
-
267
- Returns list of { url, score, folder } dicts for matches above
268
- OBJECT_SCORE_THRESHOLD, sorted by score descending.
269
- """
270
- response = idx_obj.query(vector=vec, top_k=OBJECT_TOP_K, include_metadata=True)
271
- results = []
272
- for match in response.get("matches", []):
273
- if match["score"] < OBJECT_SCORE_THRESHOLD:
274
- continue
275
- url = match["metadata"].get("url", "")
276
- if url:
277
- results.append({
278
- "url": url,
279
- "score": round(match["score"], 4),
280
- "folder": match["metadata"].get("folder", ""),
281
- })
282
- return sorted(results, key=lambda x: x["score"], reverse=True)
283
-
284
-
285
- def merge_face_results(raw_groups: list[dict]) -> list[dict]:
286
- """
287
- Merge per-face search results from multiple query faces into one ranked list.
288
-
289
- Algorithm:
290
- 1. Build a global image_url → {best_score, matched_face_indices, ...} map.
291
- 2. An image is included if ANY query face matched it.
292
- 3. Score = best raw score across all matching faces, remapped to UI score.
293
- 4. Multi-face boost: +5 % per additional matched face (capped at 0.99).
294
- Rationale: an image containing 3 of your 3 searched people should rank
295
- above one containing only 1 of 3.
296
-
297
- Args:
298
- raw_groups: list of dicts, each containing '_image_map' from search_faces()
299
-
300
- Returns: sorted list of merged result dicts (best first), max 20 items.
301
- """
302
- global_map: dict[str, dict] = {}
303
-
304
- for gi, group in enumerate(raw_groups):
305
- for url, d in group["_image_map"].items():
306
- raw = d["raw_score"]
307
- if url not in global_map:
308
- global_map[url] = {
309
- "raw_score": raw,
310
- "face_crop": d["face_crop"],
311
- "folder": d["folder"],
312
- "face_width_px": d["face_width_px"],
313
- "matched_faces": [gi],
314
- }
315
- else:
316
- entry = global_map[url]
317
- entry["matched_faces"].append(gi)
318
- if raw > entry["raw_score"]:
319
- entry["raw_score"] = raw
320
- entry["face_crop"] = d["face_crop"]
321
- entry["face_width_px"] = d["face_width_px"]
322
-
323
- results = []
324
- for url, d in global_map.items():
325
- n = len(d["matched_faces"])
326
- results.append({
327
- "url": url,
328
- "score": face_ui_score(d["raw_score"], n),
329
- "raw_score": round(d["raw_score"], 4),
330
- "face_crop": d["face_crop"],
331
- "folder": d["folder"],
332
- "face_width_px": d["face_width_px"],
333
- "matched_faces": d["matched_faces"],
334
- "caption": f"👥 {n} faces matched" if n > 1 else "👤 Verified Identity",
335
- })
336
-
337
- return sorted(results, key=lambda x: x["score"], reverse=True)[:20]
338
-
339
-
340
- def merge_object_results(nested: list[list[dict]]) -> list[dict]:
341
- """
342
- Merge object search results from multiple crops into one ranked list.
343
-
344
- When YOLO produces N crops, each gets its own embedding and Pinecone query.
345
- The same image URL may appear in multiple crop results; only the highest
346
- score per URL is kept.
347
-
348
- Returns: sorted list, max OBJECT_TOP_K items.
349
- """
350
- seen: dict[str, dict] = {}
351
- for results in nested:
352
- for r in results:
353
- url = r["url"]
354
- if url and (url not in seen or r["score"] > seen[url]["score"]):
355
- seen[url] = r
356
- return sorted(seen.values(), key=lambda x: x["score"], reverse=True)[:OBJECT_TOP_K]