AdarshDRC commited on
Commit
e5a7bbe
·
1 Parent(s): d12d95f

fix : database pinecone issue

Browse files
Files changed (2) hide show
  1. main.py +81 -35
  2. src/models.py +12 -4
main.py CHANGED
@@ -328,31 +328,54 @@ async def upload_new_images(
328
  async with _inference_sem:
329
  vectors = await ai.process_image_async(tmp_path, is_query=False, detect_faces=detect_faces)
330
 
331
- face_upserts, object_upserts = [], []
 
 
332
  for v in vectors:
333
  vec_list = v["vector"].tolist() if hasattr(v["vector"], "tolist") else v["vector"]
334
  if v["type"] == "face":
335
- # V3: store per-face metadata including crop thumbnail + bbox
336
- metadata = {
337
- "url": image_url,
338
- "folder": folder,
339
- "face_idx": v.get("face_idx", 0),
340
- "bbox": str(v.get("bbox", [])),
341
- "face_crop": v.get("face_crop", ""), # base64 thumbnail
342
- "det_score": v.get("det_score", 1.0),
343
- }
344
- face_upserts.append({"id": str(uuid.uuid4()), "values": vec_list, "metadata": metadata})
 
 
 
 
 
 
345
  else:
346
- object_upserts.append({"id": str(uuid.uuid4()), "values": vec_list,
347
- "metadata": {"url": image_url, "folder": folder}})
 
 
 
 
 
 
 
 
 
348
 
 
 
 
349
  face_vec_total += len(face_upserts)
350
  object_vec_total += len(object_upserts)
351
 
352
  upsert_tasks = []
353
- if face_upserts: upsert_tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_upserts))
354
- if object_upserts: upsert_tasks.append(asyncio.to_thread(idx_obj.upsert, vectors=object_upserts))
355
- if upsert_tasks: await asyncio.gather(*upsert_tasks)
 
 
 
356
 
