StableVITON Deployer commited on
Commit
1ca64e8
·
1 Parent(s): 90f25e3

Fix: Corrected StableVITONInference initialization and aligned logs

Browse files
Files changed (1) hide show
  1. main.py +123 -134
main.py CHANGED
@@ -3,7 +3,9 @@ FastAPI Backend for StableVITON Virtual Try-On
3
  Provides REST API endpoint for virtual try-on inference
4
  """
5
 
6
- from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks
 
 
7
  from fastapi.middleware.cors import CORSMiddleware
8
  from fastapi.responses import JSONResponse
9
  from pydantic import BaseModel
@@ -27,11 +29,34 @@ logging.basicConfig(
27
  )
28
  logger = logging.getLogger(__name__)
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  # Initialize FastAPI app
31
  app = FastAPI(
32
  title="StableVITON Virtual Try-On API",
33
  description="AI-powered virtual try-on service using StableVITON",
34
- version="1.0.0"
 
35
  )
36
 
37
  # CORS configuration - allow all origins for demo (restrict in production)
@@ -43,6 +68,28 @@ app.add_middleware(
43
  allow_headers=["*"],
44
  )
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  # Global model instance (loaded once at startup)
47
  model: Optional[StableVITONInference] = None
48
 
@@ -66,63 +113,17 @@ class TryOnResponse(BaseModel):
66
  error_code: Optional[str] = None
67
 
68
 
69
- @app.on_event("startup")
70
- async def startup_event():
71
- """Load model on startup"""
72
- global model
73
- try:
74
- logger.info("Loading StableVITON model...")
75
-
76
- model = StableVITONInference(
77
- model_path=os.getenv("MODEL_PATH", "yisol/IDM-VTON"),
78
- device=os.getenv("DEVICE", "auto"),
79
- resolution=int(os.getenv("RESOLUTION", "768")),
80
- num_inference_steps=int(os.getenv("NUM_INFERENCE_STEPS", "30")),
81
- guidance_scale=float(os.getenv("GUIDANCE_SCALE", "2.5"))
82
- )
83
-
84
- logger.info("Model loaded successfully")
85
-
86
- # Warmup inference
87
- logger.info("Running warmup inference...")
88
- dummy_person = Image.new("RGB", (512, 768), color=(200, 200, 200))
89
- dummy_garment = Image.new("RGB", (512, 512), color=(100, 150, 200))
90
- model.tryon(dummy_person, dummy_garment)
91
- model.cleanup()
92
- logger.info("Warmup completed")
93
-
94
- except Exception as e:
95
- logger.error(f"Failed to load model: {e}")
96
- raise
97
-
98
-
99
- @app.on_event("shutdown")
100
- async def shutdown_event():
101
- """Cleanup on shutdown"""
102
- global model
103
- if model:
104
- model.cleanup()
105
- logger.info("Model cleaned up")
106
 
107
 
108
  def validate_image(file: UploadFile) -> None:
109
- """
110
- Validate uploaded image file.
111
-
112
- Args:
113
- file: Uploaded file
114
-
115
- Raises:
116
- HTTPException: If validation fails
117
- """
118
- # Check content type
119
  if file.content_type not in ALLOWED_EXTENSIONS:
120
  raise HTTPException(
121
  status_code=400,
122
  detail=f"Invalid file type. Allowed: {ALLOWED_EXTENSIONS}"
123
  )
124
 
125
- # Check file size (this is approximate, actual size checked during read)
126
  if hasattr(file, 'size') and file.size > MAX_IMAGE_SIZE:
127
  raise HTTPException(
128
  status_code=400,
@@ -131,15 +132,7 @@ def validate_image(file: UploadFile) -> None:
131
 
132
 
133
  def image_to_base64(image: Image.Image) -> str:
134
- """
135
- Convert PIL Image to base64 string.
136
-
137
- Args:
138
- image: PIL Image
139
-
140
- Returns:
141
- Base64 encoded string with data URI prefix
142
- """
143
  buffered = io.BytesIO()
144
  image.save(buffered, format="PNG")
145
  img_bytes = buffered.getvalue()
@@ -148,22 +141,9 @@ def image_to_base64(image: Image.Image) -> str:
148
 
149
 
150
  async def read_image_from_upload(file: UploadFile) -> Image.Image:
151
- """
152
- Read PIL Image from uploaded file.
153
-
154
- Args:
155
- file: Uploaded file
156
-
157
- Returns:
158
- PIL Image
159
-
160
- Raises:
161
- HTTPException: If image cannot be read
162
- """
163
  try:
164
  contents = await file.read()
165
-
166
- # Check actual size
167
  if len(contents) > MAX_IMAGE_SIZE:
168
  raise HTTPException(
169
  status_code=400,
@@ -171,20 +151,6 @@ async def read_image_from_upload(file: UploadFile) -> Image.Image:
171
  )
172
 
173
  image = Image.open(io.BytesIO(contents))
174
-
175
- # Validate dimensions
176
- width, height = image.size
177
- if width < 256 or height < 256:
178
- raise HTTPException(
179
- status_code=400,
180
- detail="Image too small. Minimum dimensions: 256x256"
181
- )
182
- if width > 2048 or height > 2048:
183
- raise HTTPException(
184
- status_code=400,
185
- detail="Image too large. Maximum dimensions: 2048x2048"
186
- )
187
-
188
  return image
189
 
190
  except HTTPException:
@@ -201,90 +167,113 @@ async def read_image_from_upload(file: UploadFile) -> Image.Image:
201
  async def root():
202
  """Root endpoint"""
203
  return {
204
- "message": "StableVITON Virtual Try-On API",
205
- "version": "1.0.0",
206
  "endpoints": {
207
  "/tryon": "POST - Virtual try-on inference",
 
208
  "/health": "GET - Health check"
209
  }
210
  }
211
 
212
 
 
 
 
 
 
 
 
 
 
 
213
  @app.get("/health")
214
  async def health_check():
215
  """Health check endpoint"""
216
  return {
217
  "status": "healthy",
218
- "model_loaded": model is not None,
219
  "timestamp": time.time()
220
  }
221
 
222
 
223
  @app.post("/tryon", response_model=TryOnResponse)
224
  async def virtual_tryon(
 
225
  person_image: UploadFile = File(..., description="Full-body photo of person"),
226
- garment_image: UploadFile = File(..., description="Garment image")
 
 
 
 
 
 
227
  ):
228
  """
