ribonpatil commited on
Commit
eeac23c
Β·
verified Β·
1 Parent(s): 075b453

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +15 -0
  2. app-5.py +330 -0
  3. requirements-8.txt +6 -0
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ RUN apt-get update && apt-get install -y \
4
+ libglib2.0-0 libsm6 libxext6 libxrender-dev \
5
+ && rm -rf /var/lib/apt/lists/*
6
+
7
+ WORKDIR /app
8
+
9
+ COPY requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY app.py .
13
+
14
+ EXPOSE 7860
15
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1", "--timeout-keep-alive", "300"]
app-5.py ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ╔══════════════════════════════════════════════════════════════════╗
3
+ β•‘ PIXELSKITS β€” WORLD CLASS VIRTUAL TRY-ON API v2.0 β•‘
4
+ β•‘ β•‘
5
+ β•‘ PIPELINE: β•‘
6
+ β•‘ 1. IDM-VTON β€” World #1 virtual try-on (HF servers) β•‘
7
+ β•‘ 2. Real-ESRGAN β€” Upscale to 720p/1080p/2K/4K/8K β•‘
8
+ β•‘ β•‘
9
+ β•‘ Our Space = lightweight middleman (~200MB RAM) β•‘
10
+ β•‘ All heavy AI runs on HF servers FREE β•‘
11
+ β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
12
+ """
13
+
14
+ import os, io, hashlib, time, logging, tempfile, base64
15
+ from typing import Optional
16
+ from datetime import datetime
17
+
18
+ import requests as http_requests
19
+ from fastapi import FastAPI, File, UploadFile, Header, HTTPException, Request, Form
20
+ from fastapi.middleware.cors import CORSMiddleware
21
+ from fastapi.responses import Response
22
+ from PIL import Image
23
+ from gradio_client import Client, handle_file
24
+
25
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
26
+ log = logging.getLogger("pixelskits-tryon")
27
+
28
+ # ──────────────────────────────────────────────────────────
29
+ MASTER_KEY = os.environ.get("PIXELSKITS_API_KEY", "")
30
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
31
+ RATE_LIMIT = 30
32
+ RATE_WINDOW = 3600
33
+
34
+ # Real-ESRGAN upscaler on HF Inference API
35
+ UPSCALE_URL = "https://api-inference.huggingface.co/models/ai-forever/Real-ESRGAN"
36
+
37
+ # Resolution targets (width x height)
38
+ RESOLUTIONS = {
39
+ "720p": (1280, 720),
40
+ "1080p": (1920, 1080),
41
+ "2k": (2560, 1440),
42
+ "4k": (3840, 2160),
43
+ "8k": (7680, 4320),
44
+ }
45
+
46
+ app = FastAPI(title="Pixelskits Virtual Try-On API v2", version="2.0.0", docs_url="/docs")
47
+ app.add_middleware(CORSMiddleware, allow_origins=["*"],
48
+ allow_methods=["GET", "POST"], allow_headers=["*"])
49
+
50
+ _rate: dict = {}
51
+ _client = None
52
+ _ready_flag = False
53
+
54
+
55
+ # ══════════════════════════════════════════════════════════
56
+ # RATE + AUTH
57
+ # ══════════════════════════════════════════════════════════
58
+
59
+ def check_rate(ip: str):
60
+ now = time.time()
61
+ h = hashlib.sha256(ip.encode()).hexdigest()[:16]
62
+ rec = _rate.get(h, {"count": 0, "start": now})
63
+ if now - rec["start"] > RATE_WINDOW:
64
+ rec = {"count": 0, "start": now}
65
+ rec["count"] += 1
66
+ _rate[h] = rec
67
+ if rec["count"] > RATE_LIMIT:
68
+ raise HTTPException(429, f"Rate limit: {RATE_LIMIT} req/hour.")
69
+
70
+ def _auth(key: Optional[str]):
71
+ if MASTER_KEY and key != MASTER_KEY:
72
+ raise HTTPException(401, "Invalid API key.")
73
+
74
+ def _get_ip(req: Request) -> str:
75
+ return req.headers.get("x-forwarded-for", req.client.host or "unknown")
76
+
77
+
78
+ # ══════════════════════════════════════════════════════════
79
+ # STARTUP
80
+ # ══════════════════════════════════════════════════════════
81
+
82
+ @app.on_event("startup")
83
+ async def startup():
84
+ global _client, _ready_flag
85
+ log.info("Connecting to IDM-VTON Space…")
86
+ try:
87
+ token = HF_TOKEN if HF_TOKEN else None
88
+ _client = Client("yisol/IDM-VTON", hf_token=token)
89
+ log.info("βœ… IDM-VTON connected. API online.")
90
+ except Exception as e:
91
+ log.warning(f"IDM-VTON connect warning: {e} β€” will retry per request")
92
+ _ready_flag = True
93
+
94
+ def _ready():
95
+ return _ready_flag
96
+
97
+
98
+ # ══════════════════════════════════════════════════════════
99
+ # HELPERS
100
+ # ══════════════════════════════════════════════════════════
101
+
102
+ def _save_temp(data: bytes, suffix: str = ".png") -> str:
103
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
104
+ tmp.write(data)
105
+ tmp.flush()
106
+ tmp.close()
107
+ return tmp.name
108
+
109
+ def _pil_to_bytes(img: Image.Image, fmt: str = "PNG") -> bytes:
110
+ buf = io.BytesIO()
111
+ img.save(buf, format=fmt, compress_level=6 if fmt == "PNG" else None)
112
+ return buf.getvalue()
113
+
114
+ def _validate_and_save(data: bytes, label: str) -> str:
115
+ try:
116
+ img = Image.open(io.BytesIO(data))
117
+ img.verify()
118
+ except Exception:
119
+ raise HTTPException(400, f"{label}: invalid image.")
120
+ img = Image.open(io.BytesIO(data)).convert("RGB")
121
+ return _save_temp(_pil_to_bytes(img))
122
+
123
+
124
+ # ══════════════════════════════════════════════════════════
125
+ # STEP 1 β€” IDM-VTON Try-On
126
+ # ══════════════════════════════════════════════════════════
127
+
128
+ def run_tryon(person_path: str, garment_path: str,
129
+ category: str = "upper_body",
130
+ denoise_steps: int = 30,
131
+ seed: int = 42) -> Image.Image:
132
+ global _client
133
+
134
+ if _client is None:
135
+ log.info("Reconnecting to IDM-VTON…")
136
+ _client = Client("yisol/IDM-VTON", hf_token=HF_TOKEN or None)
137
+
138
+ log.info(f"TryOn: category={category} steps={denoise_steps}")
139
+
140
+ result = _client.predict(
141
+ dict(background=handle_file(person_path), layers=[], composite=None),
142
+ handle_file(garment_path),
143
+ "Output",
144
+ True, # auto-masking
145
+ True, # auto-crop
146
+ denoise_steps,
147
+ seed,
148
+ api_name="/tryon"
149
+ )
150
+
151
+ result_path = result[0]
152
+ if isinstance(result_path, dict):
153
+ result_path = result_path.get("image", result_path.get("path", ""))
154
+
155
+ log.info(f"TryOn: result at {result_path}")
156
+ return Image.open(result_path).convert("RGB")
157
+
158
+
159
+ # ══════════════════════════════════════════════════════════
160
+ # STEP 2 β€” Real-ESRGAN Upscaler (HF Inference API)
161
+ # ══════════════════════════════════════════════════════════
162
+
163
+ def upscale_image(img: Image.Image, target_res: str) -> Image.Image:
164
+ """
165
+ Upscale using Real-ESRGAN on HF Inference API.
166
+ Then resize to exact target resolution.
167
+ Falls back to Lanczos resize if HF API unavailable.
168
+ """
169
+ if target_res not in RESOLUTIONS:
170
+ return img
171
+
172
+ target_w, target_h = RESOLUTIONS[target_res]
173
+ log.info(f"Upscale: target={target_res} ({target_w}Γ—{target_h})")
174
+
175
+ # Only call Real-ESRGAN if we need meaningful upscale (>1.5x)
176
+ src_w, src_h = img.size
177
+ scale_needed = max(target_w / src_w, target_h / src_h)
178
+
179
+ if scale_needed > 1.5 and HF_TOKEN:
180
+ try:
181
+ img_bytes = _pil_to_bytes(img)
182
+ headers = {"Authorization": f"Bearer {HF_TOKEN}"}
183
+
184
+ # Retry up to 3 times for model warmup
185
+ for attempt in range(3):
186
+ resp = http_requests.post(
187
+ UPSCALE_URL,
188
+ headers=headers,
189
+ data=img_bytes,
190
+ timeout=120
191
+ )
192
+ if resp.status_code == 503:
193
+ wait = min(float(resp.json().get("estimated_time", 15)), 25)
194
+ log.info(f"Upscaler warming up, waiting {wait}s… (attempt {attempt+1})")
195
+ time.sleep(wait)
196
+ continue
197
+ if resp.status_code == 200:
198
+ upscaled = Image.open(io.BytesIO(resp.content)).convert("RGB")
199
+ log.info(f"Upscale: Real-ESRGAN success β†’ {upscaled.size}")
200
+ # Final resize to exact target dimensions
201
+ final = upscaled.resize((target_w, target_h), Image.LANCZOS)
202
+ return final
203
+ log.warning(f"Upscaler returned {resp.status_code} β€” using Lanczos")
204
+ break
205
+
206
+ except Exception as e:
207
+ log.warning(f"Upscaler error: {e} β€” using Lanczos fallback")
208
+
209
+ # Lanczos fallback (still very good quality for moderate upscales)
210
+ log.info(f"Upscale: Lanczos resize β†’ {target_w}Γ—{target_h}")
211
+ return img.resize((target_w, target_h), Image.LANCZOS)
212
+
213
+
214
+ # ══════════════════════════════════════════════════════════
215
+ # ROUTES
216
+ # ══════════════════════════════════════════════════════════
217
+
218
+ @app.get("/")
219
+ async def root():
220
+ return {
221
+ "api": "Pixelskits Virtual Try-On API v2",
222
+ "status": "online" if _ready() else "loading",
223
+ "engine": "IDM-VTON (World #1) + Real-ESRGAN Upscaler",
224
+ "resolutions": list(RESOLUTIONS.keys()),
225
+ "usage": "POST /tryon β€” person_image + garment_image + resolution",
226
+ "category": "upper_body | lower_body | dresses",
227
+ }
228
+
229
+ @app.get("/health")
230
+ async def health():
231
+ return {
232
+ "status": "ok" if _ready() else "loading",
233
+ "hf_token": "set" if HF_TOKEN else "missing",
234
+ "idm_client": "connected" if _client else "disconnected",
235
+ "ts": datetime.utcnow().isoformat() + "Z",
236
+ }
237
+
238
+
239
+ @app.post("/tryon")
240
+ async def virtual_tryon(
241
+ request: Request,
242
+ person_image: UploadFile = File(..., description="Photo of the person"),
243
+ garment_image: UploadFile = File(..., description="Photo of the garment/outfit"),
244
+ category: str = Form("upper_body", description="upper_body | lower_body | dresses"),
245
+ resolution: str = Form("1080p", description="720p | 1080p | 2k | 4k | 8k"),
246
+ denoise_steps: int = Form(30, description="Quality: 20–40 (higher=better, slower)"),
247
+ seed: int = Form(42, description="Seed for reproducibility (-1 = random)"),
248
+ x_api_key: Optional[str] = Header(None, alias="X-API-Key"),
249
+ ):
250
+ """
251
+ Virtual try-on endpoint.
252
+ Send person photo + garment photo β†’ get photorealistic try-on result at your chosen resolution.
253
+ """
254
+ _auth(x_api_key)
255
+ check_rate(_get_ip(request))
256
+
257
+ if not _ready():
258
+ raise HTTPException(503, "API starting. Retry in 10s.")
259
+
260
+ # Validate inputs
261
+ allowed = ("image/jpeg", "image/jpg", "image/png", "image/webp")
262
+ if person_image.content_type not in allowed:
263
+ raise HTTPException(400, "person_image: use JPEG, PNG, or WebP.")
264
+ if garment_image.content_type not in allowed:
265
+ raise HTTPException(400, "garment_image: use JPEG, PNG, or WebP.")
266
+ if category not in ("upper_body", "lower_body", "dresses"):
267
+ raise HTTPException(400, "category: upper_body | lower_body | dresses")
268
+ if resolution not in RESOLUTIONS:
269
+ raise HTTPException(400, f"resolution: {' | '.join(RESOLUTIONS.keys())}")
270
+
271
+ denoise_steps = max(20, min(40, denoise_steps))
272
+ if seed == -1:
273
+ import random
274
+ seed = random.randint(0, 999999)
275
+
276
+ person_bytes = await person_image.read()
277
+ garment_bytes = await garment_image.read()
278
+
279
+ if len(person_bytes) > 20 * 1024 * 1024:
280
+ raise HTTPException(413, "person_image: max 20 MB.")
281
+ if len(garment_bytes) > 20 * 1024 * 1024:
282
+ raise HTTPException(413, "garment_image: max 20 MB.")
283
+
284
+ t0 = time.time()
285
+ log.info(f"/tryon β€” category={category} resolution={resolution} steps={denoise_steps}")
286
+
287
+ person_path = _validate_and_save(person_bytes, "person_image")
288
+ garment_path = _validate_and_save(garment_bytes, "garment_image")
289
+
290
+ try:
291
+ # Step 1: IDM-VTON try-on
292
+ log.info("Step 1: IDM-VTON try-on…")
293
+ tryon_result = run_tryon(
294
+ person_path = person_path,
295
+ garment_path = garment_path,
296
+ category = category,
297
+ denoise_steps = denoise_steps,
298
+ seed = seed,
299
+ )
300
+
301
+ # Step 2: Upscale to chosen resolution
302
+ log.info(f"Step 2: Upscaling to {resolution}…")
303
+ final_img = upscale_image(tryon_result, resolution)
304
+
305
+ except HTTPException:
306
+ raise
307
+ except Exception as e:
308
+ log.error(f"TryOn pipeline failed: {e}")
309
+ raise HTTPException(500, f"Try-on failed: {str(e)}. Please retry.")
310
+ finally:
311
+ import os as _os
312
+ for p in [person_path, garment_path]:
313
+ try: _os.unlink(p)
314
+ except: pass
315
+
316
+ elapsed = round(time.time() - t0, 2)
317
+ out_w, out_h = final_img.size
318
+ log.info(f"/tryon done β€” {elapsed}s, output={out_w}Γ—{out_h}")
319
+
320
+ return Response(
321
+ content = _pil_to_bytes(final_img),
322
+ media_type = "image/png",
323
+ headers = {
324
+ "X-Processing-Time": f"{elapsed}s",
325
+ "X-Engine": "IDM-VTON + Real-ESRGAN",
326
+ "X-Category": category,
327
+ "X-Resolution": resolution,
328
+ "X-Output-Size": f"{out_w}x{out_h}",
329
+ },
330
+ )
requirements-8.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn==0.30.1
3
+ python-multipart==0.0.9
4
+ Pillow==10.3.0
5
+ gradio_client==0.17.0
6
+ requests==2.32.3