tryon-vton-api / app.py
Burhan Bhatti
Send a blank mask layer so CatVTON automasking kicks in
95e381c
Raw
History Blame Contribute Delete
4.99 kB
"""
Virtual Try-On API — diffusion-based, backed by the CatVTON Space.
Exposes two surfaces:
- a Gradio UI for manual testing
- api_name="try_on", a base64-in / JSON-out endpoint the web backend calls
Why this Space is a bridge rather than a model host: CatVTON's own Space already
runs the weights on ZeroGPU. Calling it needs gradio_client — its file-upload
handshake is not reachable over plain HTTP (/gradio_api/upload returns 404), so
the Node backend cannot talk to it directly. This Space owns that handshake and
the HF token, and keeps the simple base64 contract the backend was built against.
"""
import base64
import inspect
import io
import json
import os
import tempfile
import gradio as gr
import spaces
from gradio_client import Client, handle_file
from PIL import Image
VTON_SPACE = os.environ.get("VTON_SPACE", "zhengchong/CatVTON")
HF_TOKEN = os.environ.get("HF_TOKEN")
CLOTH_TYPES = ["upper", "lower", "overall"]
_client = None
def get_client() -> Client:
global _client
if _client is None:
# gradio_client renamed this kwarg from hf_token to token in 2.0; the
# Space and local dev are pinned to different majors.
params = inspect.signature(Client.__init__).parameters
kwarg = "token" if "token" in params else "hf_token"
_client = Client(VTON_SPACE, **{kwarg: HF_TOKEN})
return _client
def to_b64(img: Image.Image) -> str:
buf = io.BytesIO()
img.save(buf, format="JPEG", quality=90)
return base64.b64encode(buf.getvalue()).decode()
def _write_temp(img: Image.Image) -> str:
path = tempfile.mktemp(suffix=".png")
img.convert("RGB").save(path)
return path
def try_on(
person_img: Image.Image,
garment_img: Image.Image,
cloth_type: str = "upper",
steps: int = 30,
guidance: float = 2.5,
seed: int = 42,
) -> Image.Image:
person_path = _write_temp(person_img)
garment_path = _write_temp(garment_img)
# CatVTON's handler unconditionally reads person_image["layers"][0] as the
# hand-drawn mask, so the layer has to exist. A fully transparent one is how
# its UI represents "nothing drawn", which is what selects automasking.
mask_path = tempfile.mktemp(suffix=".png")
Image.new("RGBA", person_img.size, (0, 0, 0, 0)).save(mask_path)
result = get_client().predict(
person_image={
"background": handle_file(person_path),
"layers": [handle_file(mask_path)],
"composite": handle_file(person_path),
},
cloth_image=handle_file(garment_path),
cloth_type=cloth_type if cloth_type in CLOTH_TYPES else "upper",
num_inference_steps=steps,
guidance_scale=guidance,
seed=seed,
show_type="result only",
api_name="/submit_function",
)
# predict() hands back a local path, or a dict when the output is a gallery item.
if isinstance(result, dict):
result = result.get("path") or result.get("value")
if isinstance(result, (list, tuple)):
result = result[0]
return Image.open(result).convert("RGB")
@spaces.GPU(duration=1)
def _zerogpu_startup_probe():
"""Unused. ZeroGPU refuses to boot a Space with no @spaces.GPU function."""
return None
def try_on_api(person_b64: str, garment_b64: str, cloth_type: str) -> str:
"""base64 JPEG in, JSON string out: {"output": b64} or {"error": msg}"""
try:
person = Image.open(io.BytesIO(base64.b64decode(person_b64)))
garment = Image.open(io.BytesIO(base64.b64decode(garment_b64)))
result = try_on(person, garment, cloth_type or "upper")
return json.dumps({"output": to_b64(result)})
except Exception as e:
return json.dumps({"error": f"{type(e).__name__}: {e}"})
with gr.Blocks() as demo:
gr.Markdown("# Virtual Try-On API")
gr.Markdown(f"Diffusion try-on via `{VTON_SPACE}`.")
with gr.Row():
person_input = gr.Image(label="Person Photo", type="pil")
garment_input = gr.Image(label="Garment Photo", type="pil")
cloth_type_input = gr.Radio(
CLOTH_TYPES, value="upper", label="Garment type"
)
output_image = gr.Image(label="Try-On Result")
submit_btn = gr.Button("Generate Try-On", variant="primary")
submit_btn.click(
fn=lambda p, g, t: try_on(p, g, t) if p is not None and g is not None else None,
inputs=[person_input, garment_input, cloth_type_input],
outputs=output_image,
api_name=False,
)
# Programmatic endpoint used by the web backend.
api_person = gr.Textbox(visible=False)
api_garment = gr.Textbox(visible=False)
api_cloth_type = gr.Textbox(visible=False, value="upper")
api_result = gr.Textbox(visible=False)
api_trigger = gr.Button(visible=False)
api_trigger.click(
fn=try_on_api,
inputs=[api_person, api_garment, api_cloth_type],
outputs=api_result,
api_name="try_on",
)
if __name__ == "__main__":
demo.launch()