Davidscut commited on
Commit
d85819f
·
1 Parent(s): 06923e8

Add ZeroGPU virtual try-on demo

Browse files
Files changed (6) hide show
  1. README.md +120 -8
  2. app.py +120 -121
  3. inference.py +262 -0
  4. packages.txt +5 -0
  5. preprocess.py +71 -0
  6. requirements.txt +13 -3
README.md CHANGED
@@ -1,14 +1,126 @@
1
  ---
2
- title: Tryon Demo
3
- emoji: 🖼
4
- colorFrom: purple
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.5.1
 
8
  app_file: app.py
9
  pinned: false
10
- license: apache-2.0
11
- short_description: A demo for our unified virtual tryon model
12
  ---
13
 
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Virtual Try-On ZeroGPU Demo
3
+ emoji: VTON
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 5.0.0
8
+ python_version: 3.10
9
  app_file: app.py
10
  pinned: false
11
+ license: other
12
+ short_description: ZeroGPU xlarge Gradio demo for image-based virtual try-on.
13
  ---
14
 
15
+ # Virtual Try-On ZeroGPU Demo
16
+
17
+ This Space is a Gradio SDK deployment wrapper for a virtual try-on model.
18
+
19
+ ## Inputs
20
+
21
+ - One person image
22
+ - One or more garment reference images
23
+ - Garment category
24
+ - Primary garment index
25
+ - Seed, inference steps, guidance scale, and auto-crop controls
26
+
27
+ ## Output
28
+
29
+ - One generated try-on image
30
+
31
+ ## ZeroGPU
32
+
33
+ The inference callback is decorated with:
34
+
35
+ ```python
36
+ @spaces.GPU(size="xlarge", duration=180)
37
+ ```
38
+
39
+ Use ZeroGPU `xlarge` because the model is expected to need at least 60 GB of VRAM.
40
+
41
+ ## Required Space Variables
42
+
43
+ Set these in the Space settings:
44
+
45
+ ```text
46
+ MODEL_ID=your-org/your-vton-model
47
+ ```
48
+
49
+ If the model repository is private, add this as a Space Secret:
50
+
51
+ ```text
52
+ HF_TOKEN=hf_xxxxxxxxxxxxxxxxx
53
+ ```
54
+
55
+ Optional variables:
56
+
57
+ ```text
58
+ MODEL_REVISION=main
59
+ MODEL_PIPELINE_MODULE=pipeline
60
+ MODEL_PIPELINE_CLASS=VirtualTryOnPipeline
61
+ MODEL_SUPPORTS_MULTI_GARMENT=false
62
+ MODEL_TORCH_DTYPE=float16
63
+ HF_HOME=/data/.huggingface
64
+ ```
65
+
66
+ ## Expected Model Repository Interface
67
+
68
+ This wrapper downloads the model repository with `huggingface_hub.snapshot_download`.
69
+ By default, it expects the model repo to contain:
70
+
71
+ ```text
72
+ pipeline.py
73
+ ```
74
+
75
+ and inside it:
76
+
77
+ ```python
78
+ class VirtualTryOnPipeline:
79
+ @classmethod
80
+ def from_pretrained(cls, model_dir, torch_dtype=None):
81
+ ...
82
+
83
+ def to(self, device):
84
+ ...
85
+
86
+ def __call__(
87
+ self,
88
+ person_image,
89
+ garment_image=None,
90
+ garment_images=None,
91
+ garment_type="upper_body",
92
+ num_inference_steps=30,
93
+ guidance_scale=2.5,
94
+ generator=None,
95
+ seed=42,
96
+ auto_crop=True,
97
+ ):
98
+ ...
99
+ ```
100
+
101
+ The return value may be:
102
+
103
+ - `PIL.Image.Image`
104
+ - a list or tuple whose first element is a PIL image
105
+ - an object with `.images[0]`
106
+ - a dict containing `image`, `images`, or `result`
107
+
108
+ If your current virtual try-on model only supports a single garment image, keep:
109
+
110
+ ```text
111
+ MODEL_SUPPORTS_MULTI_GARMENT=false
112
+ ```
113
+
114
+ The app will use `primary_garment_index` to select exactly one garment image from the uploaded references. It does not pretend to perform multi-reference fusion.
115
+
116
+ ## Deployment Steps
117
+
118
+ 1. Create a new Hugging Face Space with SDK `Gradio` and hardware `ZeroGPU`.
119
+ 2. Copy these files into the Space repository.
120
+ 3. Add any model-specific Python dependencies to `requirements.txt`.
121
+ 4. Set `MODEL_ID` as a Space Variable.
122
+ 5. If the model is private, set `HF_TOKEN` as a Space Secret.
123
+ 6. Push the Space repository.
124
+ 7. Open the Space and run a test with one person image and one garment image.
125
+
126
+ Do not commit large model weights to this Space repository. Keep weights in a Hugging Face Model Repo.
app.py CHANGED
@@ -1,154 +1,153 @@
1
- import gradio as gr
2
- import numpy as np
3
  import random
 
 
 
 
 
