Spaces:
Running
Running
File size: 6,313 Bytes
53f7d61 36f68cc 53f7d61 36f68cc 53f7d61 e40d130 53f7d61 36f68cc e40d130 53f7d61 e40d130 36f68cc 53f7d61 a6f63e9 53f7d61 e40d130 53f7d61 36f68cc 53f7d61 a6f63e9 53f7d61 e40d130 a6f63e9 53f7d61 a6f63e9 53f7d61 e40d130 53f7d61 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | """Hugging Face Space entry point for the trusted HTML shell."""
from __future__ import annotations
import base64
import binascii
import threading
import time
import warnings
from io import BytesIO
from pathlib import Path
from typing import Any, Callable
from fastapi import HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.responses import JSONResponse
from PIL import Image, UnidentifiedImageError
from snap2sim.backend import InferenceClient, Settings
from snap2sim.schema import normalize_confidence_threshold
try:
from gradio import Server
except ImportError:
from fastapi import FastAPI
import uvicorn
class Server(FastAPI): # type: ignore[no-redef]
"""Local compatibility shim for environments older than Gradio Server."""
def api(self, name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
return func
return decorator
def launch(self, **kwargs: Any) -> None:
uvicorn.run(
self,
host=kwargs.get("server_name", "0.0.0.0"),
port=kwargs.get("server_port", 7860),
)
app = Server()
INDEX_PATH = Path(__file__).with_name("index.html")
MAX_IMAGE_BASE64_CHARS = 12 * 1024 * 1024
MAX_IMAGE_BYTES = 9 * 1024 * 1024
MAX_IMAGE_PIXELS = 12_000_000
RATE_LIMIT_WINDOW_SECONDS = 60
RATE_LIMIT_PER_CLIENT = 12
RATE_LIMIT_GLOBAL = 72
RATE_LIMIT_PATHS = {"/analyze_image", "/generate_scene"}
Image.MAX_IMAGE_PIXELS = MAX_IMAGE_PIXELS
_rate_lock = threading.Lock()
_client_hits: dict[str, list[float]] = {}
_global_hits: list[float] = []
@app.middleware("http")
async def rate_limit_api(request: Request, call_next: Callable[..., Any]) -> Any:
if request.method == "POST" and request.url.path in RATE_LIMIT_PATHS:
allowed, retry_after = _record_request(_client_id(request))
if not allowed:
return JSONResponse(
{"detail": f"Rate limit exceeded. Retry after {retry_after} seconds."},
status_code=429,
headers={"Retry-After": str(retry_after)},
)
return await call_next(request)
@app.get("/", response_class=HTMLResponse)
async def homepage() -> str:
return INDEX_PATH.read_text(encoding="utf-8")
@app.get("/manifest.json")
async def manifest() -> dict[str, Any]:
return {
"name": "Snap2Sim Inside the Machine",
"short_name": "Snap2Sim",
"start_url": "/",
"display": "standalone",
"background_color": "#0F1318",
"theme_color": "#E8A33D",
}
@app.api(name="analyze_image")
def analyze_image_api(image_base64: str) -> dict[str, Any]:
return _analyze_image(image_base64)
@app.post("/analyze_image")
def analyze_image_http(payload: dict[str, Any]) -> dict[str, Any]:
return _analyze_image(str(payload.get("image_base64", "")))
@app.api(name="generate_scene")
def generate_scene_api(
analysis: dict[str, Any],
confidence_threshold: float | None = None,
) -> dict[str, Any]:
return _generate_scene(analysis, confidence_threshold)
@app.post("/generate_scene")
def generate_scene_http(payload: dict[str, Any]) -> dict[str, Any]:
return _generate_scene(
payload.get("analysis") or {},
payload.get("confidence_threshold"),
)
def _analyze_image(image_base64: str) -> dict[str, Any]:
image = _decode_image(image_base64) if image_base64 else None
return InferenceClient(Settings()).analyze_image(image)
def _generate_scene(analysis: dict[str, Any], threshold: Any = None) -> dict[str, Any]:
return InferenceClient(Settings()).generate_scene(
analysis,
normalize_confidence_threshold(threshold),
)
def _decode_image(image_base64: str) -> Image.Image:
if "," in image_base64 and image_base64.lstrip().startswith("data:"):
image_base64 = image_base64.split(",", 1)[1]
if len(image_base64) > MAX_IMAGE_BASE64_CHARS:
raise HTTPException(status_code=413, detail="Image upload is too large.")
try:
raw = base64.b64decode(image_base64, validate=True)
except (binascii.Error, ValueError) as exc:
raise HTTPException(status_code=400, detail="Image payload is not valid base64.") from exc
if len(raw) > MAX_IMAGE_BYTES:
raise HTTPException(status_code=413, detail="Image upload is too large.")
try:
with warnings.catch_warnings():
warnings.simplefilter("error", Image.DecompressionBombWarning)
image = Image.open(BytesIO(raw))
image.load()
except Image.DecompressionBombWarning as exc:
raise HTTPException(status_code=413, detail="Image dimensions are too large.") from exc
except Image.DecompressionBombError as exc:
raise HTTPException(status_code=413, detail="Image dimensions are too large.") from exc
except (UnidentifiedImageError, OSError, ValueError) as exc:
raise HTTPException(status_code=400, detail="Upload a valid image file.") from exc
if image.width * image.height > MAX_IMAGE_PIXELS:
raise HTTPException(status_code=413, detail="Image dimensions are too large.")
return image.convert("RGB")
def _client_id(request: Request) -> str:
forwarded_for = request.headers.get("x-forwarded-for", "")
if forwarded_for:
return forwarded_for.split(",", 1)[0].strip()
return request.client.host if request.client else "unknown"
def _record_request(client_id: str) -> tuple[bool, int]:
now = time.monotonic()
cutoff = now - RATE_LIMIT_WINDOW_SECONDS
with _rate_lock:
_global_hits[:] = [hit for hit in _global_hits if hit >= cutoff]
hits = [hit for hit in _client_hits.get(client_id, []) if hit >= cutoff]
if len(hits) >= RATE_LIMIT_PER_CLIENT or len(_global_hits) >= RATE_LIMIT_GLOBAL:
oldest = min(hits[0] if hits else now, _global_hits[0] if _global_hits else now)
retry_after = max(1, int(RATE_LIMIT_WINDOW_SECONDS - (now - oldest)))
_client_hits[client_id] = hits
return False, retry_after
hits.append(now)
_global_hits.append(now)
_client_hits[client_id] = hits
return True, 0
if __name__ == "__main__":
app.launch()
|