Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
|
@@ -25,6 +25,7 @@ MAX_CONCURRENT_INFERENCES = int(os.getenv("MAX_CONCURRENT_INFERENCES", "6"))
|
|
| 25 |
_inference_sem: asyncio.Semaphore
|
| 26 |
|
| 27 |
_pinecone_pool = OrderedDict()
|
|
|
|
| 28 |
_POOL_MAX = 64
|
| 29 |
|
| 30 |
IDX_FACES = "enterprise-faces"
|
|
@@ -38,10 +39,16 @@ def _get_pinecone(api_key: str) -> Pinecone:
|
|
| 38 |
_pinecone_pool.move_to_end(api_key)
|
| 39 |
return _pinecone_pool[api_key]
|
| 40 |
|
| 41 |
-
def
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
@asynccontextmanager
|
| 47 |
async def lifespan(app: FastAPI):
|
|
@@ -67,6 +74,11 @@ def standardize_category_name(name: str) -> str:
|
|
| 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,9 +87,8 @@ def sanitize_filename(filename: str) -> str:
|
|
| 75 |
async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("")):
|
| 76 |
if cloudinary_url:
|
| 77 |
try:
|
| 78 |
-
|
| 79 |
-
|
| 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,20 +108,25 @@ async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("
|
|
| 97 |
|
| 98 |
|
| 99 |
# ══════════════════════════════════════════════════════════════════
|
| 100 |
-
# 2. UPLOAD
|
| 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 |
-
|
| 105 |
-
|
|
|
|
| 106 |
|
| 107 |
if not actual_pc_key or not actual_cld_url:
|
| 108 |
-
raise HTTPException(400, "API Keys are
|
| 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,7 +137,7 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
|
|
| 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
|
| 125 |
image_url = res["secure_url"]
|
| 126 |
uploaded_urls.append(image_url)
|
| 127 |
|
|
@@ -138,12 +154,9 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
|
|
| 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
|
| 147 |
finally:
|
| 148 |
if os.path.exists(tmp_path): os.remove(tmp_path)
|
| 149 |
|
|
@@ -151,13 +164,13 @@ async def upload_new_images(files: List[UploadFile] = File(...), folder_name: st
|
|
| 151 |
|
| 152 |
|
| 153 |
# ══════════════════════════════════════════════════════════════════
|
| 154 |
-
# 3. SEARCH
|
| 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("")):
|
| 158 |
-
actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY")
|
| 159 |
if not actual_pc_key:
|
| 160 |
-
raise HTTPException(400, "Pinecone Key is
|
| 161 |
|
| 162 |
tmp_path = f"temp_uploads/query_{uuid.uuid4().hex}_{sanitize_filename(file.filename)}"
|
| 163 |
try:
|
|
@@ -175,7 +188,13 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,12 +211,9 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
|
|
| 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,17 +221,21 @@ async def search_database(file: UploadFile = File(...), detect_faces: bool = For
|
|
| 205 |
|
| 206 |
|
| 207 |
# ══════════════════════════════════════════════════════════════════
|
| 208 |
-
# 4. CATEGORIES
|
| 209 |
# ══════════════════════════════════════════════════════════════════
|
| 210 |
@app.post("/api/categories")
|
| 211 |
async def get_categories(user_cloudinary_url: str = Form("")):
|
| 212 |
-
actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL")
|
| 213 |
if not actual_cld_url:
|
| 214 |
return {"categories": []}
|
| 215 |
|
| 216 |
try:
|
| 217 |
-
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
return {"categories": [f["name"] for f in result.get("folders", [])]}
|
| 220 |
except Exception as e:
|
| 221 |
print(f"Category fetch error: {e}")
|
|
|
|
| 25 |
_inference_sem: asyncio.Semaphore
|
| 26 |
|
| 27 |
_pinecone_pool = OrderedDict()
|
| 28 |
+
_cloudinary_pool = {}
|
| 29 |
_POOL_MAX = 64
|
| 30 |
|
| 31 |
IDX_FACES = "enterprise-faces"
|
|
|
|
| 39 |
_pinecone_pool.move_to_end(api_key)
|
| 40 |
return _pinecone_pool[api_key]
|
| 41 |
|
| 42 |
+
def _configure_cloudinary(creds: dict):
|
| 43 |
+
key = creds.get("cloud_name")
|
| 44 |
+
if not key: return
|
| 45 |
+
if key not in _cloudinary_pool:
|
| 46 |
+
cloudinary.config(
|
| 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 |
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 |
+
if not env_url:
|
| 79 |
+
return {}
|
| 80 |
+
parsed = urlparse(env_url)
|
| 81 |
+
return {"api_key": parsed.username, "api_secret": parsed.password, "cloud_name": parsed.hostname}
|
| 82 |
|
| 83 |
# ══════════════════════════════════════════════════════════════════
|
| 84 |
# 1. VERIFY KEYS & AUTO-BUILD INDEXES
|
|
|
|
| 87 |
async def verify_keys(pinecone_key: str = Form(""), cloudinary_url: str = Form("")):
|
| 88 |
if cloudinary_url:
|
| 89 |
try:
|
| 90 |
+
_configure_cloudinary(get_cloudinary_creds(cloudinary_url))
|
| 91 |
+
await asyncio.to_thread(cloudinary.api.ping)
|
|
|
|
| 92 |
except Exception:
|
| 93 |
raise HTTPException(400, "Invalid Cloudinary Environment URL.")
|
| 94 |
if pinecone_key:
|
|
|
|
| 108 |
|
| 109 |
|
| 110 |
# ══════════════════════════════════════════════════════════════════
|
| 111 |
+
# 2. UPLOAD
|
| 112 |
# ══════════════════════════════════════════════════════════════════
|
| 113 |
@app.post("/api/upload")
|
| 114 |
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("")):
|
| 115 |
+
# DEFENSIVE FIX: The 'or ""' ensures it never becomes None, preventing 500 crashes
|
| 116 |
+
actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY", "")
|
| 117 |
+
actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL", "")
|
| 118 |
|
| 119 |
if not actual_pc_key or not actual_cld_url:
|
| 120 |
+
raise HTTPException(400, "API Keys are missing. If you are a guest, the server is missing its DEFAULT_ secrets in Hugging Face.")
|
| 121 |
|
| 122 |
folder = standardize_category_name(folder_name)
|
| 123 |
uploaded_urls = []
|
|
|
|
| 124 |
|
| 125 |
+
creds = get_cloudinary_creds(actual_cld_url)
|
| 126 |
+
if not creds.get("cloud_name"):
|
| 127 |
+
raise HTTPException(400, "Invalid Cloudinary URL format.")
|
| 128 |
+
|
| 129 |
+
_configure_cloudinary(creds)
|
| 130 |
pc = _get_pinecone(actual_pc_key)
|
| 131 |
idx_obj = pc.Index(IDX_OBJECTS)
|
| 132 |
idx_face = pc.Index(IDX_FACES)
|
|
|
|
| 137 |
with open(tmp_path, "wb") as buf:
|
| 138 |
shutil.copyfileobj(file.file, buf)
|
| 139 |
|
| 140 |
+
res = await asyncio.to_thread(cloudinary.uploader.upload, tmp_path, folder=folder)
|
| 141 |
image_url = res["secure_url"]
|
| 142 |
uploaded_urls.append(image_url)
|
| 143 |
|
|
|
|
| 154 |
if face_upserts: upsert_tasks.append(asyncio.to_thread(idx_face.upsert, vectors=face_upserts))
|
| 155 |
if object_upserts: upsert_tasks.append(asyncio.to_thread(idx_obj.upsert, vectors=object_upserts))
|
| 156 |
if upsert_tasks: await asyncio.gather(*upsert_tasks)
|
|
|
|
| 157 |
except Exception as e:
|
|
|
|
|
|
|
| 158 |
print(f"❌ Upload error: {e}")
|
| 159 |
+
raise HTTPException(500, f"Upload processing failed: {str(e)}")
|
| 160 |
finally:
|
| 161 |
if os.path.exists(tmp_path): os.remove(tmp_path)
|
| 162 |
|
|
|
|
| 164 |
|
| 165 |
|
| 166 |
# ══════════════════════════════════════════════════════════════════
|
| 167 |
+
# 3. SEARCH
|
| 168 |
# ══════════════════════════════════════════════════════════════════
|
| 169 |
@app.post("/api/search")
|
| 170 |
async def search_database(file: UploadFile = File(...), detect_faces: bool = Form(True), user_pinecone_key: str = Form(""), user_cloudinary_url: str = Form("")):
|
| 171 |
+
actual_pc_key = user_pinecone_key or os.getenv("DEFAULT_PINECONE_KEY", "")
|
| 172 |
if not actual_pc_key:
|
| 173 |
+
raise HTTPException(400, "Pinecone Key is missing. If you are a guest, the server is missing its DEFAULT_PINECONE_KEY in Hugging Face.")
|
| 174 |
|
| 175 |
tmp_path = f"temp_uploads/query_{uuid.uuid4().hex}_{sanitize_filename(file.filename)}"
|
| 176 |
try:
|
|
|
|
| 188 |
vec_list = vec_dict["vector"].tolist() if hasattr(vec_dict["vector"], "tolist") else vec_dict["vector"]
|
| 189 |
target_idx = idx_face if vec_dict["type"] == "face" else idx_obj
|
| 190 |
|
| 191 |
+
try:
|
| 192 |
+
res = await asyncio.to_thread(target_idx.query, vector=vec_list, top_k=10, include_metadata=True)
|
| 193 |
+
except Exception as e:
|
| 194 |
+
if "404" in str(e):
|
| 195 |
+
raise HTTPException(404, f"Pinecone Index not found. Please log in and click 'Verify Keys' in Settings to build the indexes.")
|
| 196 |
+
raise e
|
| 197 |
+
|
| 198 |
out = []
|
| 199 |
for match in res.get("matches", []):
|
| 200 |
caption = "👤 Verified Identity" if vec_dict["type"] == "face" else match["metadata"].get("folder", "🎯 Object Match")
|
|
|
|
| 211 |
seen[url] = r
|
| 212 |
|
| 213 |
return {"results": sorted(seen.values(), key=lambda x: x["score"], reverse=True)[:10]}
|
|
|
|
| 214 |
except HTTPException:
|
| 215 |
raise
|
| 216 |
except Exception as e:
|
|
|
|
|
|
|
| 217 |
print(f"❌ Search error: {e}")
|
| 218 |
raise HTTPException(500, str(e))
|
| 219 |
finally:
|
|
|
|
| 221 |
|
| 222 |
|
| 223 |
# ══════════════════════════════════════════════════════════════════
|
| 224 |
+
# 4. CATEGORIES
|
| 225 |
# ══════════════════════════════════════════════════════════════════
|
| 226 |
@app.post("/api/categories")
|
| 227 |
async def get_categories(user_cloudinary_url: str = Form("")):
|
| 228 |
+
actual_cld_url = user_cloudinary_url or os.getenv("DEFAULT_CLOUDINARY_URL", "")
|
| 229 |
if not actual_cld_url:
|
| 230 |
return {"categories": []}
|
| 231 |
|
| 232 |
try:
|
| 233 |
+
creds = get_cloudinary_creds(actual_cld_url)
|
| 234 |
+
if not creds.get("cloud_name"):
|
| 235 |
+
return {"categories": []}
|
| 236 |
+
|
| 237 |
+
_configure_cloudinary(creds)
|
| 238 |
+
result = await asyncio.to_thread(cloudinary.api.root_folders)
|
| 239 |
return {"categories": [f["name"] for f in result.get("folders", [])]}
|
| 240 |
except Exception as e:
|
| 241 |
print(f"Category fetch error: {e}")
|