AdarshDRC commited on
Commit
dbb5852
·
verified ·
1 Parent(s): 5add2a4

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +70 -130
main.py CHANGED
@@ -17,6 +17,7 @@ import cloudinary.uploader
17
  import cloudinary.api
18
  from pinecone import Pinecone, ServerlessSpec
19
 
 
20
  ai = None
21
  p = inflect.engine()
22
 
@@ -26,10 +27,9 @@ _inference_sem: asyncio.Semaphore
26
  _pinecone_pool = OrderedDict()
27
  _POOL_MAX = 64
28
 
29
- IDX_FACES = "enterprise-faces"
30
  IDX_OBJECTS = "enterprise-objects"
31
 
32
- # ── Pinecone pool (keyed by api_key — safe, each key = one account) ──
33
  def _get_pinecone(api_key: str) -> Pinecone:
34
  if api_key not in _pinecone_pool:
35
  if len(_pinecone_pool) >= _POOL_MAX:
@@ -38,52 +38,38 @@ def _get_pinecone(api_key: str) -> Pinecone:
38
  _pinecone_pool.move_to_end(api_key)
39
  return _pinecone_pool[api_key]
40
 
41
- # ── Cloudinary: NO global config, NO pool.
42
- # Credentials are injected per-call via keyword args.
43
- # This is the ONLY correct pattern when multiple users share one server process.
44
  def _cld_upload(tmp_path: str, folder: str, creds: dict):
45
  return cloudinary.uploader.upload(
46
- tmp_path,
47
- folder=folder,
48
- api_key=creds["api_key"],
49
- api_secret=creds["api_secret"],
50
- cloud_name=creds["cloud_name"],
51
  )
52
 
53
  def _cld_ping(creds: dict):
54
  return cloudinary.api.ping(
55
- api_key=creds["api_key"],
56
- api_secret=creds["api_secret"],
57
- cloud_name=creds["cloud_name"],
58
  )
59
 
60
  def _cld_root_folders(creds: dict):
61
  return cloudinary.api.root_folders(
62
- api_key=creds["api_key"],
63
- api_secret=creds["api_secret"],
64
- cloud_name=creds["cloud_name"],
65
  )
66
 
67
- # ─────────────────────────────────────────────────────────────────
68
  @asynccontextmanager
69
  async def lifespan(app: FastAPI):
70
  global ai, _inference_sem
71
  from src.models import AIModelManager
72
- print("Loading AI models...")
 
73
  loop = asyncio.get_event_loop()
74
  ai = await loop.run_in_executor(None, AIModelManager)
75
  _inference_sem = asyncio.Semaphore(MAX_CONCURRENT_INFERENCES)
76
- print("Ready!")
77
  yield
78
 
79
  app = FastAPI(lifespan=lifespan)
80
- app.add_middleware(
81
- CORSMiddleware,
82
- allow_origins=["*"],
83
- allow_credentials=True,
84
- allow_methods=["*"],
85
- allow_headers=["*"],
86
- )
87
  os.makedirs("temp_uploads", exist_ok=True)
88
 
89
  def standardize_category_name(name: str) -> str:
@@ -98,80 +84,60 @@ def get_cloudinary_creds(env_url: str) -> dict:
98
  if not env_url:
99
  return {}
100
  parsed = urlparse(env_url)
101
- return {
102
- "api_key": parsed.username,
103
- "api_secret": parsed.password,
104
- "cloud_name": parsed.hostname,
105
- }
106
-
107
- def require_cloudinary_creds(creds: dict):
108
- if not creds.get("cloud_name") or not creds.get("api_key") or not creds.get("api_secret"):
109
- raise HTTPException(400, "Invalid or missing Cloudinary Environment URL.")
110
-
111
 
112
  # ══════════════════════════════════════════════════════════════════
113
- # 1. VERIFY KEYS
114
  # ══════════════════════════════════════════════════════════════════
115
  @app.post("/api/verify-keys")
116
  async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("")):
