AdarshDRC commited on
Commit
7f323ef
·
verified ·
1 Parent(s): dfc42e1

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +139 -75
main.py CHANGED
@@ -17,7 +17,6 @@ import cloudinary.uploader
17
  import cloudinary.api
18
  from pinecone import Pinecone, ServerlessSpec
19
 
20
- # ── Deferred imports ─────────────────────────────────────────────
21
  ai = None
22
  p = inflect.engine()
23
 
@@ -25,12 +24,12 @@ MAX_CONCURRENT_INFERENCES = int(os.getenv("MAX_CONCURRENT_INFERENCES", "6"))
25
  _inference_sem: asyncio.Semaphore
26
 
27
  _pinecone_pool = OrderedDict()
28
- _cloudinary_pool = {}
29
  _POOL_MAX = 64
30
 
31
  IDX_FACES = "enterprise-faces"
32
  IDX_OBJECTS = "enterprise-objects"
33
 
 
34
  def _get_pinecone(api_key: str) -> Pinecone:
35
  if api_key not in _pinecone_pool:
36
  if len(_pinecone_pool) >= _POOL_MAX:
@@ -39,31 +38,52 @@ def _get_pinecone(api_key: str) -> Pinecone:
39
  _pinecone_pool.move_to_end(api_key)
40
  return _pinecone_pool[api_key]
41
 
42
- def _configure_cloudinary(creds: dict):
43
- key = creds.get("cloud_name")
44
- if not key: return
45
- if key not in _cloudinary_pool:
46
- cloudinary.config(
47
- cloud_name=creds["cloud_name"],
48
- api_key=creds["api_key"],
49
- api_secret=creds["api_secret"]
50
- )
51
- _cloudinary_pool[key] = True
52
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  @asynccontextmanager
54
  async def lifespan(app: FastAPI):
55
  global ai, _inference_sem
56
  from src.models import AIModelManager
57
-
58
- print("⏳ Loading AI models …")
59
  loop = asyncio.get_event_loop()
60
  ai = await loop.run_in_executor(None, AIModelManager)
61
  _inference_sem = asyncio.Semaphore(MAX_CONCURRENT_INFERENCES)
62
- print("Ready!")
63
  yield
64
 
65
  app = FastAPI(lifespan=lifespan)
66
- app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
 
 
 
 
 
 
67
  os.makedirs("temp_uploads", exist_ok=True)
68
 
69
  def standardize_category_name(name: str) -> str:
@@ -78,99 +98,135 @@ def get_cloudinary_creds(env_url: str) -> dict:
78
  if not env_url:
79
  return {}
80
  parsed = urlparse(env_url)
81
- return {"api_key": parsed.username, "api_secret": parsed.password, "cloud_name": parsed.hostname}
 
 
 
 
 
 
 
 
 
82
 
83
  # ══════════════════════════════════════════════════════════════════
84
- # 1. VERIFY KEYS & AUTO-BUILD INDEXES
85
  # ══════════════════════════════════════════════════════════════════
86
  @app.post("/api/verify-keys")
87
  async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("")):
88
  if cloudinary_url:
 
 
89
  try:
90
- _configure_cloudinary(get_cloudinary_creds(cloudinary_url))
91
- await asyncio.to_thread(cloudinary.api.ping)
92
  except Exception:
93
- raise HTTPException(400, "Invalid Cloudinary Environment URL.")
 
94
  if pinecone_key:
95
  try:
96
  pc = _get_pinecone(pinecone_key)
97
  existing = {idx.name for idx in await asyncio.to_thread(pc.list_indexes)}
98
  tasks = []
99
  if IDX_OBJECTS not in existing:
100
- tasks.append(asyncio.to_thread(pc.create_index, name=IDX_OBJECTS, dimension=1536, metric="cosine", spec=ServerlessSpec(cloud="aws", region="us-east-1")))
 
 
 
101
  if IDX_FACES not in existing:
102
- tasks.append(asyncio.to_thread(pc.create_index, name=IDX_FACES, dimension=512, metric="cosine", spec=ServerlessSpec(cloud="aws", region="us-east-1")))
 
 
 