357
  log("INFO", "upload.file.success",
358
  user_id=user_id or "anonymous", ip=ip, mode=mode,
@@ -456,35 +479,59 @@ async def search_database(
456
  object_vectors = [v for v in vectors if v["type"] == "object"]
457
 
458
  if detect_faces and face_vectors:
459
- # ── FACE MODE: return grouped results per detected face ──
 
 
 
 
 
 
460
  async def _query_face_group(face_vec: dict) -> dict:
461
  vec_list = face_vec["vector"].tolist() if hasattr(face_vec["vector"], "tolist") else face_vec["vector"]
462
  try:
463
- res = await asyncio.to_thread(idx_face.query,
464
- vector=vec_list, top_k=10, include_metadata=True)
 
 
 
 
 
465
  except Exception as e:
466
  if "404" in str(e):
467
  raise HTTPException(404, "Pinecone index not found. Go to Settings → Verify & Save.")
468
  raise e
469
 
470
- matches = []
471
- seen_urls = set()
 
472
  for match in res.get("matches", []):
473
- score = match["score"]
474
- # ArcFace cosine — threshold 0.35 same as before
475
- if score < 0.35:
 
 
476
  continue
477
- url = match["metadata"].get("url", "")
478
- if url in seen_urls:
 
 
 
 
 
 
479
  continue
480
- seen_urls.add(url)
481
- # Remap score to 75-99% for UI
482
- ui_score = min(0.99, 0.75 + ((score - 0.35) / 0.65) * 0.24)
 
 
 
 
483
  matches.append({
484
- "url": url,
485
  "score": round(ui_score, 4),
486
- "face_crop": match["metadata"].get("face_crop", ""),
487
- "bbox": match["metadata"].get("bbox", ""),
488
  "folder": match["metadata"].get("folder", ""),
489
  "caption": "👤 Verified Identity",
490
  })
@@ -497,10 +544,9 @@ async def search_database(
497
  }
498
 
499
  face_groups = await asyncio.gather(*[_query_face_group(fv) for fv in face_vectors])
500
- # Filter out groups with 0 matches
501
  face_groups = [g for g in face_groups if g["matches"]]
502
 
503
- duration_ms = round((time.perf_counter() - start) * 1000)
504
  total_matches = sum(len(g["matches"]) for g in face_groups)
505
  log("INFO", "search.complete",
506
  user_id=user_id or "anonymous", ip=ip, mode=mode,
@@ -512,7 +558,7 @@ async def search_database(
512
  return {
513
  "mode": "face",
514
  "face_groups": list(face_groups),
515
- "results": [], # empty for backward compat
516
  }
517
 
518
  else:
 
328
  async with _inference_sem:
329
  vectors = await ai.process_image_async(tmp_path, is_query=False, detect_faces=detect_faces)
330
 
331
+ face_upserts = []
332
+ object_upserts = []
333
+
334
  for v in vectors:
335
  vec_list = v["vector"].tolist() if hasattr(v["vector"], "tolist") else v["vector"]
336
  if v["type"] == "face":
337
+ # ── FACE STORE: ArcFace 512-D embedding
338
+ # Metadata includes original image_url so we can
339
+ # retrieve the full image after a face match
340
+ face_upserts.append({
341
+ "id": str(uuid.uuid4()),
342
+ "values": vec_list,
343
+ "metadata": {
344
+ "image_url": image_url, # original full image
345
+ "url": image_url, # alias for compatibility
346
+ "folder": folder,
347
+ "face_idx": v.get("face_idx", 0),
348
+ "bbox": str(v.get("bbox", [])),
349
+ "face_crop": v.get("face_crop", ""), # base64 thumb for UI
350
+ "det_score": v.get("det_score", 1.0),
351
+ }
352
+ })
353
  else:
354
+ # ── OBJECT STORE: SigLIP+DINOv2 1536-D fused embedding
355
+ # Always stores full image — includes all crops + full image
356
+ object_upserts.append({
357
+ "id": str(uuid.uuid4()),
358
+ "values": vec_list,
359
+ "metadata": {
360
+ "image_url": image_url,
361
+ "url": image_url,
362
+ "folder": folder,
363
+ }
364
+ })
365
 
366
+ # Always upsert to BOTH indexes:
367
+ # - face index gets face embeddings (if any faces detected)
368
+ # - object index ALWAYS gets full image embedding
369
  face_vec_total += len(face_upserts)
370
  object_vec_total += len(object_upserts)
371
 
372
  upsert_tasks = []
373
+ if face_upserts:
374
+ upsert_tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_upserts))
375
+ if object_upserts:
376
+ upsert_tasks.append(asyncio.to_thread(idx_obj.upsert, vectors=object_upserts))
377
+ if upsert_tasks:
378
+ await asyncio.gather(*upsert_tasks)
379
 
380
  log("INFO", "upload.file.success",
381
  user_id=user_id or "anonymous", ip=ip, mode=mode,
 
479
  object_vectors = [v for v in vectors if v["type"] == "object"]
480
 
481
  if detect_faces and face_vectors:
482
+ # ══════════════════════════════════════════════════════
483
+ # FACE MODE — Two-step retrieval (from cloud_db.py):
484
+ # Step 1: Query enterprise-faces index with ArcFace vector
485
+ # → get matched image_urls + face_crop thumbnails
486
+ # Step 2: De-duplicate by image_url, rank by face score
487
+ # → return full original images grouped per face
488
+ # ══════════════════════════════════════════════════════
489
  async def _query_face_group(face_vec: dict) -> dict:
490
  vec_list = face_vec["vector"].tolist() if hasattr(face_vec["vector"], "tolist") else face_vec["vector"]
491
  try:
492
+ # Query enterprise-FACES index (512-D ArcFace)
493
+ res = await asyncio.to_thread(
494
+ idx_face.query,
495
+ vector=vec_list,
496
+ top_k=20, # fetch more, filter below
497
+ include_metadata=True,
498
+ )
499
  except Exception as e:
500
  if "404" in str(e):
501
  raise HTTPException(404, "Pinecone index not found. Go to Settings → Verify & Save.")
502
  raise e
503
 
504
+ matches = []
505
+ seen_urls = set()
506
+
507
  for match in res.get("matches", []):
508
+ raw_score = match["score"]
509
+
510
+ # ArcFace cosine similarity threshold
511
+ # 0.35 = same person (from cloud_db.py RAW_THRESHOLD)
512
+ if raw_score < 0.35:
513
  continue
514
+
515
+ # Get the ORIGINAL full image URL (not face crop)
516
+ # Both "url" and "image_url" keys stored for compatibility
517
+ image_url_match = (
518
+ match["metadata"].get("url") or
519
+ match["metadata"].get("image_url", "")
520
+ )
521
+ if not image_url_match or image_url_match in seen_urls:
522
  continue
523
+ seen_urls.add(image_url_match)
524
+
525
+ # Remap raw ArcFace score (0.35–1.0) UI score (75%–99%)
526
+ # Matches cloud_db.py remapping exactly
527
+ ui_score = 0.75 + ((raw_score - 0.35) / (1.0 - 0.35)) * 0.24
528
+ ui_score = min(0.99, ui_score)
529
+
530
  matches.append({
531
+ "url": image_url_match, # full original image
532
  "score": round(ui_score, 4),
533
+ "raw_score": round(raw_score, 4),
534
+ "face_crop": match["metadata"].get("face_crop", ""), # matched face thumb
535
  "folder": match["metadata"].get("folder", ""),
536
  "caption": "👤 Verified Identity",
537
  })
 
544
  }
545
 
546
  face_groups = await asyncio.gather(*[_query_face_group(fv) for fv in face_vectors])
 
547
  face_groups = [g for g in face_groups if g["matches"]]
548
 
549
+ duration_ms = round((time.perf_counter() - start) * 1000)
550
  total_matches = sum(len(g["matches"]) for g in face_groups)
551
  log("INFO", "search.complete",
552
  user_id=user_id or "anonymous", ip=ip, mode=mode,
 
558
  return {
559
  "mode": "face",
560
  "face_groups": list(face_groups),
561
+ "results": [], # empty face mode returns groups only
562
  }
563
 
564
  else:
src/models.py CHANGED
@@ -281,16 +281,23 @@ class AIModelManager:
281
  extracted.append(fr)
282
 
283
  # ── OBJECT LANE ──────────────────────────────────────────
284
- # Always run object lane even if faces found
285
- # (image may contain both people and objects)
286
- crops_pil = [_resize_pil(original_pil, MAX_IMAGE_SIZE)] # full-image always
 
 
 
 
 
 
287
  yolo_results = self.yolo(image_path, conf=0.5, verbose=False)
288
 
289
  for r in yolo_results:
290
  if r.masks is not None:
291
  for seg_idx, mask_xy in enumerate(r.masks.xy):
292
  cls_id = int(r.boxes.cls[seg_idx].item())
293
- # Skip person crops if face lane already handled them
 
294
  if faces_found and cls_id == YOLO_PERSON_CLASS_ID:
295
  print("🔵 PERSON crop skipped — face lane active")
296
  continue
@@ -317,6 +324,7 @@ class AIModelManager:
317
  if len(crops_pil) >= MAX_CROPS + 1:
318
  break
319
 
 
320
  crops = [_resize_pil(c, MAX_IMAGE_SIZE) for c in crops_pil]
321
  print(f"🧠 Embedding {len(crops)} object crop(s) in one batch …")
322
  obj_vecs = self._embed_crops_batch(crops)
 
281
  extracted.append(fr)
282
 
283
  # ── OBJECT LANE ──────────────────────────────────────────
284
+ # ALWAYS runs stores full image + YOLO crops in enterprise-objects
285
+ # This is critical: even when faces found, the full image must be
286
+ # stored in enterprise-objects so face search can retrieve it!
287
+ #
288
+ # Strategy (matches cloud_db.py):
289
+ # - full image ALWAYS included as first crop
290
+ # - YOLO person crops SKIPPED when faces found (avoid duplication)
291
+ # - other object crops always included
292
+ crops_pil = [_resize_pil(original_pil, MAX_IMAGE_SIZE)] # ALWAYS include full image
293
  yolo_results = self.yolo(image_path, conf=0.5, verbose=False)
294
 
295
  for r in yolo_results:
296
  if r.masks is not None:
297
  for seg_idx, mask_xy in enumerate(r.masks.xy):
298
  cls_id = int(r.boxes.cls[seg_idx].item())
299
+ # Skip YOLO person crops when face lane handled faces
300
+ # (avoids storing low-quality person crops redundantly)
301
  if faces_found and cls_id == YOLO_PERSON_CLASS_ID:
302
  print("🔵 PERSON crop skipped — face lane active")
303
  continue
 
324
  if len(crops_pil) >= MAX_CROPS + 1:
325
  break
326
 
327
+ # Embed all crops — full image is ALWAYS first in the list
328
  crops = [_resize_pil(c, MAX_IMAGE_SIZE) for c in crops_pil]
329
  print(f"🧠 Embedding {len(crops)} object crop(s) in one batch …")
330
  obj_vecs = self._embed_crops_batch(crops)