229
- Perform virtual try-on inference.
230
-
231
- Args:
232
- person_image: Full-body photo of the person
233
- garment_image: Image of the garment to try on
234
-
235
- Returns:
236
- TryOnResponse with result image or error
237
  """
238
  start_time = time.time()
239
 
240
  try:
241
- # Check if model is loaded
242
  if model is None:
243
  raise HTTPException(
244
  status_code=503,
245
- detail="Model not loaded. Please try again later."
246
  )
247
 
248
- # Validate files
 
 
 
 
 
249
  validate_image(person_image)
250
  validate_image(garment_image)
 
 
251
 
252
- logger.info(f"Processing try-on request: {person_image.filename}, {garment_image.filename}")
253
-
254
- # Read images
255
  person_img = await read_image_from_upload(person_image)
256
  garment_img = await read_image_from_upload(garment_image)
 
 
257
 
258
- # Acquire processing lock (single request at a time)
259
- acquired = processing_lock.acquire(blocking=False)
260
- if not acquired:
261
- raise HTTPException(
262
- status_code=503,
263
- detail="Server is busy processing another request. Please try again in a moment."
264
- )
265
 
266
- try:
267
- # Run inference
268
- logger.info("Running inference...")
269
- result_image = model.tryon(person_img, garment_img)
270
-
271
- # Convert to base64
272
- result_base64 = image_to_base64(result_image)
273
-
274
- # Cleanup
275
- model.cleanup()
276
-
277
- processing_time = time.time() - start_time
278
- logger.info(f"Inference completed in {processing_time:.2f}s")
279
-
280
- return TryOnResponse(
281
- success=True,
282
- result_image=result_base64,
283
- processing_time=processing_time
284
- )
285
-
286
- finally:
287
- processing_lock.release()
 
 
 
 
 
 
 
 
 
288
 
289
  except HTTPException:
290
  raise
 
3
  Provides REST API endpoint for virtual try-on inference
4
  """
5
 
6
+ from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks, Form, Request
7
+ from fastapi.exceptions import RequestValidationError
8
+ from contextlib import asynccontextmanager
9
  from fastapi.middleware.cors import CORSMiddleware