4
 
5
- # import spaces #[uncomment to use ZeroGPU]
6
- from diffusers import DiffusionPipeline
7
- import torch
8
 
9
- device = "cuda" if torch.cuda.is_available() else "cpu"
10
- model_repo_id = "stabilityai/sdxl-turbo" # Replace to the model you would like to use
11
 
12
- if torch.cuda.is_available():
13
- torch_dtype = torch.float16
14
- else:
15
- torch_dtype = torch.float32
16
 
17
- pipe = DiffusionPipeline.from_pretrained(model_repo_id, torch_dtype=torch_dtype)
18
- pipe = pipe.to(device)
 
19
 
20
- MAX_SEED = np.iinfo(np.int32).max
21
- MAX_IMAGE_SIZE = 1024
22
 
23
 
24
- # @spaces.GPU #[uncomment to use ZeroGPU]
25
- def infer(
26
- prompt,
27
- negative_prompt,
28
- seed,
29
- randomize_seed,
30
- width,
31
- height,
32
- guidance_scale,
33
- num_inference_steps,
34
- progress=gr.Progress(track_tqdm=True),
35
  ):
36
- if randomize_seed:
37
- seed = random.randint(0, MAX_SEED)
38
-
39
- generator = torch.Generator().manual_seed(seed)
40
-
41
- image = pipe(
42
- prompt=prompt,
43
- negative_prompt=negative_prompt,
44
- guidance_scale=guidance_scale,
45
- num_inference_steps=num_inference_steps,
46
- width=width,
47
- height=height,
48
- generator=generator,
49
- ).images[0]
50
-
51
- return image, seed
52
-
53
-
54
- examples = [
55
- "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k",
56
- "An astronaut riding a green horse",
57
- "A delicious ceviche cheesecake slice",
58
- ]
59
-
60
- css = """
61
- #col-container {
62
- margin: 0 auto;
63
- max-width: 640px;
64
- }
65
- """
66
-
67
- with gr.Blocks(css=css) as demo:
68
- with gr.Column(elem_id="col-container"):
69
- gr.Markdown(" # Text-to-Image Gradio Template")
70
-
71
- with gr.Row():
72
- prompt = gr.Text(
73
- label="Prompt",
74
- show_label=False,
75
- max_lines=1,
76
- placeholder="Enter your prompt",
77
- container=False,
78
- )
79
 
80
- run_button = gr.Button("Run", scale=0, variant="primary")
 
 
 
 
 
 
81
 
82
- result = gr.Image(label="Result", show_label=False)
 
 
 
 
 
83
 
84
- with gr.Accordion("Advanced Settings", open=False):
85
- negative_prompt = gr.Text(
86
- label="Negative prompt",
87
- max_lines=1,
88
- placeholder="Enter a negative prompt",
89
- visible=False,
90
  )
91
 
92
- seed = gr.Slider(
93
- label="Seed",
94
  minimum=0,
95
- maximum=MAX_SEED,
96
- step=1,
97
  value=0,
 
 
98
  )
