Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -38,20 +38,21 @@ def _get_pinecone(api_key: str) -> Pinecone:
|
|
| 38 |
_pinecone_pool.move_to_end(api_key)
|
| 39 |
return _pinecone_pool[api_key]
|
| 40 |
|
| 41 |
-
# Cloudinary
|
| 42 |
-
#
|
| 43 |
-
|
|
|
|
| 44 |
return cloudinary.uploader.upload(
|
| 45 |
tmp_path, folder=folder,
|
| 46 |
api_key=creds["api_key"], api_secret=creds["api_secret"], cloud_name=creds["cloud_name"],
|
| 47 |
)
|
| 48 |
|
| 49 |
-
def _cld_ping(creds
|
| 50 |
return cloudinary.api.ping(
|
| 51 |
api_key=creds["api_key"], api_secret=creds["api_secret"], cloud_name=creds["cloud_name"],
|
| 52 |
)
|
| 53 |
|
| 54 |
-
def _cld_root_folders(creds
|
| 55 |
return cloudinary.api.root_folders(
|
| 56 |
api_key=creds["api_key"], api_secret=creds["api_secret"], cloud_name=creds["cloud_name"],
|
| 57 |
)
|
|
@@ -93,9 +94,9 @@ def get_cloudinary_creds(env_url: str) -> dict:
|
|
| 93 |
async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("")):
|
| 94 |
if cloudinary_url:
|
| 95 |
try:
|
| 96 |
-
|
| 97 |
-
if not
|
| 98 |
-
await asyncio.to_thread(_cld_ping,
|
| 99 |
except HTTPException: raise
|
| 100 |
except Exception:
|
| 101 |
raise HTTPException(400, "Invalid Cloudinary Environment URL.")
|
|
@@ -120,13 +121,12 @@ async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("
|
|
| 120 |
# ══════════════════════════════════════════════════════════════════
|
| 121 |
@app.post("/api/upload")
|
| 122 |
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("")):
|
| 123 |
-
#
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
if not actual_pc_key or not actual_cld_url:
|
| 129 |
-
raise HTTPException(400, "API
|
| 130 |
|
| 131 |
folder = standardize_category_name(folder_name)
|
| 132 |
uploaded_urls = []
|
|
@@ -135,8 +135,8 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
|
|
| 135 |
if not creds.get("cloud_name"):
|
| 136 |
raise HTTPException(400, "Invalid Cloudinary URL format.")
|
| 137 |
|
| 138 |
-
pc
|
| 139 |
-
idx_obj
|
| 140 |
idx_face = pc.Index(IDX_FACES)
|
| 141 |
|
| 142 |
for file in files:
|
|
@@ -144,7 +144,7 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
|
|
| 144 |
try:
|
| 145 |
with open(tmp_path, "wb") as buf:
|
| 146 |
shutil.copyfileobj(file.file, buf)
|
| 147 |
-
|
| 148 |
res = await asyncio.to_thread(_cld_upload, tmp_path, folder, creds)
|
| 149 |
image_url = res["secure_url"]
|
| 150 |
uploaded_urls.append(image_url)
|
|
@@ -176,9 +176,9 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
|
|
| 176 |
# ══════════════════════════════════════════════════════════════════
|
| 177 |
@app.post("/api/search")
|
| 178 |
async def search_database(file: UploadFile = File(...), detect_faces: bool = Form(True), user_pinecone_key: str = Form(""), user_cloudinary_url: str = Form("")):
|
| 179 |
-
actual_pc_key =
|
| 180 |
if not actual_pc_key:
|
| 181 |
-
raise HTTPException(400, "Pinecone
|
| 182 |
|
| 183 |
tmp_path = f"temp_uploads/query_{uuid.uuid4().hex}_{sanitize_filename(file.filename)}"
|
| 184 |
try:
|
|
@@ -205,8 +205,37 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
|
|
| 205 |
|
| 206 |
out = []
|
| 207 |
for match in res.get("matches", []):
|
| 208 |
-
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
return out
|
| 211 |
|
| 212 |
nested = await asyncio.gather(*[_query_one(v) for v in vectors])
|
|
@@ -233,7 +262,7 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
|
|
| 233 |
# ══════════════════════════════════════════════════════════════════
|
| 234 |
@app.post("/api/categories")
|
| 235 |
async def get_categories(user_cloudinary_url: str = Form("")):
|
| 236 |
-
actual_cld_url =
|
| 237 |
if not actual_cld_url:
|
| 238 |
return {"categories": []}
|
| 239 |
|
|
|
|
| 38 |
_pinecone_pool.move_to_end(api_key)
|
| 39 |
return _pinecone_pool[api_key]
|
| 40 |
|
| 41 |
+
# ── Cloudinary: credentials injected per-call, NEVER globally configured.
|
| 42 |
+
# If cloudinary.config() is called once, it applies to the whole process —
|
| 43 |
+
# User A's credentials would bleed into User B's request under concurrency.
|
| 44 |
+
def _cld_upload(tmp_path, folder, creds):
|
| 45 |
return cloudinary.uploader.upload(
|
| 46 |
tmp_path, folder=folder,
|
| 47 |
api_key=creds["api_key"], api_secret=creds["api_secret"], cloud_name=creds["cloud_name"],
|
| 48 |
)
|
| 49 |
|
| 50 |
+
def _cld_ping(creds):
|
| 51 |
return cloudinary.api.ping(
|
| 52 |
api_key=creds["api_key"], api_secret=creds["api_secret"], cloud_name=creds["cloud_name"],
|
| 53 |
)
|
| 54 |
|
| 55 |
+
def _cld_root_folders(creds):
|
| 56 |
return cloudinary.api.root_folders(
|
| 57 |
api_key=creds["api_key"], api_secret=creds["api_secret"], cloud_name=creds["cloud_name"],
|
| 58 |
)
|
|
|
|
| 94 |
async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("")):
|
| 95 |
if cloudinary_url:
|
| 96 |
try:
|
| 97 |
+
creds_v = get_cloudinary_creds(cloudinary_url)
|
| 98 |
+
if not creds_v.get("cloud_name"): raise ValueError("bad url")
|
| 99 |
+
await asyncio.to_thread(_cld_ping, creds_v)
|
| 100 |
except HTTPException: raise
|
| 101 |
except Exception:
|
| 102 |
raise HTTPException(400, "Invalid Cloudinary Environment URL.")
|
|
|
|
| 121 |
# ══════════════════════════════════════════════════════════════════
|
| 122 |
@app.post("/api/upload")
|
| 123 |
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("")):
|
| 124 |
+
# DEFENSIVE FIX: The 'or ""' ensures it never becomes None, preventing 500 crashes
|
| 125 |
+
actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY", "")
|
| 126 |
+
actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL", "")
|
| 127 |
+
|
|
|
|
| 128 |
if not actual_pc_key or not actual_cld_url:
|
| 129 |
+
raise HTTPException(400, "API Keys are missing. If you are a guest, the server is missing its DEFAULT_ secrets in Hugging Face.")
|
| 130 |
|
| 131 |
folder = standardize_category_name(folder_name)
|
| 132 |
uploaded_urls = []
|
|
|
|
| 135 |
if not creds.get("cloud_name"):
|
| 136 |
raise HTTPException(400, "Invalid Cloudinary URL format.")
|
| 137 |
|
| 138 |
+
pc = _get_pinecone(actual_pc_key)
|
| 139 |
+
idx_obj = pc.Index(IDX_OBJECTS)
|
| 140 |
idx_face = pc.Index(IDX_FACES)
|
| 141 |
|
| 142 |
for file in files:
|
|
|
|
| 144 |
try:
|
| 145 |
with open(tmp_path, "wb") as buf:
|
| 146 |
shutil.copyfileobj(file.file, buf)
|
| 147 |
+
|
| 148 |
res = await asyncio.to_thread(_cld_upload, tmp_path, folder, creds)
|
| 149 |
image_url = res["secure_url"]
|
| 150 |
uploaded_urls.append(image_url)
|
|
|
|
| 176 |
# ══════════════════════════════════════════════════════════════════
|
| 177 |
@app.post("/api/search")
|
| 178 |
async def search_database(file: UploadFile = File(...), detect_faces: bool = Form(True), user_pinecone_key: str = Form(""), user_cloudinary_url: str = Form("")):
|
| 179 |
+
actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY", "")
|
| 180 |
if not actual_pc_key:
|
| 181 |
+
raise HTTPException(400, "Pinecone Key is missing. If you are a guest, the server is missing its DEFAULT_PINECONE_KEY in Hugging Face.")
|
| 182 |
|
| 183 |
tmp_path = f"temp_uploads/query_{uuid.uuid4().hex}_{sanitize_filename(file.filename)}"
|
| 184 |
try:
|
|
|
|
| 205 |
|
| 206 |
out = []
|
| 207 |
for match in res.get("matches", []):
|
| 208 |
+
score = match["score"]
|
| 209 |
+
is_face = vec_dict["type"] == "face"
|
| 210 |
+
|
| 211 |
+
# ── Score filtering ──────────────────────────────────────
|
| 212 |
+
# Face lane: RetinaFace+GhostFaceNet 512-D cosine similarity.
|
| 213 |
+
# Raw scores cluster around 0.3-0.5 for same person; anything
|
| 214 |
+
# below 0.30 is noise. We remap to 75-99% for the UI.
|
| 215 |
+
if is_face:
|
| 216 |
+
RAW_THRESHOLD = 0.30
|
| 217 |
+
if score < RAW_THRESHOLD:
|
| 218 |
+
continue
|
| 219 |
+
ui_score = 0.75 + ((score - RAW_THRESHOLD) / (1.0 - RAW_THRESHOLD)) * 0.24
|
| 220 |
+
ui_score = min(0.99, ui_score)
|
| 221 |
+
else:
|
| 222 |
+
# Object lane: SigLIP+DINOv2 1536-D fused cosine similarity.
|
| 223 |
+
# These vectors are dense and high-dimensional — even completely
|
| 224 |
+
# different images can score 0.70-0.80 just by chance.
|
| 225 |
+
# 0.82+ = genuinely similar images (same animal, same scene type)
|
| 226 |
+
# 0.70-0.82 = loosely related (outdoor vs outdoor, animal vs animal)
|
| 227 |
+
# < 0.70 = unrelated — hard filter these out entirely
|
| 228 |
+
MIN_OBJECT_SCORE = 0.70
|
| 229 |
+
if score < MIN_OBJECT_SCORE:
|
| 230 |
+
continue
|
| 231 |
+
ui_score = score
|
| 232 |
+
|
| 233 |
+
caption = "👤 Verified Identity" if is_face else match["metadata"].get("folder", "🎯 Object Match")
|
| 234 |
+
out.append({
|
| 235 |
+
"url": match["metadata"].get("url", ""),
|
| 236 |
+
"score": round(ui_score, 4),
|
| 237 |
+
"caption": caption,
|
| 238 |
+
})
|
| 239 |
return out
|
| 240 |
|
| 241 |
nested = await asyncio.gather(*[_query_one(v) for v in vectors])
|
|
|
|
| 262 |
# ══════════════════════════════════════════════════════════════════
|
| 263 |
@app.post("/api/categories")
|
| 264 |
async def get_categories(user_cloudinary_url: str = Form("")):
|
| 265 |
+
actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL", "")
|
| 266 |
if not actual_cld_url:
|
| 267 |
return {"categories": []}
|
| 268 |
|