Dell commited on
Commit
31bde8c
·
1 Parent(s): 62569eb
Files changed (2) hide show
  1. .history/app_20260617193632.py +332 -0
  2. app.py +24 -13
.history/app_20260617193632.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ repo_path = str(self.repo_dir.resolve())
104
+ if repo_path not in sys.path:
105
+ sys.path.insert(0, repo_path)
106
+
107
+ # Ensure CatVTON's `model/` package is importable as `import model.*`.
108
+ # In many deployments the code is flattened under `/app`, so `CATVTON_REPO_DIR`
109
+ # might be relative and/or point to the wrong nesting level.
110
+ model_dir = (self.repo_dir / "model").resolve()
111
+ if model_dir.exists():
112
+ # Add the parent so `model` becomes a top-level module
113
+ model_parent = str(model_dir.parent)
114
+ if model_parent not in sys.path:
115
+ sys.path.insert(0, model_parent)
116
+
117
+ # Extra fallback: if repo_dir/model doesn't exist, try sibling 'model' under repo_path
118
+ if not (self.repo_dir / "model").exists():
119
+ guessed = (Path(repo_path) / "model").resolve()
120
+ if guessed.exists():
121
+ guessed_parent = str(guessed.parent)
122
+ if guessed_parent not in sys.path:
123
+ sys.path.insert(0, guessed_parent)
124
+
125
+
126
+ # If this still fails inside HF, add debugging info.
127
+ try:
128
+ from model.cloth_masker import AutoMasker, vis_mask
129
+ from model.pipeline import CatVTONPipeline
130
+ except Exception as import_exc:
131
+ # Helpful diagnostics for HF Spaces.
132
+ repo_model_exists = (self.repo_dir / "model").exists()
133
+ candidate_roots = [
134
+ self.repo_dir,
135
+ self.repo_dir / "model",
136
+ (self.repo_dir / "model").parent,
137
+ ]
138
+ candidate_roots_str = ", ".join(str(p) for p in candidate_roots)
139
+
140
+ raise RuntimeError(
141
+ "CatVTON import failed. "
142
+ f"repo_dir={self.repo_dir} "
143
+ f"repo_dir/model_exists={repo_model_exists} "
144
+ f"repo_model_candidate_roots={candidate_roots_str} "
145
+ f"sys.path[0:10]={sys.path[:10]} "
146
+ f"import_error={import_exc}"
147
+ ) from import_exc
148
+
149
+
150
+ repo_weights_dir = Path(snapshot_download(repo_id=CATVTON_RESUME_PATH))
151
+ self.pipeline = CatVTONPipeline(
152
+ base_ckpt=CATVTON_BASE_MODEL,
153
+ attn_ckpt=str(repo_weights_dir),
154
+ attn_ckpt_version="mix",
155
+ weight_dtype=init_weight_dtype(DEFAULT_MIXED_PRECISION),
156
+ use_tf32=True,
157
+ device=self.device,
158
+ )
159
+ self.mask_processor = VaeImageProcessor(
160
+ vae_scale_factor=8,
161
+ do_normalize=False,
162
+ do_binarize=True,
163
+ do_convert_grayscale=True,
164
+ )
165
+ self.automasker = AutoMasker(
166
+ densepose_ckpt=os.path.join(repo_weights_dir, "DensePose"),
167
+ schp_ckpt=os.path.join(repo_weights_dir, "SCHP"),
168
+ device=self.device,
169
+ )
170
+ self.resize_and_crop = resize_and_crop
171
+ self.resize_and_padding = resize_and_padding
172
+ self.vis_mask = vis_mask
173
+ CATVTON_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
174
+ self.ready = True
175
+ self.status = "loaded"
176
+
177
+ def run(
178
+ self,
179
+ person_image: Image.Image,
180
+ garment_image: Image.Image,
181
+ cloth_type: str,
182
+ num_inference_steps: int,
183
+ guidance_scale: float,
184
+ seed: int,
185
+ show_type: str,
186
+ ) -> Image.Image:
187
+ self.load()
188
+ assert self.pipeline is not None
189
+ assert self.automasker is not None
190
+ assert self.mask_processor is not None
191
+ assert self.resize_and_crop is not None
192
+ assert self.resize_and_padding is not None
193
+ assert self.vis_mask is not None
194
+
195
+ person_image = self.resize_and_crop(person_image.convert("RGB"), (DEFAULT_WIDTH, DEFAULT_HEIGHT))
196
+ garment_image = self.resize_and_padding(garment_image.convert("RGB"), (DEFAULT_WIDTH, DEFAULT_HEIGHT))
197
+
198
+ generated_mask = self.automasker(person_image, cloth_type)["mask"]
199
+ generated_mask = self.mask_processor.blur(generated_mask, blur_factor=9)
200
+
201
+ generator = None
202
+ if seed != -1:
203
+ generator = torch.Generator(device=self.device).manual_seed(seed)
204
+
205
+ result_image = self.pipeline(
206
+ image=person_image,
207
+ condition_image=garment_image,
208
+ mask=generated_mask,
209
+ num_inference_steps=num_inference_steps,
210
+ guidance_scale=guidance_scale,
211
+ generator=generator,
212
+ )[0]
213
+
214
+ if show_type == "result only":
215
+ return result_image.convert("RGB")
216
+
217
+ masked_person = self.vis_mask(person_image, generated_mask)
218
+ return compose_preview(person_image, garment_image, masked_person, result_image, show_type)
219
+
220
+
221
+ runtime = CatVTONRuntime(repo_dir=resolve_catvton_repo_dir(CATVTON_REPO_DIR), device=DEVICE)
222
+
223
+
224
+
225
+ def prepare_image(image: Image.Image) -> Image.Image:
226
+ return ImageOps.exif_transpose(image).convert("RGB")
227
+
228
+
229
+ def image_grid(images: list[Image.Image], rows: int, cols: int) -> Image.Image:
230
+ if len(images) != rows * cols:
231
+ raise ValueError("The number of images does not match the grid shape.")
232
+ width, height = images[0].size
233
+ grid = Image.new("RGB", size=(cols * width, rows * height))
234
+ for index, image in enumerate(images):
235
+ grid.paste(image, box=(index % cols * width, index // cols * height))
236
+ return grid
237
+
238
+
239
+ def compose_preview(
240
+ person_image: Image.Image,
241
+ garment_image: Image.Image,
242
+ masked_person: Image.Image,
243
+ result_image: Image.Image,
244
+ show_type: str,
245
+ ) -> Image.Image:
246
+ width, height = person_image.size
247
+ if show_type == "input & result":
248
+ side_panel = image_grid([person_image, garment_image], 2, 1).resize((width // 2, height), Image.NEAREST)
249
+ else:
250
+ side_panel = image_grid([person_image, masked_person, garment_image], 3, 1).resize((width // 3, height), Image.NEAREST)
251
+
252
+ preview = Image.new("RGB", (side_panel.width + 5 + width, height), color=(255, 255, 255))
253
+ preview.paste(side_panel, (0, 0))
254
+ preview.paste(result_image.convert("RGB"), (side_panel.width + 5, 0))
255
+ return preview
256
+
257
+
258
+ def try_on(
259
+ person_image: Optional[Image.Image],
260
+ garment_image: Optional[Image.Image],
261
+ cloth_type: str,
262
+ num_inference_steps: int,
263
+ guidance_scale: float,
264
+ seed: int,
265
+ show_type: str,
266
+ ) -> Image.Image:
267
+ if person_image is None or garment_image is None:
268
+ raise gr.Error("Please upload both a shopper photo and a clothing image.")
269
+
270
+ prepared_person = prepare_image(person_image)
271
+ prepared_garment = prepare_image(garment_image)
272
+
273
+ try:
274
+ return runtime.run(
275
+ person_image=prepared_person,
276
+ garment_image=prepared_garment,
277
+ cloth_type=cloth_type,
278
+ num_inference_steps=num_inference_steps,
279
+ guidance_scale=guidance_scale,
280
+ seed=seed,
281
+ show_type=show_type,
282
+ )
283
+ except Exception as exc:
284
+ raise gr.Error(f"CatVTON inference failed: {exc}") from exc
285
+
286
+
287
+ with gr.Blocks(theme=gr.themes.Soft(), title=APP_TITLE) as demo:
288
+ gr.Markdown(f"# {APP_TITLE}")
289
+ gr.Markdown(APP_DESCRIPTION)
290
+ gr.Markdown(
291
+ f"**Runtime:** repo=`{CATVTON_REPO_DIR}` | weights=`{CATVTON_RESUME_PATH}` | device=`{DEVICE}`"
292
+ )
293
+
294
+ with gr.Row():
295
+ with gr.Column(scale=1):
296
+ person_input = gr.Image(type="pil", label="Shopper photo")
297
+ garment_input = gr.Image(type="pil", label="Clothing image")
298
+ cloth_type_input = gr.Radio(
299
+ label="Garment type",
300
+ choices=["upper", "lower", "overall"],
301
+ value="upper",
302
+ )
303
+ submit_button = gr.Button("Try On", variant="primary")
304
+ with gr.Accordion("Advanced options", open=False):
305
+ step_input = gr.Slider(label="Inference steps", minimum=10, maximum=100, step=5, value=DEFAULT_STEPS)
306
+ guidance_input = gr.Slider(label="Guidance scale", minimum=0.0, maximum=7.5, step=0.5, value=DEFAULT_GUIDANCE_SCALE)
307
+ seed_input = gr.Slider(label="Seed", minimum=-1, maximum=10000, step=1, value=DEFAULT_SEED)
308
+ show_type_input = gr.Radio(
309
+ label="Preview mode",
310
+ choices=["result only", "input & result", "input & mask & result"],
311
+ value="result only",
312
+ )
313
+ with gr.Column(scale=1):
314
+ result_output = gr.Image(type="pil", label="Try-on result")
315
+
316
+ gr.Markdown(
317
+ """
318
+ ### Notes
319
+ - This app is just for testing `CatVTON/` codebase.
320
+ - Model weights are downloaded on demand from Hugging Face using `zhengchong/CatVTON` by default.
321
+ - Just for testing purposes only.
322
+ """
323
+ )
324
+
325
+ submit_button.click(
326
+ fn=try_on,
327
+ inputs=[person_input, garment_input, cloth_type_input, step_input, guidance_input, seed_input, show_type_input],
328
+ outputs=result_output,
329
+ )
330
+
331
+
332
+ demo.queue().launch(show_error=True)
app.py CHANGED
@@ -24,18 +24,25 @@ CATVTON_RESUME_PATH = os.getenv("CATVTON_RESUME_PATH", "zhengchong/CatVTON")
24
 
25
 
26
  def resolve_catvton_repo_dir(start_dir: Path) -> Path:
27
- """Try to find the CatVTON repo root that contains `model/`.
28
 
29
- HF spaces often mount code under `/app`, and env vars can be relative,
30
- so we probe a few likely locations.
 
 
31
  """
32
 
 
 
 
33
  candidates: list[Path] = []
34
 
35
  if start_dir is not None:
36
  candidates.append(start_dir)
37
 
38
- # Common HF layout
 
 
39
  candidates.extend([
40
  Path("/app/CatVTON"),
41
  Path("/app"),
@@ -44,20 +51,24 @@ def resolve_catvton_repo_dir(start_dir: Path) -> Path:
44
  Path("/workspace"),
45
  ])
46
 
47
- # Also allow using the env var directly if it was set
48
- if CATVTON_REPO_DIR_ENV:
49
- candidates.append(Path(CATVTON_REPO_DIR_ENV))
50
-
51
- # Probe for the *real* python root: folder containing `model/cloth_masker.py`
52
  for c in candidates:
53
- model_root = (c / "model" / "cloth_masker.py")
54
- pipeline_root = (c / "model" / "pipeline.py")
55
- if model_root.exists() and pipeline_root.exists():
56
  return c.resolve()
57
 
58
- # Fallback: return the start directory (and let the existing error message show details)
 
 
 
 
 
 
 
 
 
 
59
  return start_dir.resolve()
60
 
 
61
  CATVTON_BASE_MODEL = os.getenv("CATVTON_BASE_MODEL", "booksforcharlie/stable-diffusion-inpainting")
62
  CATVTON_OUTPUT_DIR = Path(os.getenv("CATVTON_OUTPUT_DIR", "./outputs"))
63
  DEVICE = os.getenv("CATVTON_DEVICE", "cuda")
 
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"),
 
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")