103
  if tasks:
104
  await asyncio.gather(*tasks)
 
 
105
  except Exception as e:
106
  raise HTTPException(400, f"Pinecone Error: {e}")
 
107
  return {"message": "Keys verified and indexes ready!"}
108
 
109
 
110
  # ══════════════════════════════════════════════════════════════════
111
- # 2. UPLOAD
112
  # ══════════════════════════════════════════════════════════════════
113
  @app.post("/api/upload")
114
- 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("")):
115
- # DEFENSIVE FIX: The 'or ""' ensures it never becomes None, preventing 500 crashes
116
- actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY", "")
 
 
 
 
 
117
  actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL", "")
118
-
119
  if not actual_pc_key or not actual_cld_url:
120
- raise HTTPException(400, "API Keys are missing. If you are a guest, the server is missing its DEFAULT_ secrets in Hugging Face.")
121
 
122
- folder = standardize_category_name(folder_name)
123
- uploaded_urls = []
124
-
125
  creds = get_cloudinary_creds(actual_cld_url)
126
- if not creds.get("cloud_name"):
127
- raise HTTPException(400, "Invalid Cloudinary URL format.")
128
-
129
- _configure_cloudinary(creds)
130
- pc = _get_pinecone(actual_pc_key)
131
- idx_obj = pc.Index(IDX_OBJECTS)
132
- idx_face = pc.Index(IDX_FACES)
133
 
134
  for file in files:
135
  tmp_path = f"temp_uploads/{uuid.uuid4().hex}_{sanitize_filename(file.filename)}"
136
  try:
137
  with open(tmp_path, "wb") as buf:
138
  shutil.copyfileobj(file.file, buf)
139
-
140
- res = await asyncio.to_thread(cloudinary.uploader.upload, tmp_path, folder=folder)
 
141
  image_url = res["secure_url"]
142
  uploaded_urls.append(image_url)
143
 
144
  async with _inference_sem:
145
  vectors = await ai.process_image_async(tmp_path, is_query=False, detect_faces=detect_faces)
146
 
147
- face_upserts, object_upserts = [], []
148
  for v in vectors:
149
  vec_list = v["vector"].tolist() if hasattr(v["vector"], "tolist") else v["vector"]
150
- record = {"id": str(uuid.uuid4()), "values": vec_list, "metadata": {"url": image_url, "folder": folder}}
151
- (face_upserts if v["type"] == "face" else object_upserts).append(record)
 
 
 
 
152
 
153
- upsert_tasks = []
154
- if face_upserts: upsert_tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_upserts))
155
- if object_upserts: upsert_tasks.append(asyncio.to_thread(idx_obj.upsert, vectors=object_upserts))
156
- if upsert_tasks: await asyncio.gather(*upsert_tasks)
 
 
 
157
  except Exception as e:
158
- print(f"Upload error: {e}")
159
  raise HTTPException(500, f"Upload processing failed: {str(e)}")
160
  finally:
161
- if os.path.exists(tmp_path): os.remove(tmp_path)
162
-
 
163
  return {"message": "Done!", "urls": uploaded_urls}
164
 
165
 
166
  # ══════════════════════════════════════════════════════════════════
167
- # 3. SEARCH
168
  # ══════════════════════════════════════════════════════════════════
169
  @app.post("/api/search")
170
- async def search_database(file: UploadFile = File(...), detect_faces: bool = Form(True), user_pinecone_key: str = Form(""), user_cloudinary_url: str = Form("")):
 
 
 
 
 
171
  actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY", "")
172
  if not actual_pc_key:
173
- raise HTTPException(400, "Pinecone Key is missing. If you are a guest, the server is missing its DEFAULT_PINECONE_KEY in Hugging Face.")
174
 
175
  tmp_path = f"temp_uploads/query_{uuid.uuid4().hex}_{sanitize_filename(file.filename)}"
176
  try:
@@ -180,28 +236,31 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
180
  async with _inference_sem:
