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

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +48 -28
main.py CHANGED
@@ -25,6 +25,7 @@ MAX_CONCURRENT_INFERENCES = int(os.getenv("MAX_CONCURRENT_INFERENCES", "6"))
25
  _inference_sem: asyncio.Semaphore
26
 
27
  _pinecone_pool = OrderedDict()
 
28
  _POOL_MAX = 64
29
 
30
  IDX_FACES = "enterprise-faces"
@@ -38,10 +39,16 @@ def _get_pinecone(api_key: str) -> Pinecone:
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,6 +74,11 @@ def standardize_category_name(name: str) -> str:
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,9 +87,8 @@ def sanitize_filename(filename: str) -> str:
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,20 +108,25 @@ async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("
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
 
107
  if not actual_pc_key or not actual_cld_url:
108
- raise HTTPException(400, "API Keys are required. Set defaults in HF Secrets.")
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,7 +137,7 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
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,12 +154,9 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
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,13 +164,13 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
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("")):
158
- actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY")
159
  if not actual_pc_key:
160
- raise HTTPException(400, "Pinecone Key is required.")
161
 
162
  tmp_path = f"temp_uploads/query_{uuid.uuid4().hex}_{sanitize_filename(file.filename)}"
163
  try:
@@ -175,7 +188,13 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
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,12 +211,9 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
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,17 +221,21 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
205
 
206
 
207
  # ══════════════════════════════════════════════════════════════════
208
- # 4. CATEGORIES (Stateless Isolation)
209
  # ══════════════════════════════════════════════════════════════════
210
  @app.post("/api/categories")
211
  async def get_categories(user_cloudinary_url: str = Form("")):
212
- actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL")
213
  if not actual_cld_url:
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}")
 
25
  _inference_sem: asyncio.Semaphore
26
 
27
  _pinecone_pool = OrderedDict()
28
+ _cloudinary_pool = {}
29
  _POOL_MAX = 64
30
 
31
  IDX_FACES = "enterprise-faces"
 
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):
 
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
+ 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
 
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:
 
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)
 
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
 
 
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
 
 
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:
 
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")
 
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:
 
221
 
222
 
223
  # ══════════════════════════════════════════════════════════════════
224
+ # 4. CATEGORIES
225
  # ══════════════════════════════════════════════════════════════════
226
  @app.post("/api/categories")
227
  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}")