117
  if cloudinary_url:
118
- creds = get_cloudinary_creds(cloudinary_url)
119
- require_cloudinary_creds(creds)
120
  try:
 
 
121
  await asyncio.to_thread(_cld_ping, creds)
 
122
  except Exception:
123
- raise HTTPException(400, "Cloudinary ping failed. Check your Environment URL.")
124
-
125
  if pinecone_key:
126
  try:
127
  pc = _get_pinecone(pinecone_key)
128
  existing = {idx.name for idx in await asyncio.to_thread(pc.list_indexes)}
129
  tasks = []
130
  if IDX_OBJECTS not in existing:
131
- tasks.append(asyncio.to_thread(
132
- pc.create_index, name=IDX_OBJECTS, dimension=1536, metric="cosine",
133
- spec=ServerlessSpec(cloud="aws", region="us-east-1")
134
- ))
135
  if IDX_FACES not in existing:
136
- tasks.append(asyncio.to_thread(
137
- pc.create_index, name=IDX_FACES, dimension=512, metric="cosine",
138
- spec=ServerlessSpec(cloud="aws", region="us-east-1")
139
- ))
140
  if tasks:
141
  await asyncio.gather(*tasks)
142
- except HTTPException:
143
- raise
144
  except Exception as e:
145
  raise HTTPException(400, f"Pinecone Error: {e}")
146
-
147
  return {"message": "Keys verified and indexes ready!"}
148
 
149
 
150
  # ══════════════════════════════════════════════════════════════════
151
- # 2. UPLOAD
152
  # ══════════════════════════════════════════════════════════════════
153
  @app.post("/api/upload")
154
- async def upload_new_images(
155
- files: List[UploadFile] = File(...),
156
- folder_name: str = Form(...),
157
- detect_faces: bool = Form(True),
158
- user_pinecone_key: str = Form(""),
159
- user_cloudinary_url: str = Form(""),
160
- ):
161
- actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY", "")
162
- actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL", "")
163
 
164
  if not actual_pc_key or not actual_cld_url:
165
- raise HTTPException(400, "API Keys are missing.")
166
 
167
- creds = get_cloudinary_creds(actual_cld_url)
168
- require_cloudinary_creds(creds)
169
-
170
- folder = standardize_category_name(folder_name)
171
- pc = _get_pinecone(actual_pc_key)
172
- idx_obj = pc.Index(IDX_OBJECTS)
173
- idx_face = pc.Index(IDX_FACES)
174
  uploaded_urls = []
 
 
 
 
 
 
 
 
175
 
176
  for file in files:
177
  tmp_path = f"temp_uploads/{uuid.uuid4().hex}_{sanitize_filename(file.filename)}"
@@ -179,54 +145,40 @@ async def upload_new_images(
179
  with open(tmp_path, "wb") as buf:
180
  shutil.copyfileobj(file.file, buf)
181
 
182
- # Upload using THIS request's credentials only
183
- res = await asyncio.to_thread(_cld_upload, tmp_path, folder, creds)
184
  image_url = res["secure_url"]
185
  uploaded_urls.append(image_url)
186
 
187
  async with _inference_sem:
188
  vectors = await ai.process_image_async(tmp_path, is_query=False, detect_faces=detect_faces)
189
 
190
- face_ups, obj_ups = [], []
191
  for v in vectors:
192
  vec_list = v["vector"].tolist() if hasattr(v["vector"], "tolist") else v["vector"]
193
- record = {
194
- "id": str(uuid.uuid4()),
195
- "values": vec_list,
196
- "metadata": {"url": image_url, "folder": folder},
197
- }
198
- (face_ups if v["type"] == "face" else obj_ups).append(record)
199
-
200
- tasks = []
201
- if face_ups: tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_ups))
202
- if obj_ups: tasks.append(asyncio.to_thread(idx_obj.upsert, vectors=obj_ups))
203
- if tasks: await asyncio.gather(*tasks)
204
 