10
  from fastapi.responses import JSONResponse
11
  from pydantic import BaseModel
 
29
  )
30
  logger = logging.getLogger(__name__)
31
 
32
+ @asynccontextmanager
33
+ async def lifespan(app: FastAPI):
34
+ """Lifespan context manager for startup and shutdown events"""
35
+ global model
36
+ try:
37
+ logger.info("Initializing StableVITON Inference Engine (Lifespan Startup)...")
38
+ # Initialize the local inference engine
39
+ model = StableVITONInference(
40
+ model_path=os.getenv("MODEL_PATH", "yisol/IDM-VTON"),
41
+ device="auto"
42
+ )
43
+ logger.info("StableVITON Inference Engine initialized successfully")
44
+ except Exception:
45
+ logger.exception("Failed to initialize StableVITON Inference Engine during startup")
46
+ model = None
47
+
48
+ yield
49
+
50
+ if model:
51
+ model.cleanup()
52
+ logger.info("Model cleaned up (Lifespan Shutdown)")
53
+
54
  # Initialize FastAPI app
55
  app = FastAPI(
56
  title="StableVITON Virtual Try-On API",
57
  description="AI-powered virtual try-on service using StableVITON",
58
+ version="1.0.0",
59
+ lifespan=lifespan
60
  )
61
 
62
  # CORS configuration - allow all origins for demo (restrict in production)
 
68
  allow_headers=["*"],
69
  )
70
 
71
+ @app.exception_handler(RequestValidationError)
72
+ async def validation_exception_handler(request: Request, exc: RequestValidationError):
73
+ # Log the detailed error to terminal for the user
74
+ # Convert errors to a string-safe list to avoid JSON serialization issues with ctx/ValueError
75
+ safe_errors = []
76
+ for err in exc.errors():
77
+ safe_err = {k: (str(v) if k in ['ctx', 'input'] else v) for k, v in err.items()}
78
+ safe_errors.append(safe_err)
79
+
80
+ print(f"\n[!] VALIDATION ERROR: {safe_errors}")
81
+ logger.error(f"Validation error details: {safe_errors}")
82
+
83
+ return JSONResponse(
84
+ status_code=422,
85
+ content={
86
+ "success": False,
87
+ "error": "Request validation failed",
88
+ "details": safe_errors,
89
+ "error_code": "VALIDATION_ERROR"
90
+ },
91
+ )
92
+
93
  # Global model instance (loaded once at startup)
94
  model: Optional[StableVITONInference] = None
95
 
 
113
  error_code: Optional[str] = None
114
 
115
 
116
+ # Note: Startup and shutdown are now handled by lifespan context manager
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
 
119
  def validate_image(file: UploadFile) -> None:
120
+ """Validate uploaded image file."""
 
 
 
 
 
 
 
 
 
121
  if file.content_type not in ALLOWED_EXTENSIONS:
122
  raise HTTPException(
123
  status_code=400,
124
  detail=f"Invalid file type. Allowed: {ALLOWED_EXTENSIONS}"
125
  )
126
 
 
127
  if hasattr(file, 'size') and file.size > MAX_IMAGE_SIZE:
