Spaces:
Running
Running
| import json | |
| import logging | |
| import os | |
| import subprocess | |
| import tempfile | |
| import time | |
| import uuid | |
| from pathlib import Path | |
| from fastapi import FastAPI, File, Form, UploadFile, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from pydantic import BaseModel | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger("locate-anything-server") | |
| app = FastAPI(title="LocateAnything Detection API") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| MODEL_PATH = os.environ.get("MODEL_PATH", "/app/models/locate-anything-q4_k.gguf") | |
| INFERENCE_MODE = os.environ.get("INFERENCE_MODE", "hybrid") | |
| N_THREADS = os.environ.get("N_THREADS", "2") | |
| _available = Path(MODEL_PATH).exists() | |
| class DetectionResult(BaseModel): | |
| label: str | |
| box: list[float] # [x1, y1, x2, y2] normalized 0-1 | |
| class DetectResponse(BaseModel): | |
| success: bool | |
| detections: list[DetectionResult] | |
| raw_text: str | None = None | |
| elapsed_s: float | None = None | |
| error: str | None = None | |
| class HealthResponse(BaseModel): | |
| status: str | |
| model_loaded: bool | |
| model_path: str | |
| async def health(): | |
| return HealthResponse( | |
| status="ok" if _available else "no_model", | |
| model_loaded=_available, | |
| model_path=MODEL_PATH, | |
| ) | |
| async def detect( | |
| image: UploadFile = File(...), | |
| prompt: str = Form("Locate all objects in this image."), | |
| ): | |
| if not _available: | |
| raise HTTPException( | |
| status_code=503, | |
| detail=f"Model not found at {MODEL_PATH}. Has it been downloaded?", | |
| ) | |
| ext = Path(image.filename or "image.jpg").suffix or ".jpg" | |
| tmp_path = None | |
| try: | |
| tmp_dir = Path(tempfile.gettempdir()) / "locate-anything" | |
| tmp_dir.mkdir(parents=True, exist_ok=True) | |
| tmp_path = tmp_dir / f"{uuid.uuid4().hex}{ext}" | |
| with open(tmp_path, "wb") as f: | |
| f.write(await image.read()) | |
| t0 = time.time() | |
| result = subprocess.run( | |
| [ | |
| "locate-anything-cli", "detect", | |
| "--model", MODEL_PATH, | |
| "--input", str(tmp_path), | |
| "--prompt", prompt, | |
| "--mode", INFERENCE_MODE, | |
| "--threads", N_THREADS, | |
| ], | |
| capture_output=True, | |
| text=True, | |
| timeout=300, | |
| ) | |
| elapsed = time.time() - t0 | |
| if result.returncode != 0: | |
| logger.error(f"CLI stderr: {result.stderr[:500]}") | |
| return DetectResponse( | |
| success=False, | |
| detections=[], | |
| raw_text=result.stderr[:500], | |
| elapsed_s=elapsed, | |
| error=f"CLI exited with code {result.returncode}", | |
| ) | |
| output = result.stdout.strip() | |
| data = json.loads(output) if output else {} | |
| raw_detections = data.get("detections", []) | |
| detections = [ | |
| DetectionResult( | |
| label=d.get("label", "object"), | |
| box=_normalize_box(d.get("box", [0, 0, 0, 0])), | |
| ) | |
| for d in raw_detections | |
| ] | |
| logger.info(f"Detected {len(detections)} objects in {elapsed:.1f}s") | |
| return DetectResponse( | |
| success=True, | |
| detections=detections, | |
| raw_text=output[:500], | |
| elapsed_s=elapsed, | |
| ) | |
| except subprocess.TimeoutExpired: | |
| logger.error("Inference timed out after 120s") | |
| return DetectResponse( | |
| success=False, detections=[], error="Inference timed out" | |
| ) | |
| except Exception as e: | |
| logger.error(f"Detection error: {e}") | |
| return DetectResponse( | |
| success=False, detections=[], error=str(e) | |
| ) | |
| finally: | |
| if tmp_path and tmp_path.exists(): | |
| tmp_path.unlink() | |
| def _normalize_box(box: list[float]) -> list[float]: | |
| if len(box) != 4: | |
| return [0, 0, 0, 0] | |
| x1, y1, x2, y2 = box | |
| if max(x1, y1, x2, y2) > 1.0: | |
| x1 /= 1000.0 | |
| y1 /= 1000.0 | |
| x2 /= 1000.0 | |
| y2 /= 1000.0 | |
| return [x1, y1, x2, y2] | |