99
 
100
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
101
-
102
- with gr.Row():
103
- width = gr.Slider(
104
- label="Width",
105
- minimum=256,
106
- maximum=MAX_IMAGE_SIZE,
107
- step=32,
108
- value=1024, # Replace with defaults that work for your model
109
- )
110
-
111
- height = gr.Slider(
112
- label="Height",
113
- minimum=256,
114
- maximum=MAX_IMAGE_SIZE,
115
- step=32,
116
- value=1024, # Replace with defaults that work for your model
117
  )
118
-
119
- with gr.Row():
120
  guidance_scale = gr.Slider(
121
- label="Guidance scale",
122
  minimum=0.0,
123
- maximum=10.0,
 
124
  step=0.1,
125
- value=0.0, # Replace with defaults that work for your model
126
  )
 
127
 
128
- num_inference_steps = gr.Slider(
129
- label="Number of inference steps",
130
- minimum=1,
131
- maximum=50,
132
- step=1,
133
- value=2, # Replace with defaults that work for your model
134
- )
135
 
136
- gr.Examples(examples=examples, inputs=[prompt])
137
- gr.on(
138
- triggers=[run_button.click, prompt.submit],
139
- fn=infer,
140
  inputs=[
141
- prompt,
142
- negative_prompt,
 
 
143
  seed,
144
  randomize_seed,
145
- width,
146
- height,
147
- guidance_scale,
148
  num_inference_steps,
 
 
149
  ],
150
- outputs=[result, seed],
 
151
  )
152
 
 
153
  if __name__ == "__main__":
154
- demo.launch()
 
 
1
+ from __future__ import annotations
2
+
3
  import random
4
+ import traceback
5
+
6
+ import gradio as gr
7
+ import spaces
8
+ from PIL import Image
9
 
10
+ from inference import run_tryon
11
+ from preprocess import load_garment_images, resize_for_demo
 
12
 
 
 
13
 
14
+ def _validate_inputs(person_image: Image.Image | None, garment_files) -> list[Image.Image]:
15
+ if person_image is None:
16
+ raise gr.Error("Please upload a person image.")
 
17
 
18
+ garment_images = load_garment_images(garment_files)
19
+ if not garment_images:
20
+ raise gr.Error("Please upload at least one garment reference image.")
21
 
22
+ return garment_images
 
23
 
24
 
25
+ @spaces.GPU(size="xlarge", duration=180)
26
+ def predict(
27
+ person_image: Image.Image,
28
+ garment_files,
29
+ garment_type: str,
30
+ primary_garment_index: int,
31
+ seed: int,
32
+ randomize_seed: bool,
33
+ num_inference_steps: int,
34
+ guidance_scale: float,
35
+ auto_crop: bool,
36
  ):
37
+ try:
38
+ garment_images = _validate_inputs(person_image, garment_files)
39
+
40
+ if randomize_seed:
41
+ seed = random.randint(0, 2**31 - 1)
42
+
43
+ person = resize_for_demo(person_image, max_side=1024)
44
+ garments = [resize_for_demo(img, max_side=1024) for img in garment_images]
45
+
46
+ result, used_index, status = run_tryon(
47
+ person_image=person,
48
+ garment_images=garments,
49
+ garment_type=garment_type,
50
+ primary_garment_index=int(primary_garment_index),
51
+ seed=int(seed),
52
+ num_inference_steps=int(num_inference_steps),
53
+ guidance_scale=float(guidance_scale),
54
+ auto_crop=bool(auto_crop),
55
+ )
56
+
57
+ return result, int(seed), used_index, status
58
+
59
+ except gr.Error:
60
+ raise
61
+ except Exception as exc:
62
+ traceback.print_exc()
63
+ raise gr.Error(f"Inference failed: {exc}") from exc
64
+
65
+
66
+ with gr.Blocks(title="Virtual Try-On ZeroGPU Demo") as demo:
67
+ gr.Markdown(
68
+ """
69
+ # Virtual Try-On ZeroGPU Demo
70
+
71
+ Upload one person image and one or more garment reference images.
72
+ If the underlying model only supports one garment image, the selected
73
+ primary garment index is used as the single reference image.
74
+ """
75
+ )
 
 
 
 
76
 
