Spaces:
Running
Running
Feat: Face Camera Search Enahced
Browse files- src/api/__pycache__/search.cpython-312.pyc +0 -0
- src/api/search.py +163 -1
src/api/__pycache__/search.cpython-312.pyc
ADDED
|
Binary file (19.7 kB). View file
|
|
|
src/api/search.py
CHANGED
|
@@ -263,4 +263,166 @@ async def _run_object_search(object_vectors, idx_obj, start, user_id, ip, mode)
|
|
| 263 |
user_id=user_id or "anonymous", ip=ip, mode=mode,
|
| 264 |
lanes=["object"], results=len(final), duration_ms=duration_ms)
|
| 265 |
|
| 266 |
-
return {"mode": "object", "results": final, "face_groups": []}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
user_id=user_id or "anonymous", ip=ip, mode=mode,
|
| 264 |
lanes=["object"], results=len(final), duration_ms=duration_ms)
|
| 265 |
|
| 266 |
+
return {"mode": "object", "results": final, "face_groups": []}
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
@router.post("/api/search-by-face")
|
| 270 |
+
async def search_by_face(
|
| 271 |
+
request: Request,
|
| 272 |
+
front: UploadFile = File(...),
|
| 273 |
+
left: UploadFile = File(None),
|
| 274 |
+
right: UploadFile = File(None),
|
| 275 |
+
user_id: str = Form(""),
|
| 276 |
+
keys: dict = Depends(get_verified_keys),
|
| 277 |
+
):
|
| 278 |
+
"""
|
| 279 |
+
Multi-angle face search: accepts 1-3 face images, fuses embeddings server-side,
|
| 280 |
+
performs single Pinecone query. 3x faster + lower quota usage vs 3 sequential queries.
|
| 281 |
+
"""
|
| 282 |
+
import numpy as np
|
| 283 |
+
|
| 284 |
+
ip = get_ip(request)
|
| 285 |
+
start = time.perf_counter()
|
| 286 |
+
mode = "guest" if is_default_key(keys["pinecone_key"], DEFAULT_PINECONE_KEY) else "personal"
|
| 287 |
+
|
| 288 |
+
log("INFO", "search.search_by_face.start",
|
| 289 |
+
user_id=user_id or "anonymous", ip=ip, mode=mode)
|
| 290 |
+
|
| 291 |
+
try:
|
| 292 |
+
ai_manager = request.app.state.ai
|
| 293 |
+
sem = request.app.state.ai_semaphore
|
| 294 |
+
|
| 295 |
+
# Read all image bytes in parallel
|
| 296 |
+
images = {}
|
| 297 |
+
for name, file in [("front", front), ("left", left), ("right", right)]:
|
| 298 |
+
if file:
|
| 299 |
+
images[name] = await file.read()
|
| 300 |
+
|
| 301 |
+
if not images:
|
| 302 |
+
raise HTTPException(400, "At least front image required")
|
| 303 |
+
|
| 304 |
+
# Process all images in parallel
|
| 305 |
+
async def process_img(name, data):
|
| 306 |
+
async with sem:
|
| 307 |
+
return name, await ai_manager.process_image_bytes_async(
|
| 308 |
+
data, detect_faces=True
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
results = await asyncio.gather(
|
| 312 |
+
*[process_img(name, data) for name, data in images.items()],
|
| 313 |
+
return_exceptions=True
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
# Extract face vectors from successful results
|
| 317 |
+
face_vectors_by_angle = {}
|
| 318 |
+
for result in results:
|
| 319 |
+
if isinstance(result, Exception):
|
| 320 |
+
log("WARN", "search.search_by_face.process_error",
|
| 321 |
+
user_id=user_id or "anonymous", ip=ip, error=str(result))
|
| 322 |
+
continue
|
| 323 |
+
|
| 324 |
+
name, vectors = result
|
| 325 |
+
face_vecs = [v for v in vectors if v["type"] == "face"]
|
| 326 |
+
if face_vecs:
|
| 327 |
+
face_vectors_by_angle[name] = face_vecs[0]
|
| 328 |
+
|
| 329 |
+
if not face_vectors_by_angle:
|
| 330 |
+
raise HTTPException(400, "No face detected in provided images")
|
| 331 |
+
|
| 332 |
+
# Fuse embeddings: front weighted higher
|
| 333 |
+
weights = {"front": 0.5, "left": 0.25, "right": 0.25}
|
| 334 |
+
arcface_vectors = []
|
| 335 |
+
adaface_vectors = []
|
| 336 |
+
det_scores = []
|
| 337 |
+
|
| 338 |
+
for angle, vec in face_vectors_by_angle.items():
|
| 339 |
+
w = weights.get(angle, 0)
|
| 340 |
+
if w > 0:
|
| 341 |
+
arcface_vectors.append(np.array(to_list(vec["arcface_vector"])) * w)
|
| 342 |
+
det_scores.append(vec.get("det_score", 1.0))
|
| 343 |
+
|
| 344 |
+
if vec.get("has_adaface") and vec.get("adaface_vector"):
|
| 345 |
+
adaface_vectors.append(np.array(to_list(vec["adaface_vector"])) * w)
|
| 346 |
+
|
| 347 |
+
if not arcface_vectors:
|
| 348 |
+
raise HTTPException(400, "Could not fuse face embeddings")
|
| 349 |
+
|
| 350 |
+
# Fuse and normalize
|
| 351 |
+
fused_arcface = np.sum(arcface_vectors, axis=0)
|
| 352 |
+
fused_arcface = fused_arcface / (np.linalg.norm(fused_arcface) + 1e-7)
|
| 353 |
+
|
| 354 |
+
fused_adaface = None
|
| 355 |
+
has_adaface = False
|
| 356 |
+
if adaface_vectors and len(adaface_vectors) > 0:
|
| 357 |
+
fused_adaface = np.sum(adaface_vectors, axis=0)
|
| 358 |
+
fused_adaface = fused_adaface / (np.linalg.norm(fused_adaface) + 1e-7)
|
| 359 |
+
has_adaface = True
|
| 360 |
+
|
| 361 |
+
# Build synthetic face vector dict for query
|
| 362 |
+
fv = {
|
| 363 |
+
"face_idx": 0,
|
| 364 |
+
"det_score": float(np.mean(det_scores)),
|
| 365 |
+
"arcface_vector": fused_arcface.tolist(),
|
| 366 |
+
"has_adaface": has_adaface,
|
| 367 |
+
"adaface_vector": fused_adaface.tolist() if has_adaface else None,
|
| 368 |
+
"bbox": [0, 0, 0, 0],
|
| 369 |
+
"face_width_px": 0,
|
| 370 |
+
"face_crop": "",
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
inference_ms = round((time.perf_counter() - start) * 1000)
|
| 374 |
+
log("INFO", "search.search_by_face.fused",
|
| 375 |
+
user_id=user_id or "anonymous", ip=ip,
|
| 376 |
+
angles=list(face_vectors_by_angle.keys()),
|
| 377 |
+
inference_ms=inference_ms)
|
| 378 |
+
|
| 379 |
+
pc = pinecone_pool.get(keys["pinecone_key"])
|
| 380 |
+
cluster_uid = hashlib.sha256(keys["pinecone_key"].encode()).hexdigest()[:16]
|
| 381 |
+
|
| 382 |
+
# Ensure indexes exist
|
| 383 |
+
try:
|
| 384 |
+
created = await asyncio.to_thread(ensure_indexes, pc)
|
| 385 |
+
if created:
|
| 386 |
+
log("INFO", "search.indexes_auto_created",
|
| 387 |
+
user_id=user_id or "anonymous", ip=ip, created=created)
|
| 388 |
+
await asyncio.sleep(8)
|
| 389 |
+
except Exception as e:
|
| 390 |
+
log("ERROR", "search.ensure_indexes_failed",
|
| 391 |
+
user_id=user_id or "anonymous", ip=ip, error=str(e))
|
| 392 |
+
|
| 393 |
+
# Setup indexes
|
| 394 |
+
if USE_SPLIT_FACE_INDEXES:
|
| 395 |
+
idx_arcface = pc.Index(IDX_FACES_ARCFACE)
|
| 396 |
+
idx_adaface = pc.Index(IDX_FACES_ADAFACE)
|
| 397 |
+
idx_face_legacy = None
|
| 398 |
+
else:
|
| 399 |
+
idx_face_legacy = pc.Index(IDX_FACES)
|
| 400 |
+
idx_arcface = None
|
| 401 |
+
idx_adaface = None
|
| 402 |
+
|
| 403 |
+
# Query with fused vector
|
| 404 |
+
if USE_SPLIT_FACE_INDEXES:
|
| 405 |
+
face_group = await _query_face_split(fv, idx_arcface, idx_adaface, pc=pc, cluster_uid=cluster_uid)
|
| 406 |
+
else:
|
| 407 |
+
face_group = await _query_face_legacy(fv, idx_face_legacy)
|
| 408 |
+
|
| 409 |
+
duration_ms = round((time.perf_counter() - start) * 1000)
|
| 410 |
+
log("INFO", "search.search_by_face.complete",
|
| 411 |
+
user_id=user_id or "anonymous", ip=ip,
|
| 412 |
+
results=len(face_group.get("matches", [])),
|
| 413 |
+
duration_ms=duration_ms)
|
| 414 |
+
|
| 415 |
+
return {
|
| 416 |
+
"mode": "face",
|
| 417 |
+
"face_groups": [face_group] if face_group.get("matches") else [],
|
| 418 |
+
"results": [],
|
| 419 |
+
"object_results": [],
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
except HTTPException:
|
| 423 |
+
raise
|
| 424 |
+
except Exception as e:
|
| 425 |
+
log("ERROR", "search.search_by_face.error",
|
| 426 |
+
user_id=user_id or "anonymous", ip=ip, mode=mode,
|
| 427 |
+
error=str(e), traceback=traceback.format_exc()[-800:])
|
| 428 |
+
raise HTTPException(500, str(e))
|