Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
|
@@ -25,10 +25,8 @@ MAX_CONCURRENT_INFERENCES = int(os.getenv("MAX_CONCURRENT_INFERENCES", "6"))
|
|
| 25 |
_inference_sem: asyncio.Semaphore
|
| 26 |
|
| 27 |
_pinecone_pool = OrderedDict()
|
| 28 |
-
_cloudinary_pool = {}
|
| 29 |
_POOL_MAX = 64
|
| 30 |
|
| 31 |
-
# FIX 1: Restored your original Pinecone Index names!
|
| 32 |
IDX_FACES = "enterprise-faces"
|
| 33 |
IDX_OBJECTS = "enterprise-objects"
|
| 34 |
|
|
@@ -40,15 +38,10 @@ def _get_pinecone(api_key: str) -> Pinecone:
|
|
| 40 |
_pinecone_pool.move_to_end(api_key)
|
| 41 |
return _pinecone_pool[api_key]
|
| 42 |
|
| 43 |
-
def
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
cloud_name=creds["cloud_name"],
|
| 48 |
-
api_key=creds["api_key"],
|
| 49 |
-
api_secret=creds["api_secret"]
|
| 50 |
-
)
|
| 51 |
-
_cloudinary_pool[key] = True
|
| 52 |
|
| 53 |
@asynccontextmanager
|
| 54 |
async def lifespan(app: FastAPI):
|
|
@@ -74,9 +67,6 @@ def standardize_category_name(name: str) -> str:
|
|
| 74 |
def sanitize_filename(filename: str) -> str:
|
| 75 |
return re.sub(r'[^\w.\-]', '', re.sub(r'\s+', '_', filename))
|
| 76 |
|
| 77 |
-
def get_cloudinary_creds(env_url: str) -> dict:
|
| 78 |
-
parsed = urlparse(env_url)
|
| 79 |
-
return {"api_key": parsed.username, "api_secret": parsed.password, "cloud_name": parsed.hostname}
|
| 80 |
|
| 81 |
# ══════════════════════════════════════════════════════════════════
|
| 82 |
# 1. VERIFY KEYS & AUTO-BUILD INDEXES
|
|
@@ -85,8 +75,9 @@ def get_cloudinary_creds(env_url: str) -> dict:
|
|
| 85 |
async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("")):
|
| 86 |
if cloudinary_url:
|
| 87 |
try:
|
| 88 |
-
|
| 89 |
-
|
|
|
|
| 90 |
except Exception:
|
| 91 |
raise HTTPException(400, "Invalid Cloudinary Environment URL.")
|
| 92 |
if pinecone_key:
|
|
@@ -106,11 +97,10 @@ async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("
|
|
| 106 |
|
| 107 |
|
| 108 |
# ══════════════════════════════════════════════════════════════════
|
| 109 |
-
# 2. UPLOAD (
|
| 110 |
# ══════════════════════════════════════════════════════════════════
|
| 111 |
@app.post("/api/upload")
|
| 112 |
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("")):
|
| 113 |
-
# FIX 2: Uses user keys if provided, otherwise falls back to HF Secrets
|
| 114 |
actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY")
|
| 115 |
actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL")
|
| 116 |
|
|
@@ -119,7 +109,8 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
|
|
| 119 |
|
| 120 |
folder = standardize_category_name(folder_name)
|
| 121 |
uploaded_urls = []
|
| 122 |
-
|
|
|
|
| 123 |
pc = _get_pinecone(actual_pc_key)
|
| 124 |
idx_obj = pc.Index(IDX_OBJECTS)
|
| 125 |
idx_face = pc.Index(IDX_FACES)
|
|
@@ -130,7 +121,7 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
|
|
| 130 |
with open(tmp_path, "wb") as buf:
|
| 131 |
shutil.copyfileobj(file.file, buf)
|
| 132 |
|
| 133 |
-
res = await asyncio.to_thread(cloudinary.uploader.upload, tmp_path, folder=folder)
|
| 134 |
image_url = res["secure_url"]
|
| 135 |
uploaded_urls.append(image_url)
|
| 136 |
|
|
@@ -147,8 +138,12 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
|
|
| 147 |
if face_upserts: upsert_tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_upserts))
|
| 148 |
if object_upserts: upsert_tasks.append(asyncio.to_thread(idx_obj.upsert, vectors=object_upserts))
|
| 149 |
if upsert_tasks: await asyncio.gather(*upsert_tasks)
|
|
|
|
| 150 |
except Exception as e:
|
|
|
|
|
|
|
| 151 |
print(f"❌ Upload error: {e}")
|
|
|
|
| 152 |
finally:
|
| 153 |
if os.path.exists(tmp_path): os.remove(tmp_path)
|
| 154 |
|
|
@@ -156,7 +151,7 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
|
|
| 156 |
|
| 157 |
|
| 158 |
# ══════════════════════════════════════════════════════════════════
|
| 159 |
-
# 3. SEARCH (
|
| 160 |
# ══════════════════════════════════════════════════════════════════
|
| 161 |
@app.post("/api/search")
|
| 162 |
async def search_database(file: UploadFile = File(...), detect_faces: bool = Form(True), user_pinecone_key: str = Form(""), user_cloudinary_url: str = Form("")):
|
|
@@ -180,14 +175,7 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
|
|
| 180 |
vec_list = vec_dict["vector"].tolist() if hasattr(vec_dict["vector"], "tolist") else vec_dict["vector"]
|
| 181 |
target_idx = idx_face if vec_dict["type"] == "face" else idx_obj
|
| 182 |
|
| 183 |
-
|
| 184 |
-
res = await asyncio.to_thread(target_idx.query, vector=vec_list, top_k=10, include_metadata=True)
|
| 185 |
-
except Exception as e:
|
| 186 |
-
if "404" in str(e):
|
| 187 |
-
# Graceful error if index truly doesn't exist
|
| 188 |
-
raise HTTPException(404, f"Pinecone Index not found. Please log in and click 'Verify Keys' in Settings to build the indexes.")
|
| 189 |
-
raise e
|
| 190 |
-
|
| 191 |
out = []
|
| 192 |
for match in res.get("matches", []):
|
| 193 |
caption = "👤 Verified Identity" if vec_dict["type"] == "face" else match["metadata"].get("folder", "🎯 Object Match")
|
|
@@ -204,9 +192,12 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
|
|
| 204 |
seen[url] = r
|
| 205 |
|
| 206 |
return {"results": sorted(seen.values(), key=lambda x: x["score"], reverse=True)[:10]}
|
|
|
|
| 207 |
except HTTPException:
|
| 208 |
raise
|
| 209 |
except Exception as e:
|
|
|
|
|
|
|
| 210 |
print(f"❌ Search error: {e}")
|
| 211 |
raise HTTPException(500, str(e))
|
| 212 |
finally:
|
|
@@ -214,7 +205,7 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
|
|
| 214 |
|
| 215 |
|
| 216 |
# ══════════════════════════════════════════════════════════════════
|
| 217 |
-
# 4. CATEGORIES (
|
| 218 |
# ══════════════════════════════════════════════════════════════════
|
| 219 |
@app.post("/api/categories")
|
| 220 |
async def get_categories(user_cloudinary_url: str = Form("")):
|
|
@@ -223,9 +214,8 @@ async def get_categories(user_cloudinary_url: str = Form("")):
|
|
| 223 |
return {"categories": []}
|
| 224 |
|
| 225 |
try:
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
result = await asyncio.to_thread(cloudinary.api.root_folders)
|
| 229 |
return {"categories": [f["name"] for f in result.get("folders", [])]}
|
| 230 |
except Exception as e:
|
| 231 |
print(f"Category fetch error: {e}")
|
|
|
|
| 25 |
_inference_sem: asyncio.Semaphore
|
| 26 |
|
| 27 |
_pinecone_pool = OrderedDict()
|
|
|
|
| 28 |
_POOL_MAX = 64
|
| 29 |
|
|
|
|
| 30 |
IDX_FACES = "enterprise-faces"
|
| 31 |
IDX_OBJECTS = "enterprise-objects"
|
| 32 |
|
|
|
|
| 38 |
_pinecone_pool.move_to_end(api_key)
|
| 39 |
return _pinecone_pool[api_key]
|
| 40 |
|
| 41 |
+
def get_cld_kwargs(env_url: str) -> dict:
|
| 42 |
+
"""Extracts credentials to be passed state-lessly to prevent cross-user data bleed."""
|
| 43 |
+
parsed = urlparse(env_url)
|
| 44 |
+
return {"api_key": parsed.username, "api_secret": parsed.password, "cloud_name": parsed.hostname}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
@asynccontextmanager
|
| 47 |
async def lifespan(app: FastAPI):
|
|
|
|
| 67 |
def sanitize_filename(filename: str) -> str:
|
| 68 |
return re.sub(r'[^\w.\-]', '', re.sub(r'\s+', '_', filename))
|
| 69 |
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
# ══════════════════════════════════════════════════════════════════
|
| 72 |
# 1. VERIFY KEYS & AUTO-BUILD INDEXES
|
|
|
|
| 75 |
async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("")):
|
| 76 |
if cloudinary_url:
|
| 77 |
try:
|
| 78 |
+
# Stateless ping to check credentials
|
| 79 |
+
cld_kwargs = get_cld_kwargs(cloudinary_url)
|
| 80 |
+
await asyncio.to_thread(cloudinary.api.root_folders, max_results=1, **cld_kwargs)
|
| 81 |
except Exception:
|
| 82 |
raise HTTPException(400, "Invalid Cloudinary Environment URL.")
|
| 83 |
if pinecone_key:
|
|
|
|
| 97 |
|
| 98 |
|
| 99 |
# ══════════════════════════════════════════════════════════════════
|
| 100 |
+
# 2. UPLOAD (Stateless + Auto-Catch 404s)
|
| 101 |
# ══════════════════════════════════════════════════════════════════
|
| 102 |
@app.post("/api/upload")
|
| 103 |
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("")):
|
|
|
|
| 104 |
actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY")
|
| 105 |
actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL")
|
| 106 |
|
|
|
|
| 109 |
|
| 110 |
folder = standardize_category_name(folder_name)
|
| 111 |
uploaded_urls = []
|
| 112 |
+
cld_kwargs = get_cld_kwargs(actual_cld_url)
|
| 113 |
+
|
| 114 |
pc = _get_pinecone(actual_pc_key)
|
| 115 |
idx_obj = pc.Index(IDX_OBJECTS)
|
| 116 |
idx_face = pc.Index(IDX_FACES)
|
|
|
|
| 121 |
with open(tmp_path, "wb") as buf:
|
| 122 |
shutil.copyfileobj(file.file, buf)
|
| 123 |
|
| 124 |
+
res = await asyncio.to_thread(cloudinary.uploader.upload, tmp_path, folder=folder, **cld_kwargs)
|
| 125 |
image_url = res["secure_url"]
|
| 126 |
uploaded_urls.append(image_url)
|
| 127 |
|
|
|
|
| 138 |
if face_upserts: upsert_tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_upserts))
|
| 139 |
if object_upserts: upsert_tasks.append(asyncio.to_thread(idx_obj.upsert, vectors=object_upserts))
|
| 140 |
if upsert_tasks: await asyncio.gather(*upsert_tasks)
|
| 141 |
+
|
| 142 |
except Exception as e:
|
| 143 |
+
if "404" in str(e) or "NOT_FOUND" in str(e):
|
| 144 |
+
raise HTTPException(400, "Pinecone indexes missing. Please go to Settings and click 'Verify & Save' to build them.")
|
| 145 |
print(f"❌ Upload error: {e}")
|
| 146 |
+
raise HTTPException(500, f"Upload error: {e}")
|
| 147 |
finally:
|
| 148 |
if os.path.exists(tmp_path): os.remove(tmp_path)
|
| 149 |
|
|
|
|
| 151 |
|
| 152 |
|
| 153 |
# ══════════════════════════════════════════════════════════════════
|
| 154 |
+
# 3. SEARCH (Stateless + Auto-Catch 404s)
|
| 155 |
# ══════════════════════════════════════════════════════════════════
|
| 156 |
@app.post("/api/search")
|
| 157 |
async def search_database(file: UploadFile = File(...), detect_faces: bool = Form(True), user_pinecone_key: str = Form(""), user_cloudinary_url: str = Form("")):
|
|
|
|
| 175 |
vec_list = vec_dict["vector"].tolist() if hasattr(vec_dict["vector"], "tolist") else vec_dict["vector"]
|
| 176 |
target_idx = idx_face if vec_dict["type"] == "face" else idx_obj
|
| 177 |
|
| 178 |
+
res = await asyncio.to_thread(target_idx.query, vector=vec_list, top_k=10, include_metadata=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
out = []
|
| 180 |
for match in res.get("matches", []):
|
| 181 |
caption = "👤 Verified Identity" if vec_dict["type"] == "face" else match["metadata"].get("folder", "🎯 Object Match")
|
|
|
|
| 192 |
seen[url] = r
|
| 193 |
|
| 194 |
return {"results": sorted(seen.values(), key=lambda x: x["score"], reverse=True)[:10]}
|
| 195 |
+
|
| 196 |
except HTTPException:
|
| 197 |
raise
|
| 198 |
except Exception as e:
|
| 199 |
+
if "404" in str(e) or "NOT_FOUND" in str(e):
|
| 200 |
+
raise HTTPException(400, "Pinecone indexes missing. Please go to Settings and click 'Verify & Save' to build them.")
|
| 201 |
print(f"❌ Search error: {e}")
|
| 202 |
raise HTTPException(500, str(e))
|
| 203 |
finally:
|
|
|
|
| 205 |
|
| 206 |
|
| 207 |
# ══════════════════════════════════════════════════════════════════
|
| 208 |
+
# 4. CATEGORIES (Stateless Isolation)
|
| 209 |
# ══════════════════════════════════════════════════════════════════
|
| 210 |
@app.post("/api/categories")
|
| 211 |
async def get_categories(user_cloudinary_url: str = Form("")):
|
|
|
|
| 214 |
return {"categories": []}
|
| 215 |
|
| 216 |
try:
|
| 217 |
+
cld_kwargs = get_cld_kwargs(actual_cld_url)
|
| 218 |
+
result = await asyncio.to_thread(cloudinary.api.root_folders, **cld_kwargs)
|
|
|
|
| 219 |
return {"categories": [f["name"] for f in result.get("folders", [])]}
|
| 220 |
except Exception as e:
|
| 221 |
print(f"Category fetch error: {e}")
|