77
+ with gr.Row():
78
+ with gr.Column(scale=1):
79
+ person_image = gr.Image(
80
+ label="Person Image",
81
+ type="pil",
82
+ sources=["upload", "clipboard"],
83
+ )
84
 
85
+ garment_files = gr.File(
86
+ label="Garment Reference Images",
87
+ file_count="multiple",
88
+ file_types=["image"],
89
+ type="filepath",
90
+ )
91
 
92
+ garment_type = gr.Radio(
93
+ choices=["upper_body", "lower_body", "dress"],
94
+ value="upper_body",
95
+ label="Garment Type",
 
 
96
  )
97
 
98
+ primary_garment_index = gr.Slider(
 
99
  minimum=0,
100
+ maximum=9,
 
101
  value=0,
102
+ step=1,
103
+ label="Primary Garment Index",
104
  )
105
 
106
+ with gr.Accordion("Advanced Settings", open=False):
107
+ seed = gr.Number(value=42, precision=0, label="Seed")
108
+ randomize_seed = gr.Checkbox(value=True, label="Randomize Seed")
109
+ num_inference_steps = gr.Slider(
110
+ minimum=10,
111
+ maximum=80,
112
+ value=30,
113
+ step=1,
114
+ label="Inference Steps",
 
 
 
 
 
 
 
 
115
  )
 
 
116
  guidance_scale = gr.Slider(
 
117
  minimum=0.0,
118
+ maximum=15.0,
119
+ value=2.5,
120
  step=0.1,
121
+ label="Guidance Scale",
122
  )
123
+ auto_crop = gr.Checkbox(value=True, label="Auto Crop Person Image")
124
 
125
+ run_button = gr.Button("Generate Try-On", variant="primary")
126
+
127
+ with gr.Column(scale=1):
128
+ output_image = gr.Image(label="Try-On Result", type="pil")
129
+ used_seed = gr.Number(label="Used Seed", precision=0)
130
+ used_garment_index = gr.Number(label="Used Garment Index", precision=0)
131
+ status = gr.Textbox(label="Status", interactive=False)
132
 
133
+ run_button.click(
134
+ fn=predict,
 
 
135
  inputs=[
136
+ person_image,
137
+ garment_files,
138
+ garment_type,
139
+ primary_garment_index,
140
  seed,
141
  randomize_seed,
 
 
 
142
  num_inference_steps,
143
+ guidance_scale,
144
+ auto_crop,
145
  ],
146
+ outputs=[output_image, used_seed, used_garment_index, status],
147
+ api_name="tryon",
148
  )
149
 
150
+
151
  if __name__ == "__main__":
