AdarshDRC commited on
Commit
2581fff
·
verified ·
1 Parent(s): 40787fc

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +23 -24
main.py CHANGED
@@ -33,17 +33,15 @@ _POOL_MAX = 64
33
 
34
 
35
  def _get_pinecone(api_key: str) -> Pinecone:
36
- """Return a cached Pinecone client, creating one if needed."""
37
  if api_key not in _pinecone_pool:
38
  if len(_pinecone_pool) >= _POOL_MAX:
39
- _pinecone_pool.popitem(last=False) # evict oldest
40
  _pinecone_pool[api_key] = Pinecone(api_key=api_key)
41
- _pinecone_pool.move_to_end(api_key) # refresh LRU order
42
  return _pinecone_pool[api_key]
43
 
44
 
45
  def _configure_cloudinary(creds: dict) -> None:
46
- """Configure cloudinary module only when needed, with simple caching."""
47
  key = creds["cloud_name"]
48
  if key not in _cloudinary_pool:
49
  cloudinary.config(
@@ -54,7 +52,6 @@ def _configure_cloudinary(creds: dict) -> None:
54
  _cloudinary_pool[key] = True
55
 
56
 
57
- # ── Lifespan: load models once at startup ─────────────────────────
58
  @asynccontextmanager
59
  async def lifespan(app: FastAPI):
60
  global ai, _inference_sem
@@ -73,7 +70,7 @@ app = FastAPI(lifespan=lifespan)
73
 
74
  app.add_middleware(
75
  CORSMiddleware,
76
- allow_origins=["*"], # tighten to your Vercel domain in production
77
  allow_credentials=True,
78
  allow_methods=["*"],
79
  allow_headers=["*"],
@@ -82,7 +79,6 @@ app.add_middleware(
82
  os.makedirs("temp_uploads", exist_ok=True)
83
 
84
 
85
- # ── Helpers ────────────────────────────────────────────────────────
86
  def standardize_category_name(name: str) -> str:
87
  clean = re.sub(r'\s+', '_', name.strip().lower())
88
  clean = re.sub(r'[^\w]', '', clean)
@@ -102,7 +98,6 @@ def get_cloudinary_creds(env_url: str) -> dict:
102
  "cloud_name": parsed.hostname,
103
  }
104
 
105
-
106
  # ══════════════════════════════════════════════════════════════════
107
  # 1. VERIFY KEYS & AUTO-BUILD INDEXES
108
  # ══════════════════════════════════════════════════════════════════
@@ -150,7 +145,7 @@ async def verify_keys(
150
 
151
 
152
  # ══════════════════════════════════════════════════════════════════
153
- # 2. UPLOAD (Cloudinary + Pinecone Only)
154
  # ══════════════════════════════════════════════════════════════════
155
  @app.post("/api/upload")
156
  async def upload_new_images(
@@ -160,15 +155,19 @@ async def upload_new_images(
160
  user_pinecone_key: str = Form(""),
161
  user_cloudinary_url: str = Form(""),
162
  ):
163
- if not user_pinecone_key or not user_cloudinary_url:
164
- raise HTTPException(status_code=400, detail="Cloudinary URL and Pinecone API Key are required to upload.")
 
 
 
 
165
 
166
  folder = standardize_category_name(folder_name)
167
  uploaded_urls = []
168
 
169
- cld_creds = get_cloudinary_creds(user_cloudinary_url)
170
  _configure_cloudinary(cld_creds)
171
- pc = _get_pinecone(user_pinecone_key)
172
  idx_obj = pc.Index("lens-objects")
173
  idx_face = pc.Index("lens-faces")
174
 
@@ -202,7 +201,6 @@ async def upload_new_images(
202
  }
203
  (face_upserts if v["type"] == "face" else object_upserts).append(record)
204
 
205
- # Fire both upserts concurrently
206
  upsert_tasks = []
207
  if face_upserts:
208
  upsert_tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_upserts))
@@ -213,7 +211,6 @@ async def upload_new_images(
213
 
214
  except Exception as e:
215
  print(f"❌ Upload error for {file.filename}: {e}")
216
- # Continue with the next file instead of aborting the whole batch
217
  finally:
218
  if os.path.exists(tmp_path):
219
  os.remove(tmp_path)
@@ -222,16 +219,18 @@ async def upload_new_images(
222
 
223
 
224
  # ══════════════════════════════════════════════════════════════════
225
- # 3. SEARCH (Pinecone Only)
226
  # ══════════════════════════════════════════════════════════════════
227
  @app.post("/api/search")
228
  async def search_database(
229
  file: UploadFile = File(...),
230
  detect_faces: bool = Form(True),
231
  user_pinecone_key: str = Form(""),
232
- user_cloudinary_url: str = Form(""), # Kept to match frontend form payload
233
  ):
234
- if not user_pinecone_key:
 
 
235
  raise HTTPException(status_code=400, detail="Pinecone API Key is required to search.")
236
 
237
  safe_name = sanitize_filename(file.filename)
@@ -245,11 +244,10 @@ async def search_database(
245
  async with _inference_sem:
246
  vectors = await ai.process_image_async(tmp_path, is_query=True, detect_faces=detect_faces)
247
 
248
- pc = _get_pinecone(user_pinecone_key)
249
  idx_obj = pc.Index("lens-objects")
250
  idx_face = pc.Index("lens-faces")
251
 
252
- # Fire ALL vector queries in parallel
253
  async def _query_one(vec_dict: dict) -> list[dict]:
254
  vec_list = (vec_dict["vector"].tolist() if hasattr(vec_dict["vector"], "tolist") else vec_dict["vector"])
255
  target_idx = idx_face if vec_dict["type"] == "face" else idx_obj
@@ -270,7 +268,6 @@ async def search_database(
270
  nested = await asyncio.gather(*[_query_one(v) for v in vectors])
271
  all_results = [r for sub in nested for r in sub]
272
 
273
- # Deduplicate, keep best score per URL
274
  seen: dict[str, dict] = {}
275
  for r in all_results:
276
  url = r["url"]
@@ -289,15 +286,17 @@ async def search_database(
289
 
290
 
291
  # ══════════════════════════════════════════════════════════════════
292
- # 4. CATEGORIES (Cloudinary Folders Only)
293
  # ══════════════════════════════════════════════════════════════════
294
  @app.post("/api/categories")
295
  async def get_categories(user_cloudinary_url: str = Form("")):
296
- if not user_cloudinary_url:
 
 
297
  return {"categories": []}
298
 
299
  try:
300
- creds = get_cloudinary_creds(user_cloudinary_url)
301
  _configure_cloudinary(creds)
302
  result = await asyncio.to_thread(cloudinary.api.root_folders)
303
  folders = [f["name"] for f in result.get("folders", [])]
 
33
 
34
 
35
  def _get_pinecone(api_key: str) -> Pinecone:
 
36
  if api_key not in _pinecone_pool:
37
  if len(_pinecone_pool) >= _POOL_MAX:
38
+ _pinecone_pool.popitem(last=False)
39
  _pinecone_pool[api_key] = Pinecone(api_key=api_key)
40
+ _pinecone_pool.move_to_end(api_key)
41
  return _pinecone_pool[api_key]
42
 
43
 
44
  def _configure_cloudinary(creds: dict) -> None:
 
45
  key = creds["cloud_name"]
46
  if key not in _cloudinary_pool:
47
  cloudinary.config(
 
52
  _cloudinary_pool[key] = True
53
 
54
 
 
55
  @asynccontextmanager
56
  async def lifespan(app: FastAPI):
57
  global ai, _inference_sem
 
70
 
71
  app.add_middleware(
72
  CORSMiddleware,
73
+ allow_origins=["*"],
74
  allow_credentials=True,
75
  allow_methods=["*"],
76
  allow_headers=["*"],
 
79
  os.makedirs("temp_uploads", exist_ok=True)
80
 
81
 
 
82
  def standardize_category_name(name: str) -> str:
83
  clean = re.sub(r'\s+', '_', name.strip().lower())
84
  clean = re.sub(r'[^\w]', '', clean)
 
98
  "cloud_name": parsed.hostname,
99
  }
100
 
 
101
  # ══════════════════════════════════════════════════════════════════
102
  # 1. VERIFY KEYS & AUTO-BUILD INDEXES
103
  # ══════════════════════════════════════════════════════════════════
 
145
 
146
 
147
  # ══════════════════════════════════════════════════════════════════
148
+ # 2. UPLOAD (With Demo Fallback)
149
  # ══════════════════════════════════════════════════════════════════
150
  @app.post("/api/upload")
151
  async def upload_new_images(
 
155
  user_pinecone_key: str = Form(""),
156
  user_cloudinary_url: str = Form(""),
157
  ):
158
+ # FALLBACK LOGIC: Use user keys if provided, otherwise use Space secrets
159
+ actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY")
160
+ actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL")
161
+
162
+ if not actual_pc_key or not actual_cld_url:
163
+ raise HTTPException(status_code=400, detail="Cloudinary URL and Pinecone API Key are required.")
164
 
165
  folder = standardize_category_name(folder_name)
166
  uploaded_urls = []
167
 
168
+ cld_creds = get_cloudinary_creds(actual_cld_url)
169
  _configure_cloudinary(cld_creds)
170
+ pc = _get_pinecone(actual_pc_key)
171
  idx_obj = pc.Index("lens-objects")
172
  idx_face = pc.Index("lens-faces")
173
 
 
201
  }
202
  (face_upserts if v["type"] == "face" else object_upserts).append(record)
203
 
 
204
  upsert_tasks = []
205
  if face_upserts:
206
  upsert_tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_upserts))
 
211
 
212
  except Exception as e:
213
  print(f"❌ Upload error for {file.filename}: {e}")
 
214
  finally:
215
  if os.path.exists(tmp_path):
216
  os.remove(tmp_path)
 
219
 
220
 
221
  # ══════════════════════════════════════════════════════════════════
222
+ # 3. SEARCH (With Demo Fallback)
223
  # ══════════════════════════════════════════════════════════════════
224
  @app.post("/api/search")
225
  async def search_database(
226
  file: UploadFile = File(...),
227
  detect_faces: bool = Form(True),
228
  user_pinecone_key: str = Form(""),
229
+ user_cloudinary_url: str = Form(""),
230
  ):
231
+ actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY")
232
+
233
+ if not actual_pc_key:
234
  raise HTTPException(status_code=400, detail="Pinecone API Key is required to search.")
235
 
236
  safe_name = sanitize_filename(file.filename)
 
244
  async with _inference_sem:
245
  vectors = await ai.process_image_async(tmp_path, is_query=True, detect_faces=detect_faces)
246
 
247
+ pc = _get_pinecone(actual_pc_key)
248
  idx_obj = pc.Index("lens-objects")
249
  idx_face = pc.Index("lens-faces")
250
 
 
251
  async def _query_one(vec_dict: dict) -> list[dict]:
252
  vec_list = (vec_dict["vector"].tolist() if hasattr(vec_dict["vector"], "tolist") else vec_dict["vector"])
253
  target_idx = idx_face if vec_dict["type"] == "face" else idx_obj
 
268
  nested = await asyncio.gather(*[_query_one(v) for v in vectors])
269
  all_results = [r for sub in nested for r in sub]
270
 
 
271
  seen: dict[str, dict] = {}
272
  for r in all_results:
273
  url = r["url"]
 
286
 
287
 
288
  # ══════════════════════════════════════════════════════════════════
289
+ # 4. CATEGORIES (With Demo Fallback)
290
  # ══════════════════════════════════════════════════════════════════
291
  @app.post("/api/categories")
292
  async def get_categories(user_cloudinary_url: str = Form("")):
293
+ actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL")
294
+
295
+ if not actual_cld_url:
296
  return {"categories": []}
297
 
298
  try:
299
+ creds = get_cloudinary_creds(actual_cld_url)
300
  _configure_cloudinary(creds)
301
  result = await asyncio.to_thread(cloudinary.api.root_folders)
302
  folders = [f["name"] for f in result.get("folders", [])]