128
  raise HTTPException(
129
  status_code=400,
 
132
 
133
 
134
  def image_to_base64(image: Image.Image) -> str:
135
+ """Convert PIL Image to base64 string."""
 
 
 
 
 
 
 
 
136
  buffered = io.BytesIO()
137
  image.save(buffered, format="PNG")
138
  img_bytes = buffered.getvalue()
 
141
 
142
 
143
  async def read_image_from_upload(file: UploadFile) -> Image.Image:
144
+ """Read PIL Image from uploaded file."""
 
 
 
 
 
 
 
 
 
 
 
145
  try:
146
  contents = await file.read()
 
 
147
  if len(contents) > MAX_IMAGE_SIZE:
148
  raise HTTPException(
149
  status_code=400,
 
151
  )
152
 
153
  image = Image.open(io.BytesIO(contents))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  return image
155
 
156
  except HTTPException:
 
167
  async def root():
168
  """Root endpoint"""
169
  return {
170
+ "message": "StableVITON Virtual Try-On API (Remote Version)",
171
+ "version": "1.2.0",
172
  "endpoints": {
173
  "/tryon": "POST - Virtual try-on inference",
174
+ "/demo": "GET - Web browser demo interface",
175
  "/health": "GET - Health check"
176
  }
177
  }
178
 
179
 
180
+ @app.get("/demo")
181
+ async def get_demo():
182
+ """Serve the static demo.html file"""
183
+ from fastapi.responses import FileResponse
184
+ demo_path = os.path.join(os.path.dirname(__file__), "demo.html")
185
+ if os.path.exists(demo_path):
186
+ return FileResponse(demo_path)
187
+ raise HTTPException(status_code=404, detail="demo.html not found")
188
+
189
+
190
  @app.get("/health")
191
  async def health_check():
192
  """Health check endpoint"""
193
  return {
194
  "status": "healthy",
195
+ "model_connected": model is not None,
196
  "timestamp": time.time()
197
  }
198
 
199
 
200
  @app.post("/tryon", response_model=TryOnResponse)
201
  async def virtual_tryon(
202
+ request: Request,
203
  person_image: UploadFile = File(..., description="Full-body photo of person"),
204
+ garment_image: UploadFile = File(..., description="Garment image"),
205
+ category: str = Form("tops"),
206
+ garment_photo_type: str = Form("model"),
207
+ num_timesteps: Optional[int] = Form(50),
208
+ guidance_scale: Optional[float] = Form(1.5),
209
+ seed: Optional[int] = Form(42),
210
+ segmentation_free: Optional[bool] = Form(True)
211
  ):
212
  """
213
+ Perform virtual try-on inference via remote API.
 
 
 
 
 
 
 
214
  """
215
  start_time = time.time()
216
 
217
  try:
218
+ # Check if model client is initialized
219
  if model is None:
220
  raise HTTPException(
221
  status_code=503,
222
+ detail="Remote API client not initialized. Please check server logs."
223
  )
224
 
225
+ # Step 1: Request Received
226
+ print("\n>>> Step 1: Request received at /tryon")
227
+ print(f"Headers: {request.headers}")
228
+ logger.info("Step 1: Request received and logging started")
229
+
230
+ # Step 2: Validate Files
231
  validate_image(person_image)
232
  validate_image(garment_image)
233
+ print(">>> Step 2: Input images validated successfully")
234
+ logger.info("Step 2: Input images validated successfully")
235
 
236
+ # Step 3: Read Images
 
 
237
  person_img = await read_image_from_upload(person_image)
238
  garment_img = await read_image_from_upload(garment_image)
239
+ print(">>> Step 3: Images loaded into memory")
240
+ logger.info("Step 3: Images loaded into memory")
241
 
242
+ # Step 4: Call Remote Inference
243
+ print(f">>> Step 4: Calling remote Gradio API (Category: {category})")
244
+ logger.info("Step 4: Calling remote Gradio API inference...")
 
 
 
 
245
 
246
+ # Ensure correct types for the model wrapper
247
+ n_steps = int(num_timesteps) if num_timesteps is not None else 50
248
+ g_scale = float(guidance_scale) if guidance_scale is not None else 1.5
249
+ s_seed = int(seed) if seed is not None else 42
250
+ seg_free = bool(segmentation_free) if segmentation_free is not None else True
251
+
252
+ result_image = model.tryon(
253
+ person_image=person_img,
254
+ garment_image=garment_img,
255
+ category=category,
256
+ garment_photo_type=garment_photo_type,
257
+ num_timesteps=n_steps,
258
+ guidance_scale=g_scale,
259
+ seed=s_seed,
260
+ segmentation_free=seg_free
261
+ )
262
+ print(">>> Step 5: Remote inference completed successfully")
263
+ logger.info("Step 5: Remote inference completed")
264
+
265
+ # Step 6: Convert to base64
266
+ result_base64 = image_to_base64(result_image)
267
+ print(">>> Step 6: Result processed and sending to frontend")
268
+
269
+ processing_time = time.time() - start_time
270
+ logger.info(f"Remote inference completed in {processing_time:.2f}s")
271
+
272
+ return TryOnResponse(
273
+ success=True,
274
+ result_image=result_base64,
275
+ processing_time=processing_time
276
+ )
277
 
278
  except HTTPException:
279
  raise