205
- except HTTPException:
206
- raise
 
 
207
  except Exception as e:
208
- print(f"Upload error: {e}")
209
  raise HTTPException(500, f"Upload processing failed: {str(e)}")
210
  finally:
211
- if os.path.exists(tmp_path):
212
- os.remove(tmp_path)
213
-
214
  return {"message": "Done!", "urls": uploaded_urls}
215
 
216
 
217
  # ══════════════════════════════════════════════════════════════════
218
- # 3. SEARCH
219
  # ══════════════════════════════════════════════════════════════════
220
  @app.post("/api/search")
221
- async def search_database(
222
- file: UploadFile = File(...),
223
- detect_faces: bool = Form(True),
224
- user_pinecone_key: str = Form(""),
225
- user_cloudinary_url: str = Form(""),
226
- ):
227
- actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY", "")
228
  if not actual_pc_key:
229
- raise HTTPException(400, "Pinecone Key is missing.")
230
 
231
  tmp_path = f"temp_uploads/query_{uuid.uuid4().hex}_{sanitize_filename(file.filename)}"
232
  try:
@@ -236,31 +188,28 @@ async def search_database(
236
  async with _inference_sem:
237
  vectors = await ai.process_image_async(tmp_path, is_query=True, detect_faces=detect_faces)
238
 
239
- pc = _get_pinecone(actual_pc_key)
240
- idx_obj = pc.Index(IDX_OBJECTS)
241
  idx_face = pc.Index(IDX_FACES)
242
 
243
  async def _query_one(vec_dict: dict):
244
- vec_list = vec_dict["vector"].tolist() if hasattr(vec_dict["vector"], "tolist") else vec_dict["vector"]
245
  target_idx = idx_face if vec_dict["type"] == "face" else idx_obj
 
246
  try:
247
  res = await asyncio.to_thread(target_idx.query, vector=vec_list, top_k=10, include_metadata=True)
248
  except Exception as e:
249
  if "404" in str(e):
250
- raise HTTPException(404, "Pinecone index not found. Go to Settings Verify & Save to build indexes.")
251
- raise
 
252
  out = []
253
  for match in res.get("matches", []):
254
- caption = ("👤 Verified Identity" if vec_dict["type"] == "face"
255
- else match["metadata"].get("folder", "🎯 Object Match"))
256
- out.append({
257
- "url": match["metadata"].get("url", ""),
258
- "score": match["score"],
259
- "caption": caption,
260
- })
261
  return out
262
 
263
- nested = await asyncio.gather(*[_query_one(v) for v in vectors])
264
  all_results = [r for sub in nested for r in sub]
265
 
266
  seen = {}
@@ -270,15 +219,13 @@ async def search_database(
270
  seen[url] = r
271
 
272
  return {"results": sorted(seen.values(), key=lambda x: x["score"], reverse=True)[:10]}
273
-
274
  except HTTPException:
275
  raise
276
  except Exception as e:
277
- print(f"Search error: {e}")
278
  raise HTTPException(500, str(e))
279
  finally:
280
- if os.path.exists(tmp_path):
281
- os.remove(tmp_path)
282
 
283
 
284
  # ══════════════════════════════════════════════════════════════════
@@ -286,19 +233,15 @@ async def search_database(
286
  # ═════════��════════════════════════════════════════════════════════
287
  @app.post("/api/categories")
288
  async def get_categories(user_cloudinary_url: str = Form("")):
289
- # Priority:
290
- # 1. Logged-in user sends their own URL → use it, fetch THEIR folders
291
- # 2. Guest (no URL sent) → use DEFAULT_CLOUDINARY_URL (server owner's demo cloud)
292
- # 3. No URL anywhere → return empty list, never error
293
- actual_cld_url = user_cloudinary_url.strip() or os.getenv("DEFAULT_CLOUDINARY_URL", "")
294
  if not actual_cld_url:
295
  return {"categories": []}
296
-
297
- creds = get_cloudinary_creds(actual_cld_url)
298
- if not creds.get("cloud_name") or not creds.get("api_key"):
299
- return {"categories": []}
300
-
301
  try:
 
 
 
 
302
  result = await asyncio.to_thread(_cld_root_folders, creds)
303
  return {"categories": [f["name"] for f in result.get("folders", [])]}
304
  except Exception as e:
@@ -306,9 +249,6 @@ async def get_categories(user_cloudinary_url: str = Form("")):
306
  return {"categories": []}
307
 
308
 
309
- # ══════════════════════════════════════════════════════════════════
310
- # 5. HEALTH
311
- # ══════════════════════════════════════════════════════════════════
312
  @app.get("/api/health")
313
  async def health():
314
- return {"status": "ok", "device": getattr(ai, "device", "loading")}
 
17
  import cloudinary.api
18
  from pinecone import Pinecone, ServerlessSpec
19
 
20
+ # ── Deferred imports ─────────────────────────────────────────────
21
  ai = None
22
  p = inflect.engine()
23
 
 
27
  _pinecone_pool = OrderedDict()
28
  _POOL_MAX = 64
29
 
30
+ IDX_FACES = "enterprise-faces"
31
  IDX_OBJECTS = "enterprise-objects"
32
 
 
33
  def _get_pinecone(api_key: str) -> Pinecone:
34
  if api_key not in _pinecone_pool:
35
  if len(_pinecone_pool) >= _POOL_MAX:
 
38
  _pinecone_pool.move_to_end(api_key)
39
  return _pinecone_pool[api_key]
40
 
41
+ # Cloudinary helpers credentials injected per-call, never globally configured.
42
+ # This is the ONLY safe pattern when multiple users share one server process.
 
43
  def _cld_upload(tmp_path: str, folder: str, creds: dict):
44
  return cloudinary.uploader.upload(
45
+ tmp_path, folder=folder,
46
+ api_key=creds["api_key"], api_secret=creds["api_secret"], cloud_name=creds["cloud_name"],
 
 
 
47
  )
48
 
49
  def _cld_ping(creds: dict):
50
  return cloudinary.api.ping(
51
+ api_key=creds["api_key"], api_secret=creds["api_secret"], cloud_name=creds["cloud_name"],
 
 
52
  )
53
 
54
  def _cld_root_folders(creds: dict):
55
  return cloudinary.api.root_folders(
56
+ api_key=creds["api_key"], api_secret=creds["api_secret"], cloud_name=creds["cloud_name"],
 
 
57
  )
58
 
 
59
  @asynccontextmanager
60
  async def lifespan(app: FastAPI):
61
  global ai, _inference_sem
62
  from src.models import AIModelManager
63
+
64
+ print("⏳ Loading AI models …")
65
  loop = asyncio.get_event_loop()
66
  ai = await loop.run_in_executor(None, AIModelManager)
67
  _inference_sem = asyncio.Semaphore(MAX_CONCURRENT_INFERENCES)
68
+ print("Ready!")
69
  yield
70
 
71
  app = FastAPI(lifespan=lifespan)
72
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
 
 
 
 
 
 
73
  os.makedirs("temp_uploads", exist_ok=True)
74
 
75
  def standardize_category_name(name: str) -> str:
 
84
  if not env_url:
85
  return {}
86
  parsed = urlparse(env_url)
87
+ return {"api_key": parsed.username, "api_secret": parsed.password, "cloud_name": parsed.hostname}
 
 
 
 
 
 
 
 
 
88
 
89
  # ══════════════════════════════════════════════════════════════════
90
+ # 1. VERIFY KEYS & AUTO-BUILD INDEXES
91
  # ══════════════════════════════════════════════════════════════════
92
  @app.post("/api/verify-keys")
93
  async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("")):
94
  if cloudinary_url:
 
 
95
  try:
96
+ creds = get_cloudinary_creds(cloudinary_url)
97
+ if not creds.get("cloud_name"): raise ValueError("bad url")
98
  await asyncio.to_thread(_cld_ping, creds)
99
+ except HTTPException: raise
100
  except Exception:
101
+ raise HTTPException(400, "Invalid Cloudinary Environment URL.")
 
102
  if pinecone_key:
103
  try:
104
  pc = _get_pinecone(pinecone_key)
105
  existing = {idx.name for idx in await asyncio.to_thread(pc.list_indexes)}
106
  tasks = []
107
  if IDX_OBJECTS not in existing:
108
+ tasks.append(asyncio.to_thread(pc.create_index, name=IDX_OBJECTS, dimension=1536, metric="cosine", spec=ServerlessSpec(cloud="aws", region="us-east-1")))
 
 
 
109
  if IDX_FACES not in existing:
110
+ tasks.append(asyncio.to_thread(pc.create_index, name=IDX_FACES, dimension=512, metric="cosine", spec=ServerlessSpec(cloud="aws", region="us-east-1")))
 
 
 
111
  if tasks:
112
  await asyncio.gather(*tasks)
 
 
113
  except Exception as e:
114
  raise HTTPException(400, f"Pinecone Error: {e}")
 
115
  return {"message": "Keys verified and indexes ready!"}
116
 
117
 
118
  # ══════════════════════════════════════════════════════════════════
119
+ # 2. UPLOAD
120
  # ══════════════════════════════════════════════════════════════════
121
  @app.post("/api/upload")
122
+ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: str = Form(...), detect_faces: bool = Form(True), user_pinecone_key: str = Form(""), user_cloudinary_url: str = Form("")):
123
+ # Frontend always sends the correct keys (guest keys = hardcoded in App.jsx,
124
+ # PRO keys = from Supabase). Backend never falls back to env vars here.
125
+ actual_pc_key = (user_pinecone_key or "").strip()
126
+ actual_cld_url = (user_cloudinary_url or "").strip()
 
 
 
 
127
 
128
  if not actual_pc_key or not actual_cld_url:
129
+ raise HTTPException(400, "API keys missing. Configure them in Settings or check the app.")
130
 
131
+ folder = standardize_category_name(folder_name)
 
 
 
 
 
 
132
  uploaded_urls = []
133
+
134
+ creds = get_cloudinary_creds(actual_cld_url)
135
+ if not creds.get("cloud_name"):
136
+ raise HTTPException(400, "Invalid Cloudinary URL format.")
137
+
138
+ pc = _get_pinecone(actual_pc_key)
139
+ idx_obj = pc.Index(IDX_OBJECTS)
140
+ idx_face = pc.Index(IDX_FACES)
141
 
142
  for file in files:
143
  tmp_path = f"temp_uploads/{uuid.uuid4().hex}_{sanitize_filename(file.filename)}"
 
145
  with open(tmp_path, "wb") as buf:
146
  shutil.copyfileobj(file.file, buf)
147
 
148
+ res = await asyncio.to_thread(_cld_upload, tmp_path, folder, creds)
 
149
  image_url = res["secure_url"]
150
  uploaded_urls.append(image_url)
151
 
152
  async with _inference_sem:
153
  vectors = await ai.process_image_async(tmp_path, is_query=False, detect_faces=detect_faces)
154
 
155
+ face_upserts, object_upserts = [], []
156
  for v in vectors:
157
  vec_list = v["vector"].tolist() if hasattr(v["vector"], "tolist") else v["vector"]
158
+ record = {"id": str(uuid.uuid4()), "values": vec_list, "metadata": {"url": image_url, "folder": folder}}
159
+ (face_upserts if v["type"] == "face" else object_upserts).append(record)
 
 
 
 
 
 
 
 
 
160
 
161
+ upsert_tasks = []
162
+ if face_upserts: upsert_tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_upserts))
163
+ if object_upserts: upsert_tasks.append(asyncio.to_thread(idx_obj.upsert, vectors=object_upserts))
164
+ if upsert_tasks: await asyncio.gather(*upsert_tasks)
165
  except Exception as e:
