Spaces:
Sleeping
Sleeping
fix : database pinecone issue
Browse files- main.py +81 -35
- 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
|
|
|
|
|
|
|
| 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 |
-
#
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
"
|
| 340 |
-
"
|
| 341 |
-
"
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
else:
|
| 346 |
-
|
| 347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
|
|
|
|
|
|
|
|
|
|
| 349 |
face_vec_total += len(face_upserts)
|
| 350 |
object_vec_total += len(object_upserts)
|
| 351 |
|
| 352 |
upsert_tasks = []
|
| 353 |
-
if face_upserts:
|
| 354 |
-
|
| 355 |
-
if
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 464 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 472 |
for match in res.get("matches", []):
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
|
|
|
|
|
|
| 476 |
continue
|
| 477 |
-
|
| 478 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
continue
|
| 480 |
-
seen_urls.add(
|
| 481 |
-
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
matches.append({
|
| 484 |
-
"url":
|
| 485 |
"score": round(ui_score, 4),
|
| 486 |
-
"
|
| 487 |
-
"
|
| 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
|
| 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
|
| 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 |
-
#
|
| 285 |
-
#
|
| 286 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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)
|