Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-07-22 22:58:28 +05:30
parent d1308bf149
commit bc8bf2007d
99 changed files with 4784 additions and 111 deletions
+26
View File
@@ -0,0 +1,26 @@
# RunPod Serverless worker: SDXL inpainting (Magic Eraser / Generative Fill / Outpaint).
# CUDA 12.1 base + torch cu121 — the same base the detect/upscale workers use, which
# runs on RunPod's RTX 3090/4090 (driver 550) hosts (12.1 <= 12.4, backward-compatible).
# Build context = repo root, so COPY paths are workers/inpaint/*.
FROM nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \
PIP_NO_CACHE_DIR=1
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 python3-pip git && \
ln -sf /usr/bin/python3 /usr/bin/python && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# torch (CUDA 12.1 build) FIRST — carries sm_86 (3090) + sm_89 (4090) kernels.
RUN pip install --upgrade pip && \
pip install torch --index-url https://download.pytorch.org/whl/cu121
COPY workers/inpaint/requirements.txt .
RUN pip install -r requirements.txt
COPY workers/inpaint/handler.py .
CMD ["python3", "-u", "handler.py"]
+30
View File
@@ -0,0 +1,30 @@
# Inpaint worker — SDXL (`diffusers/stable-diffusion-xl-1.0-inpainting-0.1`)
One RunPod serverless endpoint that powers **Magic Eraser**, **Generative Fill**,
and **Outpaint** in the photo editor. Set its `/runsync` URL as the Vercel env var
**`RUNPOD_SD_INPAINT_URL`**.
## Contract
Request `{"input": {...}}` — matches the app's `rpInpaint`:
| field | required | meaning |
|---|---|---|
| `image` | yes | base64 image (raw or `data:` URI) |
| `mask` | yes | base64 mask — **white = regenerate, black = keep** |
| `prompt` | no | `""` = Magic Eraser (clean fill); text = Generative Fill / Outpaint |
| `strength` | no | 0..1 (empty prompt forces 1.0 to fully erase) |
| `guidance_scale` | no | default 7 |
| `num_inference_steps` | no | default 35 |
| `negative_prompt` | no | — |
| `seed` | no | — |
Response: `{"image": "<base64 PNG>"}` or `{"error": "..."}`.
## RunPod setup
1. New Serverless endpoint → connect this GitHub repo → **Dockerfile path** `workers/inpaint/Dockerfile` (build context = repo root).
2. GPU: **24 GB** (RTX 4090 / 3090). Max workers ≥ 1; optionally a network volume to cache the ~7 GB model so cold starts don't re-download.
3. Copy the endpoint's `…/runsync` URL → Vercel `RUNPOD_SD_INPAINT_URL`, then redeploy the web app.
## Env knobs
- `SD_INPAINT_MODEL` (default `diffusers/stable-diffusion-xl-1.0-inpainting-0.1`)
- `SD_INPAINT_MAX_SIZE` (default `1024`)
+137
View File
@@ -0,0 +1,137 @@
"""
RunPod Serverless worker: SDXL inpainting.
Powers the app's Magic Eraser, Generative Fill AND Outpaint — all three go
through this one endpoint (RUNPOD_SD_INPAINT_URL). No app change needed; this
matches the exact contract the app already sends (rpInpaint):
{"input": {
"image": "<base64>", # required (raw base64 or data: URI)
"mask": "<base64>", # required — WHITE = regenerate, BLACK = keep
"prompt": "<text>", # optional ("" = Magic Eraser: clean fill)
"strength": 0.8, # optional
"guidance_scale": 7, # optional
"num_inference_steps": 35, # optional
"negative_prompt": "...", # optional
"seed": 123 # optional
}}
Returns: {"image": "<base64 PNG>"} (or {"error": "..."}).
CUDA 12.1 base + torch cu121 (same as the detect/upscale workers) so it runs on
RunPod's RTX 3090/4090 (driver 550) hosts.
"""
import base64
import io
import os
_V = "/runpod-volume"
if os.path.isdir(_V):
os.environ.setdefault("HF_HOME", os.path.join(_V, "huggingface"))
import torch # noqa: E402
import runpod # noqa: E402
from PIL import Image # noqa: E402
from diffusers import AutoPipelineForInpainting # noqa: E402
MODEL = os.environ.get("SD_INPAINT_MODEL", "diffusers/stable-diffusion-xl-1.0-inpainting-0.1")
MAX_SIZE = int(os.environ.get("SD_INPAINT_MAX_SIZE", "1024"))
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
_pipe = None
def _get_pipe():
global _pipe
if _pipe is None:
try:
pipe = AutoPipelineForInpainting.from_pretrained(
MODEL, torch_dtype=torch.float16, variant="fp16"
)
except Exception:
# Repo may not ship an fp16 variant — fall back to default weights.
pipe = AutoPipelineForInpainting.from_pretrained(MODEL, torch_dtype=torch.float16)
total_gb = torch.cuda.get_device_properties(0).total_memory / 1e9 if DEVICE == "cuda" else 0
if total_gb >= 20:
pipe = pipe.to("cuda")
else:
pipe.enable_model_cpu_offload()
pipe.set_progress_bar_config(disable=True)
_pipe = pipe
return _pipe
def _b64_to_image(data, mode="RGB"):
if "," in data:
data = data.split(",", 1)[1]
return Image.open(io.BytesIO(base64.b64decode(data))).convert(mode)
def _image_to_b64(img):
buf = io.BytesIO()
img.save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode("utf-8")
def _fit(img):
# SDXL wants multiples of 8; cap the long side at MAX_SIZE.
w, h = img.size
scale = min(1.0, MAX_SIZE / max(w, h))
nw = max(8, (int(w * scale) // 8) * 8)
nh = max(8, (int(h * scale) // 8) * 8)
return img.resize((nw, nh), Image.LANCZOS)
def handler(event):
inp = (event or {}).get("input") or {}
image_b64 = inp.get("image")
mask_b64 = inp.get("mask")
if not image_b64:
return {"error": "Missing 'image' (base64)."}
if not mask_b64:
return {"error": "Missing 'mask' (base64)."}
prompt = (inp.get("prompt") or "").strip()
negative = (inp.get("negative_prompt") or "").strip() or None
try:
image = _fit(_b64_to_image(image_b64, "RGB"))
mask = _b64_to_image(mask_b64, "L").resize(image.size, Image.NEAREST)
steps = int(inp.get("num_inference_steps") or 35)
guidance = float(inp.get("guidance_scale") or 7.0)
# Strength: empty prompt = Magic Eraser -> fully replace (1.0). With a
# prompt (Generative Fill / Outpaint) respect the caller but keep a floor
# so the masked region actually changes.
raw = inp.get("strength")
if not prompt:
strength = 1.0
else:
strength = max(0.7, min(1.0, float(raw))) if raw is not None else 0.9
seed = inp.get("seed")
generator = (
torch.Generator(device=DEVICE).manual_seed(int(seed)) if seed is not None else None
)
eff_prompt = prompt or "clean, seamless, photorealistic background, natural continuation"
result = _get_pipe()(
prompt=eff_prompt,
negative_prompt=negative,
image=image,
mask_image=mask,
height=image.height,
width=image.width,
num_inference_steps=steps,
guidance_scale=guidance,
strength=strength,
generator=generator,
).images[0]
return {"image": _image_to_b64(result)}
except Exception as exc:
return {"error": f"{type(exc).__name__}: {exc}"}
runpod.serverless.start({"handler": handler})
+8
View File
@@ -0,0 +1,8 @@
# SDXL inpainting worker. torch is installed (cu121) in the Dockerfile first.
# AutoPipelineForInpainting + SDXL uses two text encoders → transformers.
diffusers>=0.30.0
transformers>=4.44.0
accelerate>=0.34.0
safetensors>=0.4.5
Pillow==10.4.0
runpod==1.7.9
+1
View File
@@ -0,0 +1 @@
{"input": {"image": "iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOtAAAFlklEQVR42u3VMQEAAAzCMKQjHQ97l0jo0xSAlyIBgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGACAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGACAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGACAAUgAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGACAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGACAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgBgAAAYAAAGAIABAGAAABgAAAYAgAEAYAAAGAAABgCAAQBgAAAYAAAGAIABAGAAABgAADcDrctaAb6XeXAAAAAASUVORK5CYII=", "mask": "iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAAAAADRE4smAAADXUlEQVR42u3SQREAAAzCMPybhvc0LJXQS6rXxQIABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAgAACwAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAAsAEAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAAALAAAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAAwAIABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAgAACwAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIAAEAACQAAIAAEgAASAABAAAkAACAABIAAEgAAQAAJAAAgAASAABIBuA8sSO8WbTgaBAAAAAElFTkSuQmCC", "prompt": "a clean concrete wall", "num_inference_steps": 20}}