166
+ print(f"Upload error: {e}")
167
  raise HTTPException(500, f"Upload processing failed: {str(e)}")
168
  finally:
169
+ if os.path.exists(tmp_path): os.remove(tmp_path)
170
+
 
171
  return {"message": "Done!", "urls": uploaded_urls}
172
 
173
 
174
  # ══════════════════════════════════════════════════════════════════
175
+ # 3. SEARCH
176
  # ══════════════════════════════════════════════════════════════════
177
  @app.post("/api/search")
178
+ async def search_database(file: UploadFile = File(...), detect_faces: bool = Form(True), user_pinecone_key: str = Form(""), user_cloudinary_url: str = Form("")):
179
+ actual_pc_key = (user_pinecone_key or "").strip()
 
 
 
 
 
180
  if not actual_pc_key:
181
+ raise HTTPException(400, "Pinecone key missing.")
182
 
183
  tmp_path = f"temp_uploads/query_{uuid.uuid4().hex}_{sanitize_filename(file.filename)}"
184
  try:
 
188
  async with _inference_sem:
189
  vectors = await ai.process_image_async(tmp_path, is_query=True, detect_faces=detect_faces)
190
 
191
+ pc = _get_pinecone(actual_pc_key)
192
+ idx_obj = pc.Index(IDX_OBJECTS)
193
  idx_face = pc.Index(IDX_FACES)