181
  vectors = await ai.process_image_async(tmp_path, is_query=True, detect_faces=detect_faces)
182
 
183
- pc = _get_pinecone(actual_pc_key)
184
- idx_obj = pc.Index(IDX_OBJECTS)
185
  idx_face = pc.Index(IDX_FACES)
186
 
187
  async def _query_one(vec_dict: dict):
188
- vec_list = vec_dict["vector"].tolist() if hasattr(vec_dict["vector"], "tolist") else vec_dict["vector"]
189
  target_idx = idx_face if vec_dict["type"] == "face" else idx_obj
190
-
191
  try:
192
  res = await asyncio.to_thread(target_idx.query, vector=vec_list, top_k=10, include_metadata=True)
193
  except Exception as e:
194
  if "404" in str(e):
195
- raise HTTPException(404, f"Pinecone Index not found. Please log in and click 'Verify Keys' in Settings to build the indexes.")
196
- raise e
197
-
198
  out = []
199
  for match in res.get("matches", []):
200
- caption = "👤 Verified Identity" if vec_dict["type"] == "face" else match["metadata"].get("folder", "🎯 Object Match")
201
- out.append({"url": match["metadata"].get("url", ""), "score": match["score"], "caption": caption})
 
 
 
 
 
202
  return out
203
 
204
- nested = await asyncio.gather(*[_query_one(v) for v in vectors])
205
  all_results = [r for sub in nested for r in sub]
206
 
207
  seen = {}
@@ -211,13 +270,15 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
211
  seen[url] = r
212
 
213
  return {"results": sorted(seen.values(), key=lambda x: x["score"], reverse=True)[:10]}
 
214
  except HTTPException:
215
  raise
216
  except Exception as e:
217
- print(f"Search error: {e}")
218
  raise HTTPException(500, str(e))
219
  finally:
220
- if os.path.exists(tmp_path): os.remove(tmp_path)
 
221
 
222
 
223
  # ══════════════════════════════════════════════════════════════════
@@ -228,20 +289,23 @@ async def get_categories(user_cloudinary_url: str = Form("")):
228
  actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL", "")
229
  if not actual_cld_url:
230
  return {"categories": []}
231
-
 
 
 
 
232
  try:
233
- creds = get_cloudinary_creds(actual_cld_url)
234
- if not creds.get("cloud_name"):
235
- return {"categories": []}
236
-
237
- _configure_cloudinary(creds)
238
- result = await asyncio.to_thread(cloudinary.api.root_folders)
239
  return {"categories": [f["name"] for f in result.get("folders", [])]}
240
  except Exception as e:
241
  print(f"Category fetch error: {e}")
242
  return {"categories": []}
243
 
244
 
 
 
 
245
  @app.get("/api/health")
246
  async def health():
247
- return {"status": "ok"}
 
17
  import cloudinary.api
18
  from pinecone import Pinecone, ServerlessSpec
19
 
 
20
  ai = None
21
  p = inflect.engine()
22
 
 
24
  _inference_sem: asyncio.Semaphore
25
 
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
  _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
  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)}"
178
  try:
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
  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
  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
  # ══════════════════════════════════════════════════════════════════
 
289
  actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL", "")
290
  if not actual_cld_url:
291
  return {"categories": []}
292
+
293
+ creds = get_cloudinary_creds(actual_cld_url)
294
+ if not creds.get("cloud_name"):
295
+ return {"categories": []}
296
+
297
  try:
298
+ # Uses THIS request's credentials — never touches another user's cloud
299
+ result = await asyncio.to_thread(_cld_root_folders, creds)
 
 
 
 
300
  return {"categories": [f["name"] for f in result.get("folders", [])]}
301
  except Exception as e:
302
  print(f"Category fetch error: {e}")
303
  return {"categories": []}
304
 
305
 
306
+ # ══════════════════════════════════════════════════════════════════
307
+ # 5. HEALTH
308
+ # ══════════════════════════════════════════════════════════════════
309
  @app.get("/api/health")
310
  async def health():
311
+ return {"status": "ok", "device": getattr(ai, "device", "loading")}