Dell commited on
Commit
354d532
·
1 Parent(s): b9599e0
.history/app_20260617195348.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import gradio as gr
10
+ import numpy as np
11
+ import torch
12
+ from diffusers.image_processor import VaeImageProcessor
13
+ from huggingface_hub import snapshot_download
14
+ from PIL import Image, ImageOps
15
+
16
+ APP_TITLE = "ChitraTech Virtual Try-On"
17
+ APP_DESCRIPTION = (
18
+ "Upload a shopper photo and clothing image to run on-demand CatVTON virtual try-on inference "
19
+ "using the Zheng-Chong CatVTON implementation."
20
+ )
21
+ CATVTON_REPO_DIR_ENV = os.getenv("CATVTON_REPO_DIR")
22
+ CATVTON_REPO_DIR = Path(CATVTON_REPO_DIR_ENV) if CATVTON_REPO_DIR_ENV else Path("./CatVTON")
23
+ CATVTON_RESUME_PATH = os.getenv("CATVTON_RESUME_PATH", "zhengchong/CatVTON")
24
+
25
+
26
+ def resolve_catvton_repo_dir(start_dir: Path) -> Path:
27
+ """Find the CatVTON repo root that contains `model/cloth_masker.py`.
28
+
29
+ HF Spaces sometimes mount code in unexpected places; relying on fixed paths like
30
+ `/app/CatVTON` can be wrong. We therefore:
31
+ 1) try a few common candidates
32
+ 2) then scan under `/app` (and `/workspace` if present) for `model/cloth_masker.py`
33
+ """
34
+
35
+ def looks_like_repo_dir(p: Path) -> bool:
36
+ return (p / "model" / "cloth_masker.py").exists() and (p / "model" / "pipeline.py").exists()
37
+
38
+ candidates: list[Path] = []
39
+
40
+ if start_dir is not None:
41
+ candidates.append(start_dir)
42
+
43
+ if CATVTON_REPO_DIR_ENV:
44
+ candidates.append(Path(CATVTON_REPO_DIR_ENV))
45
+
46
+ candidates.extend([
47
+ Path("/app/CatVTON"),
48
+ Path("/app"),
49
+ Path("./CatVTON"),
50
+ Path("./"),
51
+ Path("/workspace"),
52
+ ])
53
+
54
+ for c in candidates:
55
+ if c is not None and looks_like_repo_dir(c):
56
+ return c.resolve()
57
+
58
+ # Broad scan for the actual code root.
59
+ scan_roots = [Path("/app"), Path("/workspace")]
60
+ for root in scan_roots:
61
+ if not root.exists():
62
+ continue
63
+ for cloth_masker in root.rglob("model/cloth_masker.py"):
64
+ repo_root = cloth_masker.parent.parent # .../<repo_root>/model/cloth_masker.py
65
+ if looks_like_repo_dir(repo_root):
66
+ return repo_root.resolve()
67
+
68
+ # Fallback: return the provided start_dir (so error message includes candidates).
69
+ return start_dir.resolve()
70
+
71
+
72
+ CATVTON_BASE_MODEL = os.getenv("CATVTON_BASE_MODEL", "booksforcharlie/stable-diffusion-inpainting")
73
+ CATVTON_OUTPUT_DIR = Path(os.getenv("CATVTON_OUTPUT_DIR", "./outputs"))
74
+ DEVICE = os.getenv("CATVTON_DEVICE", "cuda")
75
+ DEFAULT_WIDTH = int(os.getenv("CATVTON_WIDTH", "768"))
76
+ DEFAULT_HEIGHT = int(os.getenv("CATVTON_HEIGHT", "1024"))
77
+ DEFAULT_STEPS = int(os.getenv("CATVTON_STEPS", "50"))
78
+ DEFAULT_GUIDANCE_SCALE = float(os.getenv("CATVTON_GUIDANCE_SCALE", "2.5"))
79
+ DEFAULT_MIXED_PRECISION = os.getenv("CATVTON_MIXED_PRECISION", "bf16")
80
+ DEFAULT_SEED = int(os.getenv("CATVTON_SEED", "42"))
81
+
82
+
83
+ @dataclass
84
+ class CatVTONRuntime:
85
+ repo_dir: Path
86
+ device: str
87
+ pipeline: object | None = field(default=None, init=False, repr=False)
88
+ automasker: object | None = field(default=None, init=False, repr=False)
89
+ mask_processor: object | None = field(default=None, init=False, repr=False)
90
+ resize_and_crop: object | None = field(default=None, init=False, repr=False)
91
+ resize_and_padding: object | None = field(default=None, init=False, repr=False)
92
+ vis_mask: object | None = field(default=None, init=False, repr=False)
93
+ ready: bool = False
94
+ status: str = "not loaded"
95
+
96
+ def load(self) -> None:
97
+ if self.ready:
98
+ return
99
+
100
+ if not self.repo_dir.exists():
101
+ raise RuntimeError(f"CatVTON repository not found at '{self.repo_dir}'.")
102
+
103
+ # --- Resolve real python root that contains `model/` ---
104
+ # Some HF environments mount the code differently; env/debug values can be wrong.
105
+ # We therefore detect the repo root by searching for `model/cloth_masker.py`.
106
+
107
+ scan_roots = [Path("/app"), Path("/workspace"), Path.cwd()]
108
+ found_model_parent: Path | None = None
109
+
110
+ for scan_root in scan_roots:
111
+ if not scan_root.exists():
112
+ continue
113
+ # bounded scan to avoid huge FS traversal
114
+ for cloth_masker in scan_root.rglob("model/cloth_masker.py"):
115
+ repo_root = cloth_masker.parent.parent
116
+ pipeline_file = repo_root / "model" / "pipeline.py"
117
+ if pipeline_file.exists():
118
+ found_model_parent = repo_root.resolve()
119
+ break
120
+ if found_model_parent is not None:
121
+ break
122
+
123
+ if found_model_parent is None:
124
+ # Keep existing behavior as last resort.
125
+ found_model_parent = self.repo_dir.resolve()
126
+
127
+ repo_path = str(found_model_parent)
128
+ if repo_path not in sys.path:
129
+ sys.path.insert(0, repo_path)
130
+
131
+ model_dir = (found_model_parent / "model").resolve()
132
+ if model_dir.exists():
133
+ model_parent_str = str(model_dir.parent)
134
+ if model_parent_str not in sys.path:
135
+ sys.path.insert(0, model_parent_str)
136
+
137
+ self.repo_dir = found_model_parent
138
+
139
+
140
+
141
+
142
+ # If this still fails inside HF, add debugging info.
143
+ try:
144
+ from model.cloth_masker import AutoMasker, vis_mask
145
+ from model.pipeline import CatVTONPipeline
146
+ except Exception as import_exc:
147
+ # Helpful diagnostics for HF Spaces.
148
+ repo_model_exists = (self.repo_dir / "model").exists()
149
+ candidate_roots = [
150
+ self.repo_dir,
151
+ self.repo_dir / "model",
152
+ (self.repo_dir / "model").parent,
153
+ ]
154
+ candidate_roots_str = ", ".join(str(p) for p in candidate_roots)
155
+
156
+ raise RuntimeError(
157
+ "CatVTON import failed. "
158
+ f"repo_dir={self.repo_dir} "
159
+ f"repo_dir/model_exists={repo_model_exists} "
160
+ f"repo_model_candidate_roots={candidate_roots_str} "
161
+ f"sys.path[0:10]={sys.path[:10]} "
162
+ f"import_error={import_exc}"
163
+ ) from import_exc
164
+
165
+
166
+ repo_weights_dir = Path(snapshot_download(repo_id=CATVTON_RESUME_PATH))
167
+ self.pipeline = CatVTONPipeline(
168
+ base_ckpt=CATVTON_BASE_MODEL,
169
+ attn_ckpt=str(repo_weights_dir),
170
+ attn_ckpt_version="mix",
171
+ weight_dtype=init_weight_dtype(DEFAULT_MIXED_PRECISION),
172
+ use_tf32=True,
173
+ device=self.device,
174
+ )
175
+ self.mask_processor = VaeImageProcessor(
176
+ vae_scale_factor=8,
177
+ do_normalize=False,
178
+ do_binarize=True,
179
+ do_convert_grayscale=True,
180
+ )
181
+ self.automasker = AutoMasker(
182
+ densepose_ckpt=os.path.join(repo_weights_dir, "DensePose"),
183
+ schp_ckpt=os.path.join(repo_weights_dir, "SCHP"),
184
+ device=self.device,
185
+ )
186
+ self.resize_and_crop = resize_and_crop
187
+ self.resize_and_padding = resize_and_padding
188
+ self.vis_mask = vis_mask
189
+ CATVTON_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
190
+ self.ready = True
191
+ self.status = "loaded"
192
+
193
+ def run(
194
+ self,
195
+ person_image: Image.Image,
196
+ garment_image: Image.Image,
197
+ cloth_type: str,
198
+ num_inference_steps: int,
199
+ guidance_scale: float,
200
+ seed: int,
201
+ show_type: str,
202
+ ) -> Image.Image:
203
+ self.load()
204
+ assert self.pipeline is not None
205
+ assert self.automasker is not None
206
+ assert self.mask_processor is not None
207
+ assert self.resize_and_crop is not None
208
+ assert self.resize_and_padding is not None
209
+ assert self.vis_mask is not None
210
+
211
+ person_image = self.resize_and_crop(person_image.convert("RGB"), (DEFAULT_WIDTH, DEFAULT_HEIGHT))
212
+ garment_image = self.resize_and_padding(garment_image.convert("RGB"), (DEFAULT_WIDTH, DEFAULT_HEIGHT))
213
+
214
+ generated_mask = self.automasker(person_image, cloth_type)["mask"]
215
+ generated_mask = self.mask_processor.blur(generated_mask, blur_factor=9)
216
+
217
+ generator = None
218
+ if seed != -1:
219
+ generator = torch.Generator(device=self.device).manual_seed(seed)
220
+
221
+ result_image = self.pipeline(
222
+ image=person_image,
223
+ condition_image=garment_image,
224
+ mask=generated_mask,
225
+ num_inference_steps=num_inference_steps,
226
+ guidance_scale=guidance_scale,
227
+ generator=generator,
228
+ )[0]
229
+
230
+ if show_type == "result only":
231
+ return result_image.convert("RGB")
232
+
233
+ masked_person = self.vis_mask(person_image, generated_mask)
234
+ return compose_preview(person_image, garment_image, masked_person, result_image, show_type)
235
+
236
+
237
+ runtime = CatVTONRuntime(repo_dir=resolve_catvton_repo_dir(CATVTON_REPO_DIR), device=DEVICE)
238
+
239
+
240
+
241
+ def prepare_image(image: Image.Image) -> Image.Image:
242
+ return ImageOps.exif_transpose(image).convert("RGB")
243
+
244
+
245
+ def image_grid(images: list[Image.Image], rows: int, cols: int) -> Image.Image:
246
+ if len(images) != rows * cols:
247
+ raise ValueError("The number of images does not match the grid shape.")
248
+ width, height = images[0].size
249
+ grid = Image.new("RGB", size=(cols * width, rows * height))
250
+ for index, image in enumerate(images):
251
+ grid.paste(image, box=(index % cols * width, index // cols * height))
252
+ return grid
253
+
254
+
255
+ def compose_preview(
256
+ person_image: Image.Image,
257
+ garment_image: Image.Image,
258
+ masked_person: Image.Image,
259
+ result_image: Image.Image,
260
+ show_type: str,
261
+ ) -> Image.Image:
262
+ width, height = person_image.size
263
+ if show_type == "input & result":
264
+ side_panel = image_grid([person_image, garment_image], 2, 1).resize((width // 2, height), Image.NEAREST)
265
+ else:
266
+ side_panel = image_grid([person_image, masked_person, garment_image], 3, 1).resize((width // 3, height), Image.NEAREST)
267
+
268
+ preview = Image.new("RGB", (side_panel.width + 5 + width, height), color=(255, 255, 255))
269
+ preview.paste(side_panel, (0, 0))
270
+ preview.paste(result_image.convert("RGB"), (side_panel.width + 5, 0))
271
+ return preview
272
+
273
+
274
+ def try_on(
275
+ person_image: Optional[Image.Image],
276
+ garment_image: Optional[Image.Image],
277
+ cloth_type: str,
278
+ num_inference_steps: int,
279
+ guidance_scale: float,
280
+ seed: int,
281
+ show_type: str,
282
+ ) -> Image.Image:
283
+ if person_image is None or garment_image is None:
284
+ raise gr.Error("Please upload both a shopper photo and a clothing image.")
285
+
286
+ prepared_person = prepare_image(person_image)
287
+ prepared_garment = prepare_image(garment_image)
288
+
289
+ try:
290
+ return runtime.run(
291
+ person_image=prepared_person,
292
+ garment_image=prepared_garment,
293
+ cloth_type=cloth_type,
294
+ num_inference_steps=num_inference_steps,
295
+ guidance_scale=guidance_scale,
296
+ seed=seed,
297
+ show_type=show_type,
298
+ )
299
+ except Exception as exc:
300
+ raise gr.Error(f"CatVTON inference failed: {exc}") from exc
301
+
302
+
303
+ with gr.Blocks(theme=gr.themes.Soft(), title=APP_TITLE) as demo:
304
+ gr.Markdown(f"# {APP_TITLE}")
305
+ gr.Markdown(APP_DESCRIPTION)
306
+ gr.Markdown(
307
+ f"**Runtime:** repo=`{CATVTON_REPO_DIR}` | weights=`{CATVTON_RESUME_PATH}` | device=`{DEVICE}`"
308
+ )
309
+
310
+ with gr.Row():
311
+ with gr.Column(scale=1):
312
+ person_input = gr.Image(type="pil", label="Shopper photo")
313
+ garment_input = gr.Image(type="pil", label="Clothing image")
314
+ cloth_type_input = gr.Radio(
315
+ label="Garment type",
316
+ choices=["upper", "lower", "overall"],
317
+ value="upper",
318
+ )
319
+ submit_button = gr.Button("Try On", variant="primary")
320
+ with gr.Accordion("Advanced options", open=False):
321
+ step_input = gr.Slider(label="Inference steps", minimum=10, maximum=100, step=5, value=DEFAULT_STEPS)
322
+ guidance_input = gr.Slider(label="Guidance scale", minimum=0.0, maximum=7.5, step=0.5, value=DEFAULT_GUIDANCE_SCALE)
323
+ seed_input = gr.Slider(label="Seed", minimum=-1, maximum=10000, step=1, value=DEFAULT_SEED)
324
+ show_type_input = gr.Radio(
325
+ label="Preview mode",
326
+ choices=["result only", "input & result", "input & mask & result"],
327
+ value="result only",
328
+ )
329
+ with gr.Column(scale=1):
330
+ result_output = gr.Image(type="pil", label="Try-on result")
331
+
332
+ gr.Markdown(
333
+ """
334
+ ### Notes
335
+ - This app is just for testing `CatVTON/` codebase.
336
+ - Model weights are downloaded on demand from Hugging Face using `zhengchong/CatVTON` by default.
337
+ - Just for testing purposes only.
338
+ """
339
+ )
340
+
341
+ submit_button.click(
342
+ fn=try_on,
343
+ inputs=[person_input, garment_input, cloth_type_input, step_input, guidance_input, seed_input, show_type_input],
344
+ outputs=result_output,
345
+ )
346
+
347
+
348
+ demo.queue().launch(show_error=True)
.history/app_20260617195520.py ADDED
@@ -0,0 +1,348 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import gradio as gr
10
+ import numpy as np
11
+ import torch
12
+ from diffusers.image_processor import VaeImageProcessor
13
+ from huggingface_hub import snapshot_download
14
+ from PIL import Image, ImageOps
15
+
16
+ APP_TITLE = "ChitraTech Virtual Try-On"
17
+ APP_DESCRIPTION = (
18
+ "Upload a shopper photo and clothing image to run on-demand CatVTON virtual try-on inference "
19
+ "using the Zheng-Chong CatVTON implementation."
20
+ )
21
+ CATVTON_REPO_DIR_ENV = os.getenv("CATVTON_REPO_DIR")
22
+ CATVTON_REPO_DIR = Path(CATVTON_REPO_DIR_ENV) if CATVTON_REPO_DIR_ENV else Path("./CatVTON")
23
+ CATVTON_RESUME_PATH = os.getenv("CATVTON_RESUME_PATH", "zhengchong/CatVTON")
24
+
25
+
26
+ def resolve_catvton_repo_dir(start_dir: Path) -> Path:
27
+ """Find the CatVTON repo root that contains `model/cloth_masker.py`.
28
+
29
+ HF Spaces sometimes mount code in unexpected places; relying on fixed paths like
30
+ `/app/CatVTON` can be wrong. We therefore:
31
+ 1) try a few common candidates
32
+ 2) then scan under `/app` (and `/workspace` if present) for `model/cloth_masker.py`
33
+ """
34
+
35
+ def looks_like_repo_dir(p: Path) -> bool:
36
+ return (p / "model" / "cloth_masker.py").exists() and (p / "model" / "pipeline.py").exists()
37
+
38
+ candidates: list[Path] = []
39
+
40
+ if start_dir is not None:
41
+ candidates.append(start_dir)
42
+
43
+ if CATVTON_REPO_DIR_ENV:
44
+ candidates.append(Path(CATVTON_REPO_DIR_ENV))
45
+
46
+ candidates.extend([
47
+ Path("/app/CatVTON"),
48
+ Path("/app"),
49
+ Path("./CatVTON"),
50
+ Path("./"),
51
+ Path("/workspace"),
52
+ ])
53
+
54
+ for c in candidates:
55
+ if c is not None and looks_like_repo_dir(c):
56
+ return c.resolve()
57
+
58
+ # Broad scan for the actual code root.
59
+ scan_roots = [Path("/app"), Path("/workspace")]
60
+ for root in scan_roots:
61
+ if not root.exists():
62
+ continue
63
+ for cloth_masker in root.rglob("model/cloth_masker.py"):
64
+ repo_root = cloth_masker.parent.parent # .../<repo_root>/model/cloth_masker.py
65
+ if looks_like_repo_dir(repo_root):
66
+ return repo_root.resolve()
67
+
68
+ # Fallback: return the provided start_dir (so error message includes candidates).
69
+ return start_dir.resolve()
70
+
71
+
72
+ CATVTON_BASE_MODEL = os.getenv("CATVTON_BASE_MODEL", "booksforcharlie/stable-diffusion-inpainting")
73
+ CATVTON_OUTPUT_DIR = Path(os.getenv("CATVTON_OUTPUT_DIR", "./outputs"))
74
+ DEVICE = os.getenv("CATVTON_DEVICE", "cuda")
75
+ DEFAULT_WIDTH = int(os.getenv("CATVTON_WIDTH", "768"))
76
+ DEFAULT_HEIGHT = int(os.getenv("CATVTON_HEIGHT", "1024"))
77
+ DEFAULT_STEPS = int(os.getenv("CATVTON_STEPS", "50"))
78
+ DEFAULT_GUIDANCE_SCALE = float(os.getenv("CATVTON_GUIDANCE_SCALE", "2.5"))
79
+ DEFAULT_MIXED_PRECISION = os.getenv("CATVTON_MIXED_PRECISION", "bf16")
80
+ DEFAULT_SEED = int(os.getenv("CATVTON_SEED", "42"))
81
+
82
+
83
+ @dataclass
84
+ class CatVTONRuntime:
85
+ repo_dir: Path
86
+ device: str
87
+ pipeline: object | None = field(default=None, init=False, repr=False)
88
+ automasker: object | None = field(default=None, init=False, repr=False)
89
+ mask_processor: object | None = field(default=None, init=False, repr=False)
90
+ resize_and_crop: object | None = field(default=None, init=False, repr=False)
91
+ resize_and_padding: object | None = field(default=None, init=False, repr=False)
92
+ vis_mask: object | None = field(default=None, init=False, repr=False)
93
+ ready: bool = False
94
+ status: str = "not loaded"
95
+
96
+ def load(self) -> None:
97
+ if self.ready:
98
+ return
99
+
100
+ if not self.repo_dir.exists():
101
+ raise RuntimeError(f"CatVTON repository not found at '{self.repo_dir}'.")
102
+
103
+ # --- Resolve real python root that contains `model/` ---
104
+ # Some HF environments mount the code differently; env/debug values can be wrong.
105
+ # We therefore detect the repo root by searching for `model/cloth_masker.py`.
106
+
107
+ scan_roots = [Path("/app"), Path("/workspace"), Path.cwd()]
108
+ found_model_parent: Path | None = None
109
+
110
+ for scan_root in scan_roots:
111
+ if not scan_root.exists():
112
+ continue
113
+ # bounded scan to avoid huge FS traversal
114
+ for cloth_masker in scan_root.rglob("model/cloth_masker.py"):
115
+ repo_root = cloth_masker.parent.parent
116
+ pipeline_file = repo_root / "model" / "pipeline.py"
117
+ if pipeline_file.exists():
118
+ found_model_parent = repo_root.resolve()
119
+ break
120
+ if found_model_parent is not None:
121
+ break
122
+
123
+ if found_model_parent is None:
124
+ # Keep existing behavior as last resort.
125
+ found_model_parent = self.repo_dir.resolve()
126
+
127
+ repo_path = str(found_model_parent)
128
+ if repo_path not in sys.path:
129
+ sys.path.insert(0, repo_path)
130
+
131
+ model_dir = (found_model_parent / "model").resolve()
132
+ if model_dir.exists():
133
+ model_parent_str = str(model_dir.parent)
134
+ if model_parent_str not in sys.path:
135
+ sys.path.insert(0, model_parent_str)
136
+
137
+ self.repo_dir = found_model_parent
138
+
139
+
140
+
141
+
142
+ # If this still fails inside HF, add debugging info.
143
+ try:
144
+ from model.cloth_masker import AutoMasker, vis_mask
145
+ from model.pipeline import CatVTONPipeline
146
+ except Exception as import_exc:
147
+ # Helpful diagnostics for HF Spaces.
148
+ repo_model_exists = (self.repo_dir / "model").exists()
149
+ candidate_roots = [
150
+ self.repo_dir,
151
+ self.repo_dir / "model",
152
+ (self.repo_dir / "model").parent,
153
+ ]
154
+ candidate_roots_str = ", ".join(str(p) for p in candidate_roots)
155
+
156
+ raise RuntimeError(
157
+ "CatVTON import failed. "
158
+ f"repo_dir={self.repo_dir} "
159
+ f"repo_dir/model_exists={repo_model_exists} "
160
+ f"repo_model_candidate_roots={candidate_roots_str} "
161
+ f"sys.path[0:10]={sys.path[:10]} "
162
+ f"import_error={import_exc}"
163
+ ) from import_exc
164
+
165
+
166
+ repo_weights_dir = Path(snapshot_download(repo_id=CATVTON_RESUME_PATH))
167
+ self.pipeline = CatVTONPipeline(
168
+ base_ckpt=CATVTON_BASE_MODEL,
169
+ attn_ckpt=str(repo_weights_dir),
170
+ attn_ckpt_version="mix",
171
+ weight_dtype=init_weight_dtype(DEFAULT_MIXED_PRECISION),
172
+ use_tf32=True,
173
+ device=self.device,
174
+ )
175
+ self.mask_processor = VaeImageProcessor(
176
+ vae_scale_factor=8,
177
+ do_normalize=False,
178
+ do_binarize=True,
179
+ do_convert_grayscale=True,
180
+ )
181
+ self.automasker = AutoMasker(
182
+ densepose_ckpt=os.path.join(repo_weights_dir, "DensePose"),
183
+ schp_ckpt=os.path.join(repo_weights_dir, "SCHP"),
184
+ device=self.device,
185
+ )
186
+ self.resize_and_crop = resize_and_crop
187
+ self.resize_and_padding = resize_and_padding
188
+ self.vis_mask = vis_mask
189
+ CATVTON_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
190
+ self.ready = True
191
+ self.status = "loaded"
192
+
193
+ def run(
194
+ self,
195
+ person_image: Image.Image,
196
+ garment_image: Image.Image,
197
+ cloth_type: str,
198
+ num_inference_steps: int,
199
+ guidance_scale: float,
200
+ seed: int,
201
+ show_type: str,
202
+ ) -> Image.Image:
203
+ self.load()
204
+ assert self.pipeline is not None
205
+ assert self.automasker is not None
206
+ assert self.mask_processor is not None
207
+ assert self.resize_and_crop is not None
208
+ assert self.resize_and_padding is not None
209
+ assert self.vis_mask is not None
210
+
211
+ person_image = self.resize_and_crop(person_image.convert("RGB"), (DEFAULT_WIDTH, DEFAULT_HEIGHT))
212
+ garment_image = self.resize_and_padding(garment_image.convert("RGB"), (DEFAULT_WIDTH, DEFAULT_HEIGHT))
213
+
214
+ generated_mask = self.automasker(person_image, cloth_type)["mask"]
215
+ generated_mask = self.mask_processor.blur(generated_mask, blur_factor=9)
216
+
217
+ generator = None
218
+ if seed != -1:
219
+ generator = torch.Generator(device=self.device).manual_seed(seed)
220
+
221
+ result_image = self.pipeline(
222
+ image=person_image,
223
+ condition_image=garment_image,
224
+ mask=generated_mask,
225
+ num_inference_steps=num_inference_steps,
226
+ guidance_scale=guidance_scale,
227
+ generator=generator,
228
+ )[0]
229
+
230
+ if show_type == "result only":
231
+ return result_image.convert("RGB")
232
+
233
+ masked_person = self.vis_mask(person_image, generated_mask)
234
+ return compose_preview(person_image, garment_image, masked_person, result_image, show_type)
235
+
236
+
237
+ runtime = CatVTONRuntime(repo_dir=resolve_catvton_repo_dir(CATVTON_REPO_DIR), device=DEVICE)
238
+
239
+
240
+
241
+ def prepare_image(image: Image.Image) -> Image.Image:
242
+ return ImageOps.exif_transpose(image).convert("RGB")
243
+
244
+
245
+ def image_grid(images: list[Image.Image], rows: int, cols: int) -> Image.Image:
246
+ if len(images) != rows * cols:
247
+ raise ValueError("The number of images does not match the grid shape.")
248
+ width, height = images[0].size
249
+ grid = Image.new("RGB", size=(cols * width, rows * height))
250
+ for index, image in enumerate(images):
251
+ grid.paste(image, box=(index % cols * width, index // cols * height))
252
+ return grid
253
+
254
+
255
+ def compose_preview(
256
+ person_image: Image.Image,
257
+ garment_image: Image.Image,
258
+ masked_person: Image.Image,
259
+ result_image: Image.Image,
260
+ show_type: str,
261
+ ) -> Image.Image:
262
+ width, height = person_image.size
263
+ if show_type == "input & result":
264
+ side_panel = image_grid([person_image, garment_image], 2, 1).resize((width // 2, height), Image.NEAREST)
265
+ else:
266
+ side_panel = image_grid([person_image, masked_person, garment_image], 3, 1).resize((width // 3, height), Image.NEAREST)
267
+
268
+ preview = Image.new("RGB", (side_panel.width + 5 + width, height), color=(255, 255, 255))
269
+ preview.paste(side_panel, (0, 0))
270
+ preview.paste(result_image.convert("RGB"), (side_panel.width + 5, 0))
271
+ return preview
272
+
273
+
274
+ def try_on(
275
+ person_image: Optional[Image.Image],
276
+ garment_image: Optional[Image.Image],
277
+ cloth_type: str,
278
+ num_inference_steps: int,
279
+ guidance_scale: float,
280
+ seed: int,
281
+ show_type: str,
282
+ ) -> Image.Image:
283
+ if person_image is None or garment_image is None:
284
+ raise gr.Error("Please upload both a shopper photo and a clothing image.")
285
+
286
+ prepared_person = prepare_image(person_image)
287
+ prepared_garment = prepare_image(garment_image)
288
+
289
+ try:
290
+ return runtime.run(
291
+ person_image=prepared_person,
292
+ garment_image=prepared_garment,
293
+ cloth_type=cloth_type,
294
+ num_inference_steps=num_inference_steps,
295
+ guidance_scale=guidance_scale,
296
+ seed=seed,
297
+ show_type=show_type,
298
+ )
299
+ except Exception as exc:
300
+ raise gr.Error(f"CatVTON inference failed: {exc}") from exc
301
+
302
+
303
+ with gr.Blocks(theme=gr.themes.Soft(), title=APP_TITLE) as demo:
304
+ gr.Markdown(f"# {APP_TITLE}")
305
+ gr.Markdown(APP_DESCRIPTION)
306
+ gr.Markdown(
307
+ f"**Runtime:** repo=`{CATVTON_REPO_DIR}` | weights=`{CATVTON_RESUME_PATH}` | device=`{DEVICE}`"
308
+ )
309
+
310
+ with gr.Row():
311
+ with gr.Column(scale=1):
312
+ person_input = gr.Image(type="pil", label="Shopper photo")
313
+ garment_input = gr.Image(type="pil", label="Clothing image")
314
+ cloth_type_input = gr.Radio(
315
+ label="Garment type",
316
+ choices=["upper", "lower", "overall"],
317
+ value="upper",
318
+ )
319
+ submit_button = gr.Button("Try On", variant="primary")
320
+ with gr.Accordion("Advanced options", open=False):
321
+ step_input = gr.Slider(label="Inference steps", minimum=10, maximum=100, step=5, value=DEFAULT_STEPS)
322
+ guidance_input = gr.Slider(label="Guidance scale", minimum=0.0, maximum=7.5, step=0.5, value=DEFAULT_GUIDANCE_SCALE)
323
+ seed_input = gr.Slider(label="Seed", minimum=-1, maximum=10000, step=1, value=DEFAULT_SEED)
324
+ show_type_input = gr.Radio(
325
+ label="Preview mode",
326
+ choices=["result only", "input & result", "input & mask & result"],
327
+ value="result only",
328
+ )
329
+ with gr.Column(scale=1):
330
+ result_output = gr.Image(type="pil", label="Try-on result")
331
+
332
+ gr.Markdown(
333
+ """
334
+ ### Notes
335
+ - This app is just for testing `CatVTON/` codebase.
336
+ - Model weights are downloaded on demand from Hugging Face using `zhengchong/CatVTON` by default.
337
+ - Just for testing purposes only.
338
+ """
339
+ )
340
+
341
+ submit_button.click(
342
+ fn=try_on,
343
+ inputs=[person_input, garment_input, cloth_type_input, step_input, guidance_input, seed_input, show_type_input],
344
+ outputs=result_output,
345
+ )
346
+
347
+
348
+ demo.queue().launch(show_error=True)
app.py CHANGED
@@ -101,53 +101,41 @@ class CatVTONRuntime:
101
  raise RuntimeError(f"CatVTON repository not found at '{self.repo_dir}'.")
102
 
103
  # --- Resolve real python root that contains `model/` ---
104
- # HF layout has been inconsistent across runs (env var can point to a directory
105
- # that doesn't actually contain `model/`). We therefore try a deterministic set
106
- # of common mounts first.
107
- candidate_roots: list[Path] = []
108
-
109
- if self.repo_dir is not None:
110
- candidate_roots.append(Path(self.repo_dir))
111
-
112
- # Common containers paths
113
- candidate_roots.extend([
114
- Path("/app"),
115
- Path("/app/CatVTON"),
116
- Path("/app/CatVTON"),
117
- Path("/workspace"),
118
- ])
119
-
120
- # Also try one level below /app (in case repo sits at /app/<something>/)
121
- for p in [Path("/app")]:
122
- if p.exists():
123
- for child in p.iterdir():
124
- if child.is_dir():
125
- candidate_roots.append(child)
126
-
127
- model_parent: Path | None = None
128
- for r in candidate_roots:
129
- md = (r / "model").resolve()
130
- if md.exists() and (md / "cloth_masker.py").exists() and (md / "pipeline.py").exists():
131
- model_parent = r.resolve()
132
  break
133
 
134
- if model_parent is None:
135
- # Keep original repo_dir as last resort.
136
- model_parent = Path(self.repo_dir).resolve()
137
 
138
- repo_path = str(model_parent)
139
  if repo_path not in sys.path:
140
  sys.path.insert(0, repo_path)
141
 
142
- # Ensure `model/` is importable; add parent of model dir as top-level module location.
143
- model_dir = (model_parent / "model").resolve()
144
  if model_dir.exists():
145
  model_parent_str = str(model_dir.parent)
146
  if model_parent_str not in sys.path:
147
  sys.path.insert(0, model_parent_str)
148
 
149
- # Update internal repo_dir to what actually contains model (for correct diagnostics)
150
- self.repo_dir = model_parent
151
 
152
 
153
 
 
101
  raise RuntimeError(f"CatVTON repository not found at '{self.repo_dir}'.")
102
 
103
  # --- Resolve real python root that contains `model/` ---
104
+ # Some HF environments mount the code differently; env/debug values can be wrong.
105
+ # We therefore detect the repo root by searching for `model/cloth_masker.py`.
106
+
107
+ scan_roots = [Path("/app"), Path("/workspace"), Path.cwd()]
108
+ found_model_parent: Path | None = None
109
+
110
+ for scan_root in scan_roots:
111
+ if not scan_root.exists():
112
+ continue
113
+ # bounded scan to avoid huge FS traversal
114
+ for cloth_masker in scan_root.rglob("model/cloth_masker.py"):
115
+ repo_root = cloth_masker.parent.parent
116
+ pipeline_file = repo_root / "model" / "pipeline.py"
117
+ if pipeline_file.exists():
118
+ found_model_parent = repo_root.resolve()
119
+ break
120
+ if found_model_parent is not None:
 
 
 
 
 
 
 
 
 
 
 
121
  break
122
 
123
+ if found_model_parent is None:
124
+ # Keep existing behavior as last resort.
125
+ found_model_parent = self.repo_dir.resolve()
126
 
127
+ repo_path = str(found_model_parent)
128
  if repo_path not in sys.path:
129
  sys.path.insert(0, repo_path)
130
 
131
+ model_dir = (found_model_parent / "model").resolve()
 
132
  if model_dir.exists():
133
  model_parent_str = str(model_dir.parent)
134
  if model_parent_str not in sys.path:
135
  sys.path.insert(0, model_parent_str)
136
 
137
+ self.repo_dir = found_model_parent
138
+
139
 
140
 
141