152
+ demo.queue(max_size=20).launch()
153
+
inference.py ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import importlib.util
5
+ import inspect
6
+ import os
7
+ import random
8
+ import sys
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+ from typing import Any
12
+
13
+ import numpy as np
14
+ import torch
15
+ from huggingface_hub import snapshot_download
16
+ from PIL import Image
17
+
18
+ from preprocess import select_primary_garment
19
+
20
+
21
+ _MODEL: Any | None = None
22
+ _MODEL_DIR: Path | None = None
23
+
24
+
25
+ def _get_bool_env(name: str, default: bool = False) -> bool:
26
+ value = os.getenv(name)
27
+ if value is None:
28
+ return default
29
+ return value.strip().lower() in {"1", "true", "yes", "on"}
30
+
31
+
32
+ def _get_torch_dtype() -> torch.dtype:
33
+ value = os.getenv("MODEL_TORCH_DTYPE", "float16").strip().lower()
34
+ if value in {"bfloat16", "bf16"}:
35
+ return torch.bfloat16
36
+ if value in {"float32", "fp32"}:
37
+ return torch.float32
38
+ return torch.float16
39
+
40
+
41
+ def _device() -> torch.device:
42
+ if not torch.cuda.is_available():
43
+ raise RuntimeError(
44
+ "CUDA is not available. Make sure the Space hardware is set to ZeroGPU "
45
+ "and that inference is called from a function decorated with @spaces.GPU."
46
+ )
47
+ return torch.device("cuda")
48
+
49
+
50
+ def set_seed(seed: int) -> torch.Generator:
51
+ seed = int(seed)
52
+ random.seed(seed)
53
+ np.random.seed(seed)
54
+ torch.manual_seed(seed)
55
+ torch.cuda.manual_seed_all(seed)
56
+ return torch.Generator(device="cuda").manual_seed(seed)
57
+
58
+
59
+ def _download_model_repo() -> Path:
60
+ model_id = os.getenv("MODEL_ID")
61
+ if not model_id:
62
+ raise RuntimeError("MODEL_ID is not set. Add it as a Hugging Face Space Variable.")
63
+
64
+ token = os.getenv("HF_TOKEN")
65
+ revision = os.getenv("MODEL_REVISION") or None
66
+ local_dir = os.getenv("MODEL_LOCAL_DIR") or None
67
+
68
+ kwargs = {
69
+ "repo_id": model_id,
70
+ "revision": revision,
71
+ "token": token,
72
+ }
73
+ if local_dir:
74
+ kwargs["local_dir"] = local_dir
75
+ kwargs["local_dir_use_symlinks"] = False
76
+
77
+ path = snapshot_download(**kwargs)
78
+ return Path(path)
79
+
80
+
81
+ def _load_module_from_file(module_path: Path) -> ModuleType:
82
+ module_name = f"vton_model_{module_path.stem}"
83
+ spec = importlib.util.spec_from_file_location(module_name, str(module_path))
84
+ if spec is None or spec.loader is None:
85
+ raise ImportError(f"Could not import module from {module_path}")
86
+ module = importlib.util.module_from_spec(spec)
87
+ sys.modules[module_name] = module
88
+ spec.loader.exec_module(module)
89
+ return module
90
+
91
+
92
+ def _load_pipeline_module(model_dir: Path) -> ModuleType:
93
+ module_name = os.getenv("MODEL_PIPELINE_MODULE", "pipeline")
94
+
95
+ file_candidate = model_dir / f"{module_name.replace('.', '/')}.py"
96
+ if file_candidate.exists():
97
+ return _load_module_from_file(file_candidate)
98
+
99
+ if str(model_dir) not in sys.path:
100
+ sys.path.insert(0, str(model_dir))
101
+
102
+ return importlib.import_module(module_name)
103
+
104
+
105
+ def _instantiate_pipeline(module: ModuleType, model_dir: Path) -> Any:
106
+ class_name = os.getenv("MODEL_PIPELINE_CLASS", "VirtualTryOnPipeline")
107
+ if not hasattr(module, class_name):
108
+ raise AttributeError(
109
+ f"Model pipeline module '{module.__name__}' does not define '{class_name}'. "
110
+ "Set MODEL_PIPELINE_CLASS or provide a compatible pipeline class."
111
+ )
112
+
113
+ cls = getattr(module, class_name)
114
+ dtype = _get_torch_dtype()
115
+
116
+ if hasattr(cls, "from_pretrained") and callable(getattr(cls, "from_pretrained")):
117
+ try:
118
+ return cls.from_pretrained(str(model_dir), torch_dtype=dtype, token=os.getenv("HF_TOKEN"))
119
+ except TypeError:
120
+ try:
121
+ return cls.from_pretrained(str(model_dir), torch_dtype=dtype)
122
+ except TypeError:
123
+ return cls.from_pretrained(str(model_dir))
124
+
125
+ try:
126
+ return cls(str(model_dir), torch_dtype=dtype)
127
+ except TypeError:
128
+ return cls(str(model_dir))
129
+
130
+
131
+ def load_model() -> Any:
132
+ """Download and load the virtual try-on model once per Space process."""
133
+ global _MODEL, _MODEL_DIR
134
+
135
+ if _MODEL is not None:
136
+ return _MODEL
137
+
138
+ _MODEL_DIR = _download_model_repo()
139
+ module = _load_pipeline_module(_MODEL_DIR)
140
+ model = _instantiate_pipeline(module, _MODEL_DIR)
141
+
142
+ device = _device()
143
+ if hasattr(model, "to") and callable(getattr(model, "to")):
144
+ model = model.to(device)
145
+ if hasattr(model, "eval") and callable(getattr(model, "eval")):
146
+ model.eval()
147
+
148
+ _MODEL = model
149
+ return _MODEL
150
+
151
+
152
+ def _call_model(
153
+ model: Any,
154
+ *,
155
+ person_image: Image.Image,
156
+ garment_image: Image.Image,
157
+ garment_images: list[Image.Image],
158
+ garment_type: str,
159
+ seed: int,
160
+ num_inference_steps: int,
161
+ guidance_scale: float,
162
+ auto_crop: bool,
163
+ generator: torch.Generator,
164
+ ) -> Any:
165
+ supports_multi = _get_bool_env("MODEL_SUPPORTS_MULTI_GARMENT", False)
166
+
167
+ kwargs = {
168
+ "person_image": person_image,
169
+ "garment_type": garment_type,
170
+ "num_inference_steps": int(num_inference_steps),
171
+ "guidance_scale": float(guidance_scale),
172
+ "generator": generator,
173
+ "seed": int(seed),
174
+ "auto_crop": bool(auto_crop),
175
+ }
176
+
177
+ if supports_multi:
178
+ kwargs["garment_images"] = garment_images
179
+ else:
180
+ kwargs["garment_image"] = garment_image
181
+
182
+ call = model.run_tryon if hasattr(model, "run_tryon") else model
183
+ signature = inspect.signature(call)
184
+ accepted_kwargs = {
185
+ key: value
186
+ for key, value in kwargs.items()
187
+ if key in signature.parameters or any(p.kind == p.VAR_KEYWORD for p in signature.parameters.values())
188
+ }
189
+ return call(**accepted_kwargs)
190
+
191
+
192
+ def _extract_image(result: Any) -> Image.Image:
193
+ if isinstance(result, Image.Image):
194
+ return result.convert("RGB")
195
+
196
+ if isinstance(result, dict):
197
+ for key in ("image", "result", "output"):
198
+ if key in result:
199
+ return _extract_image(result[key])
200
+ if "images" in result:
201
+ return _extract_image(result["images"])
202
+
203
+ if hasattr(result, "images"):
204
+ return _extract_image(result.images)
205
+
206
+ if isinstance(result, (list, tuple)) and result:
207
+ return _extract_image(result[0])
208
+
209
+ raise TypeError(
210
+ "The model output could not be converted to a PIL image. Return a PIL image, "
211
+ "a list of PIL images, an object with .images, or a dict with image/images/result."
212
+ )
213
+
214
+
215
+ @torch.inference_mode()
216
+ def run_tryon(
217
+ person_image: Image.Image,
218
+ garment_images: list[Image.Image],
219
+ garment_type: str = "upper_body",
220
+ primary_garment_index: int = 0,
221
+ seed: int = 42,
222
+ num_inference_steps: int = 30,
223
+ guidance_scale: float = 2.5,
224
+ auto_crop: bool = True,
225
+ ) -> tuple[Image.Image, int, str]:
226
+ if person_image is None:
227
+ raise ValueError("person_image is required.")
228
+ if not garment_images:
229
+ raise ValueError("At least one garment reference image is required.")
230
+
231
+ primary_garment, used_index, all_garments = select_primary_garment(
232
+ garment_images,
233
+ primary_garment_index,
234
+ )
235
+ generator = set_seed(seed)
236
+ model = load_model()
237
+
238
+ result = _call_model(
239
+ model,
240
+ person_image=person_image.convert("RGB"),
241
+ garment_image=primary_garment.convert("RGB"),
242
+ garment_images=[img.convert("RGB") for img in all_garments],
243
+ garment_type=garment_type,
244
+ seed=seed,
245
+ num_inference_steps=num_inference_steps,
246
+ guidance_scale=guidance_scale,
247
+ auto_crop=auto_crop,
248
+ generator=generator,
249
+ )
250
+
251
+ image = _extract_image(result)
252
+ mode = "multi-reference" if _get_bool_env("MODEL_SUPPORTS_MULTI_GARMENT", False) else "single-reference"
253
+ status = f"Success. Used garment index {used_index} in {mode} mode."
254
+ return image, used_index, status
255
+
256
+
257
+ def clear_model_cache() -> None:
258
+ global _MODEL, _MODEL_DIR
259
+ _MODEL = None
260
+ _MODEL_DIR = None
261
+ if torch.cuda.is_available():
262
+ torch.cuda.empty_cache()
packages.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ ffmpeg
2
+ libgl1
3
+ libglib2.0-0
4
+ git
5
+
preprocess.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Iterable
5
+
6
+ from PIL import Image, ImageOps
7
+
8
+
9
+ def load_garment_images(file_paths) -> list[Image.Image]:
10
+ """Load Gradio File outputs into RGB PIL images."""
11
+ if file_paths is None:
12
+ return []
13
+
14
+ if isinstance(file_paths, (str, Path)):
15
+ file_paths = [file_paths]
16
+
17
+ images: list[Image.Image] = []
18
+ for item in file_paths:
19
+ if item is None:
20
+ continue
21
+
22
+ path = item
23
+ if not isinstance(path, (str, Path)) and hasattr(path, "name"):
24
+ path = path.name
25
+
26
+ p = Path(path)
27
+ if not p.exists():
28
+ continue
29
+
30
+ with Image.open(p) as img:
31
+ images.append(ImageOps.exif_transpose(img).convert("RGB"))
32
+
33
+ return images
34
+
35
+
36
+ def normalize_pil_image(image: Image.Image | None) -> Image.Image | None:
37
+ if image is None:
38
+ return None
39
+ return ImageOps.exif_transpose(image).convert("RGB")
40
+
41
+
42
+ def resize_for_demo(image: Image.Image, max_side: int = 1024) -> Image.Image:
43
+ """Downscale very large uploads while preserving aspect ratio."""
44
+ image = normalize_pil_image(image)
45
+ if image is None:
46
+ raise ValueError("image is required")
47
+
48
+ width, height = image.size
49
+ longest = max(width, height)
50
+ if longest <= max_side:
51
+ return image
52
+
53
+ scale = max_side / float(longest)
54
+ new_size = (max(1, int(width * scale)), max(1, int(height * scale)))
55
+ return image.resize(new_size, Image.Resampling.LANCZOS)
56
+
57
+
58
+ def clamp_primary_index(primary_garment_index: int, garment_count: int) -> int:
59
+ if garment_count <= 0:
60
+ raise ValueError("At least one garment reference image is required.")
61
+ return max(0, min(int(primary_garment_index), garment_count - 1))
62
+
63
+
64
+ def select_primary_garment(
65
+ garment_images: Iterable[Image.Image],
66
+ primary_garment_index: int,
67
+ ) -> tuple[Image.Image, int, list[Image.Image]]:
68
+ images = list(garment_images)
69
+ index = clamp_primary_index(primary_garment_index, len(images))
70
+ return images[index], index, images
71
+
requirements.txt CHANGED
@@ -1,6 +1,16 @@
 
 
 
 
 
1
  accelerate
2
  diffusers
3
- invisible_watermark
4
- torch
5
  transformers
6
- xformers
 
 
 
 
 
 
 
 
1
+ gradio>=5.0.0
2
+ spaces
3
+ huggingface_hub>=0.27.0
4
+ torch>=2.4.0
5
+ torchvision
6
  accelerate
7
  diffusers
 
 
8
  transformers
9
+ safetensors
10
+ pillow
11
+ opencv-python-headless
12
+ numpy
13
+ scipy
14
+ einops
15
+ peft
16
+