AdarshDRC commited on
Commit
b6f5c87
·
verified ·
1 Parent(s): 8ab917e

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +23 -33
main.py CHANGED
@@ -25,10 +25,8 @@ 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
- # FIX 1: Restored your original Pinecone Index names!
32
  IDX_FACES = "enterprise-faces"
33
  IDX_OBJECTS = "enterprise-objects"
34
 
@@ -40,15 +38,10 @@ def _get_pinecone(api_key: str) -> Pinecone:
40
  _pinecone_pool.move_to_end(api_key)
41
  return _pinecone_pool[api_key]
42
 
43
- def _configure_cloudinary(creds: dict):
44
- key = creds["cloud_name"]
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):
@@ -74,9 +67,6 @@ def standardize_category_name(name: str) -> str:
74
  def sanitize_filename(filename: str) -> str:
75
  return re.sub(r'[^\w.\-]', '', re.sub(r'\s+', '_', filename))
76
 
77
- def get_cloudinary_creds(env_url: str) -> dict:
78
- parsed = urlparse(env_url)
79
- return {"api_key": parsed.username, "api_secret": parsed.password, "cloud_name": parsed.hostname}
80
 
81
  # ══════════════════════════════════════════════════════════════════
82
  # 1. VERIFY KEYS & AUTO-BUILD INDEXES
@@ -85,8 +75,9 @@ def get_cloudinary_creds(env_url: str) -> dict:
85
  async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("")):
86
  if cloudinary_url:
87
  try:
88
- _configure_cloudinary(get_cloudinary_creds(cloudinary_url))
89
- await asyncio.to_thread(cloudinary.api.ping)
 
90
  except Exception:
91
  raise HTTPException(400, "Invalid Cloudinary Environment URL.")
92
  if pinecone_key:
@@ -106,11 +97,10 @@ async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("
106
 
107
 
108
  # ══════════════════════════════════════════════════════════════════
109
- # 2. UPLOAD (Strictly Cloud-Native + Freemium Default)
110
  # ══════════════════════════════════════════════════════════════════
111
  @app.post("/api/upload")
112
  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("")):
113
- # FIX 2: Uses user keys if provided, otherwise falls back to HF Secrets
114
  actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY")
115
  actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL")
116
 
@@ -119,7 +109,8 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
119
 
120
  folder = standardize_category_name(folder_name)
121
  uploaded_urls = []
122
- _configure_cloudinary(get_cloudinary_creds(actual_cld_url))
 
123
  pc = _get_pinecone(actual_pc_key)
124
  idx_obj = pc.Index(IDX_OBJECTS)
125
  idx_face = pc.Index(IDX_FACES)
@@ -130,7 +121,7 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
130
  with open(tmp_path, "wb") as buf:
131
  shutil.copyfileobj(file.file, buf)
132
 
133
- res = await asyncio.to_thread(cloudinary.uploader.upload, tmp_path, folder=folder)
134
  image_url = res["secure_url"]
135
  uploaded_urls.append(image_url)
136
 
@@ -147,8 +138,12 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
147
  if face_upserts: upsert_tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_upserts))
148
  if object_upserts: upsert_tasks.append(asyncio.to_thread(idx_obj.upsert, vectors=object_upserts))
149
  if upsert_tasks: await asyncio.gather(*upsert_tasks)
 
150
  except Exception as e:
 
 
151
  print(f"❌ Upload error: {e}")
 
152
  finally:
153
  if os.path.exists(tmp_path): os.remove(tmp_path)
154
 
@@ -156,7 +151,7 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
156
 
157
 
158
  # ══════════════════════════════════════════════════════════════════
159
- # 3. SEARCH (Strictly Cloud-Native + Freemium Default)
160
  # ══════════════════════════════════════════════════════════════════
161
  @app.post("/api/search")
162
  async def search_database(file: UploadFile = File(...), detect_faces: bool = Form(True), user_pinecone_key: str = Form(""), user_cloudinary_url: str = Form("")):