194
 
195
  async def _query_one(vec_dict: dict):
196
+ vec_list = vec_dict["vector"].tolist() if hasattr(vec_dict["vector"], "tolist") else vec_dict["vector"]
197
  target_idx = idx_face if vec_dict["type"] == "face" else idx_obj
198
+
199
  try:
200
  res = await asyncio.to_thread(target_idx.query, vector=vec_list, top_k=10, include_metadata=True)
201
  except Exception as e:
202
  if "404" in str(e):
203
+ raise HTTPException(404, f"Pinecone Index not found. Please log in and click 'Verify Keys' in Settings to build the indexes.")
204
+ raise e
205
+
206
  out = []
207
  for match in res.get("matches", []):
208
+ caption = "👤 Verified Identity" if vec_dict["type"] == "face" else match["metadata"].get("folder", "🎯 Object Match")
209
+ out.append({"url": match["metadata"].get("url", ""), "score": match["score"], "caption": caption})
 
 
 
 
 
210
  return out
211
 
212
+ nested = await asyncio.gather(*[_query_one(v) for v in vectors])
213
  all_results = [r for sub in nested for r in sub]
214
 
215
  seen = {}
 
219
  seen[url] = r
220
 
221
  return {"results": sorted(seen.values(), key=lambda x: x["score"], reverse=True)[:10]}
 
222
  except HTTPException:
223
  raise
224
  except Exception as e:
225
+ print(f"Search error: {e}")
226
  raise HTTPException(500, str(e))
227
  finally:
228
+ if os.path.exists(tmp_path): os.remove(tmp_path)
 
229
 
230
 
231
  # ══════════════════════════════════════════════════════════════════
 
233
  # ═════════��════════════════════════════════════════════════════════
234
  @app.post("/api/categories")
235
  async def get_categories(user_cloudinary_url: str = Form("")):
236
+ actual_cld_url = (user_cloudinary_url or "").strip()
 
 
 
 
237
  if not actual_cld_url:
238
  return {"categories": []}
239
+
 
 
 
 
240
  try:
241
+ creds = get_cloudinary_creds(actual_cld_url)
242
+ if not creds.get("cloud_name"):
243
+ return {"categories": []}
244
+
245
  result = await asyncio.to_thread(_cld_root_folders, creds)
246
  return {"categories": [f["name"] for f in result.get("folders", [])]}
247
  except Exception as e:
 
249
  return {"categories": []}
250
 
251
 
 
 
 
252
  @app.get("/api/health")
253
  async def health():
254
+ return {"status": "ok"}