Spaces:
Sleeping
Sleeping
StableVITON Deployer commited on
Commit ·
1ca64e8
1
Parent(s): 90f25e3
Fix: Corrected StableVITONInference initialization and aligned logs
Browse files
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 |
-
|
| 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.
|
| 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 |
-
"
|
| 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
|
| 242 |
if model is None:
|
| 243 |
raise HTTPException(
|
| 244 |
status_code=503,
|
| 245 |
-
detail="
|
| 246 |
)
|
| 247 |
|
| 248 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
validate_image(person_image)
|
| 250 |
validate_image(garment_image)
|
|
|
|
|
|
|
| 251 |
|
| 252 |
-
|
| 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 |
-
#
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
raise HTTPException(
|
| 262 |
-
status_code=503,
|
| 263 |
-
detail="Server is busy processing another request. Please try again in a moment."
|
| 264 |
-
)
|
| 265 |
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|