@@ -180,14 +175,7 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
180
  vec_list = vec_dict["vector"].tolist() if hasattr(vec_dict["vector"], "tolist") else vec_dict["vector"]
181
  target_idx = idx_face if vec_dict["type"] == "face" else idx_obj
182
 
183
- try:
184
- res = await asyncio.to_thread(target_idx.query, vector=vec_list, top_k=10, include_metadata=True)
185
- except Exception as e:
186
- if "404" in str(e):
187
- # Graceful error if index truly doesn't exist
188
- raise HTTPException(404, f"Pinecone Index not found. Please log in and click 'Verify Keys' in Settings to build the indexes.")
189
- raise e
190
-
191
  out = []
192
  for match in res.get("matches", []):
193
  caption = "👤 Verified Identity" if vec_dict["type"] == "face" else match["metadata"].get("folder", "🎯 Object Match")
@@ -204,9 +192,12 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
204
  seen[url] = r
205
 
206
  return {"results": sorted(seen.values(), key=lambda x: x["score"], reverse=True)[:10]}
 
207
  except HTTPException:
208
  raise
209
  except Exception as e:
 
 
210
  print(f"❌ Search error: {e}")
211
  raise HTTPException(500, str(e))
212
  finally:
@@ -214,7 +205,7 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
214
 
215
 
216
  # ══════════════════════════════════════════════════════════════════
217
- # 4. CATEGORIES (Strictly Cloud-Native + Freemium Default)
218
  # ══════════════════════════════════════════════════════════════════
219
  @app.post("/api/categories")
220
  async def get_categories(user_cloudinary_url: str = Form("")):
@@ -223,9 +214,8 @@ async def get_categories(user_cloudinary_url: str = Form("")):
223
  return {"categories": []}
224
 
225
  try:
226
- # FIX 3: Removed local folder scanning logic. Guests now strictly see Cloudinary folders.
227
- _configure_cloudinary(get_cloudinary_creds(actual_cld_url))
228
- result = await asyncio.to_thread(cloudinary.api.root_folders)
229
  return {"categories": [f["name"] for f in result.get("folders", [])]}
230
  except Exception as e:
231
  print(f"Category fetch error: {e}")
 
25
  _inference_sem: asyncio.Semaphore
26
 
27
  _pinecone_pool = OrderedDict()
 
28
  _POOL_MAX = 64
29
 
 
30
  IDX_FACES = "enterprise-faces"
31
  IDX_OBJECTS = "enterprise-objects"
32
 
 
38
  _pinecone_pool.move_to_end(api_key)
39
  return _pinecone_pool[api_key]
40
 
41
+ def get_cld_kwargs(env_url: str) -> dict:
42
+ """Extracts credentials to be passed state-lessly to prevent cross-user data bleed."""
43
+ parsed = urlparse(env_url)
44
+ return {"api_key": parsed.username, "api_secret": parsed.password, "cloud_name": parsed.hostname}
 
 
 
 
 
45
 
46
  @asynccontextmanager
47
  async def lifespan(app: FastAPI):
 
67
  def sanitize_filename(filename: str) -> str:
68
  return re.sub(r'[^\w.\-]', '', re.sub(r'\s+', '_', filename))
69
 
 
 
 
70
 
71
  # ══════════════════════════════════════════════════════════════════
72
  # 1. VERIFY KEYS & AUTO-BUILD INDEXES
 
75
  async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("")):
76
  if cloudinary_url:
77
  try:
78
+ # Stateless ping to check credentials
79
+ cld_kwargs = get_cld_kwargs(cloudinary_url)
80
+ await asyncio.to_thread(cloudinary.api.root_folders, max_results=1, **cld_kwargs)
81
  except Exception:
82
  raise HTTPException(400, "Invalid Cloudinary Environment URL.")
83
  if pinecone_key:
 
97
 
98
 
99
  # ══════════════════════════════════════════════════════════════════
