CoolFace
Apppublic

crbns/drestly-image-processing

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
main.py155 linesDownload Raw Back to root
1import os2from contextlib import asynccontextmanager3from io import BytesIO4import asyncio5 6from fastapi import FastAPI, BackgroundTasks, Depends7from pydantic import BaseModel8from PIL import Image9from rembg import remove, new_session10from supabase import create_client, Client11from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor12from telemetry import setup_telemetry13from opentelemetry import metrics, trace14 15from auth import verify_user16 17SUPABASE_URL = os.environ["SUPABASE_URL"]18SECRET_KEY = os.environ["SUPABASE_SECRET_KEY"]19ORIGINALS = "originals"20CUTOUTS = "cutouts"21 22setup_telemetry()23 24state: dict = {}25 26_meter = metrics.get_meter("drestly.inference")27_queued = _meter.create_up_down_counter(28    "inference.queue.depth",29    unit="{request}",30    description="Requests waiting to acquire an inference slot",31)32_active = _meter.create_up_down_counter(33    "inference.active",34    unit="{request}",35    description="Requests currently running inference",36)37 38INFERENCE_SLOTS = asyncio.Semaphore(2)39 40 41@asynccontextmanager42async def inference_slot():43    _queued.add(1)  # entered the line44    try:45        await INFERENCE_SLOTS.acquire()46    finally:47        _queued.add(-1)  # left the line (got in, or was cancelled)48    _active.add(1)  # running49    try:50        yield51    finally:52        _active.add(-1)53        INFERENCE_SLOTS.release()54 55 56@asynccontextmanager57async def lifespan(app: FastAPI):58    state["session"] = new_session("birefnet-general-lite")  # warm the model once59    state["supabase"] = create_client(SUPABASE_URL, SECRET_KEY)60    yield61    # Flush buffered spans/metrics before the process exits (BatchSpanProcessor62    # and the 60s metric reader would otherwise drop whatever is still queued).63    trace.get_tracer_provider().shutdown()64    metrics.get_meter_provider().shutdown()65    state.clear()66 67 68app = FastAPI(lifespan=lifespan, docs_url=None, redoc_url=None, openapi_url=None)69FastAPIInstrumentor.instrument_app(app)70 71 72class ProcessRequest(BaseModel):73    item_id: str74 75 76@app.get("/health")77def health():78    return {"ok": True}79 80 81@app.post("/process", status_code=202)82def process(83    req: ProcessRequest,84    background: BackgroundTasks,85    user_id: str = Depends(verify_user),86):87    background.add_task(run_cutout, req.item_id, user_id)88    return {"status": "processing", "item_id": req.item_id}89 90 91async def run_cutout(item_id: str, user_id: str):92    tracer = trace.get_tracer("drestly.inference")93    with tracer.start_as_current_span("run_cutout") as span:94        span.set_attribute("item.id", item_id)95        async with inference_slot():96            # rembg, Pillow, and the sync Supabase client all block; run them off the97            # event loop so /health pings and other requests stay responsive.98            await asyncio.to_thread(_do_cutout_blocking, item_id, user_id)99 100 101def _do_cutout_blocking(item_id: str, user_id: str):102    sb: Client = state["supabase"]103 104    # Service key bypasses RLS, so confirm ownership ourselves before touching anything.105    # maybe_single() yields data=None for a missing row instead of raising (unlike single()).106    row = (107        sb.table("clothing_items")108        .select("user_id, original_path")109        .eq("id", item_id)110        .maybe_single()111        .execute()112    )113    if not row or not isinstance(row.data, dict) or row.data["user_id"] != user_id:114        return115 116    original_path = row.data["original_path"]117    try:118        original_bytes = sb.storage.from_(ORIGINALS).download(original_path)119 120        img = Image.open(BytesIO(original_bytes))121        out = remove(img, session=state["session"])122        out.thumbnail((1500, 1500))  # cap resolution: in-place, preserves aspect, only downscales123        buf = BytesIO()124        out.save(buf, format="WEBP", quality=80, method=6)125 126        cutout_path = f"{user_id}/{item_id}.webp"127        sb.storage.from_(CUTOUTS).upload(128            cutout_path,129            buf.getvalue(),130            file_options={"content-type": "image/webp", "upsert": "true"},131        )132 133        buf.close()134 135        sb.table("clothing_items").update(136            {137                "status": "done",138                "cutout_path": cutout_path,139            }140        ).eq("id", item_id).execute()141 142    except Exception as e:143        sb.table("clothing_items").update(144            {145                "status": "failed",146                "error": str(e)[:500],147            }148        ).eq("id", item_id).execute()149        return150 151    try:152        sb.storage.from_(ORIGINALS).remove([original_path])153    except Exception:154        pass155