100
+ # 2. UPLOAD (Stateless + Auto-Catch 404s)
101
  # ══════════════════════════════════════════════════════════════════
102
  @app.post("/api/upload")
103
  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("")):
 
104
  actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY")
105
  actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL")
106
 
 
109
 
110
  folder = standardize_category_name(folder_name)
111
  uploaded_urls = []
112
+ cld_kwargs = get_cld_kwargs(actual_cld_url)
113
+
114
  pc = _get_pinecone(actual_pc_key)
115
  idx_obj = pc.Index(IDX_OBJECTS)
116
  idx_face = pc.Index(IDX_FACES)
 
121
  with open(tmp_path, "wb") as buf:
122
  shutil.copyfileobj(file.file, buf)
123
 
124
+ res = await asyncio.to_thread(cloudinary.uploader.upload, tmp_path, folder=folder, **cld_kwargs)
125
  image_url = res["secure_url"]
126
  uploaded_urls.append(image_url)
127
 
 
138
  if face_upserts: upsert_tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_upserts))
139
  if object_upserts: upsert_tasks.append(asyncio.to_thread(idx_obj.upsert, vectors=object_upserts))
140
  if upsert_tasks: await asyncio.gather(*upsert_tasks)
141
+
142
  except Exception as e:
143
+ if "404" in str(e) or "NOT_FOUND" in str(e):
144
+ raise HTTPException(400, "Pinecone indexes missing. Please go to Settings and click 'Verify & Save' to build them.")
145
  print(f"❌ Upload error: {e}")
146
+ raise HTTPException(500, f"Upload error: {e}")
147
  finally:
148
  if os.path.exists(tmp_path): os.remove(tmp_path)
149
 
 
151
 
152
 
153
  # ══════════════════════════════════════════════════════════════════
154
+ # 3. SEARCH (Stateless + Auto-Catch 404s)
155
  # ══════════════════════════════════════════════════════════════════
156
  @app.post("/api/search")
157
  async def search_database(file: UploadFile = File(...), detect_faces: bool = Form(True), user_pinecone_key: str = Form(""), user_cloudinary_url: str = Form("")):
 
175
  vec_list = vec_dict["vector"].tolist() if hasattr(vec_dict["vector"], "tolist") else vec_dict["vector"]
176
  target_idx = idx_face if vec_dict["type"] == "face" else idx_obj
177
 
178
+ res = await asyncio.to_thread(target_idx.query, vector=vec_list, top_k=10, include_metadata=True)
 
 
 
 
 
 
 
179
  out = []
180
  for match in res.get("matches", []):
181
  caption = "👤 Verified Identity" if vec_dict["type"] == "face" else match["metadata"].get("folder", "🎯 Object Match")
 
192
  seen[url] = r
193
 
194
  return {"results": sorted(seen.values(), key=lambda x: x["score"], reverse=True)[:10]}
195
+
196
  except HTTPException:
197
  raise
198
  except Exception as e:
199
+ if "404" in str(e) or "NOT_FOUND" in str(e):
200
+ raise HTTPException(400, "Pinecone indexes missing. Please go to Settings and click 'Verify & Save' to build them.")
201
  print(f"❌ Search error: {e}")
202
  raise HTTPException(500, str(e))
203
  finally:
 
205
 
206
 
207
  # ══════════════════════════════════════════════════════════════════
208
+ # 4. CATEGORIES (Stateless Isolation)
209
  # ══════════════════════════════════════════════════════════════════
210
  @app.post("/api/categories")
211
  async def get_categories(user_cloudinary_url: str = Form("")):
 
214
  return {"categories": []}
215
 
216
  try:
217
+ cld_kwargs = get_cld_kwargs(actual_cld_url)
218
+ result = await asyncio.to_thread(cloudinary.api.root_folders, **cld_kwargs)
 
219
  return {"categories": [f["name"] for f in result.get("folders", [])]}
220
  except Exception as e:
221
  print(f"Category fetch error: {e}")