Davidscut commited on
Commit
5373877
·
1 Parent(s): 5e08555

Update compact try-on demo UI

Browse files
.gitattributes CHANGED
@@ -1,35 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
2
+ *.jpg filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .DS_Store
2
+ __pycache__/
3
+ *.py[cod]
app.py CHANGED
@@ -1,47 +1,180 @@
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
- prompt: str,
32
- prompt_enhancer: bool,
33
- negative_prompt: str,
34
- seed: int,
35
- randomize_seed: bool,
36
- num_inference_steps: int,
37
- guidance_scale: float,
38
- crop_and_paste: bool,
39
- ):
40
  try:
41
- garment_images = _validate_inputs(person_image, garment_files)
42
-
43
- if randomize_seed:
44
- seed = random.randint(0, 2**31 - 1)
45
 
46
  person = resize_for_demo(person_image, max_side=1024)
47
  garments = [resize_for_demo(img, max_side=1024) for img in garment_images]
@@ -49,18 +182,21 @@ def predict(
49
  result, used_index, enhanced_prompt, pe_status, status = run_tryon(
50
  person_image=person,
51
  garment_images=garments,
52
- garment_type=garment_type,
53
- primary_garment_index=int(primary_garment_index),
54
  seed=int(seed),
55
- num_inference_steps=int(num_inference_steps),
56
- guidance_scale=float(guidance_scale),
57
- auto_crop=bool(crop_and_paste),
58
  prompt=prompt,
59
- prompt_enhancer=bool(prompt_enhancer),
60
- negative_prompt=negative_prompt,
61
  )
62
 
63
- return result, int(seed), used_index, enhanced_prompt, pe_status, status
 
 
 
64
 
65
  except gr.Error:
66
  raise
@@ -69,111 +205,213 @@ def predict(
69
  raise gr.Error(f"Inference failed: {exc}") from exc
70
 
71
 
72
- with gr.Blocks(title="Virtual Try-On ZeroGPU Demo") as demo:
73
- gr.Markdown(
74
- """
75
- # Virtual Try-On Demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
- Upload one person image and one or more garment reference images.
78
- The model uses Picture 1 as the garment reference and Picture 2 as the person image.
79
- If you upload multiple garments, the selected primary garment index is used.
 
 
 
 
80
  """
81
  )
82
 
83
- with gr.Row():
84
- with gr.Column(scale=1):
85
- person_image = gr.Image(
86
- label="Person Image",
87
- type="pil",
88
- sources=["upload", "clipboard"],
89
- )
90
-
91
- garment_files = gr.File(
92
- label="Garment Reference Images",
93
- file_count="multiple",
94
- file_types=["image"],
95
- type="filepath",
96
- )
97
-
98
- garment_type = gr.Radio(
99
- choices=["upper_body", "lower_body", "dress", "full_body"],
100
- value="upper_body",
101
- label="Garment Type",
102
- )
103
-
104
- primary_garment_index = gr.Slider(
105
- minimum=0,
106
- maximum=9,
107
- value=0,
108
- step=1,
109
- label="Primary Garment Index",
110
- )
111
-
112
- prompt = gr.Textbox(
113
- label="Original Try-On Prompt",
114
- value="",
115
- lines=3,
116
- placeholder="Leave empty to use the default prompt for the selected garment type.",
117
- )
118
- prompt_enhancer = gr.Checkbox(value=True, label="Prompt Enhancer")
119
-
120
- with gr.Accordion("Advanced Settings", open=False):
121
- seed = gr.Number(value=42, precision=0, label="Seed")
122
- randomize_seed = gr.Checkbox(value=True, label="Randomize Seed")
123
- num_inference_steps = gr.Slider(
124
- minimum=10,
125
- maximum=80,
126
- value=30,
127
- step=1,
128
- label="Inference Steps",
129
- )
130
- guidance_scale = gr.Slider(
131
- minimum=0.0,
132
- maximum=15.0,
133
- value=6.0,
134
- step=0.1,
135
- label="Guidance Scale",
136
- )
137
- crop_and_paste = gr.Checkbox(
138
- value=False,
139
- label="Enable crop-and-paste when body bbox tools are available",
140
- )
141
- negative_prompt = gr.Textbox(
142
- value="",
143
- label="Negative Prompt",
144
- lines=3,
145
- placeholder="Leave empty to use the default negative prompt from tryon_infer.py.",
146
- )
147
-
148
- run_button = gr.Button("Generate Try-On", variant="primary")
149
-
150
- with gr.Column(scale=1):
151
- output_image = gr.Image(label="Try-On Result", type="pil")
152
  used_seed = gr.Number(label="Used Seed", precision=0)
153
  used_garment_index = gr.Number(label="Used Garment Index", precision=0)
154
- enhanced_prompt = gr.Textbox(label="Enhanced Prompt Used", interactive=False, lines=5)
155
- pe_status = gr.Textbox(label="Prompt Enhancer Status", interactive=False)
156
- status = gr.Textbox(label="Status", interactive=False)
157
-
158
- run_button.click(
159
- fn=predict,
160
- inputs=[
161
- person_image,
162
- garment_files,
163
- garment_type,
164
- primary_garment_index,
165
- prompt,
166
- prompt_enhancer,
167
- negative_prompt,
168
- seed,
169
- randomize_seed,
170
- num_inference_steps,
171
- guidance_scale,
172
- crop_and_paste,
173
- ],
174
- outputs=[output_image, used_seed, used_garment_index, enhanced_prompt, pe_status, status],
175
- api_name="tryon",
176
- )
177
 
178
 
179
  if __name__ == "__main__":
 
1
  from __future__ import annotations
2
 
3
+ import base64
4
  import random
5
+ import time
6
  import traceback
7
+ from pathlib import Path
8
 
9
  import gradio as gr
10
  import spaces
11
+ from PIL import Image, ImageDraw, ImageFilter, ImageFont
12
 
13
  from inference import run_tryon
14
+ from preprocess import resize_for_demo
15
 
16
 
17
+ APP_DIR = Path(__file__).resolve().parent
18
+ LOGO_PATH = APP_DIR / "assets" / "tryon_logo_compact.png"
19
+ SHOWCASE_DIR = APP_DIR / "assets" / "showcase"
20
+ SHOWCASE_IMAGES = [SHOWCASE_DIR / f"case_{idx}.jpg" for idx in range(4)]
21
 
22
+ def _image_data_uri(path: Path) -> str:
23
+ if not path.exists():
24
+ return ""
25
+ mime = "image/jpeg" if path.suffix.lower() in {".jpg", ".jpeg"} else "image/png"
26
+ data = base64.b64encode(path.read_bytes()).decode("utf-8")
27
+ return f"data:{mime};base64,{data}"
28
 
29
+
30
+ def _showcase_marquee_html() -> str:
31
+ cards = []
32
+ for idx, path in enumerate(SHOWCASE_IMAGES):
33
+ uri = _image_data_uri(path)
34
+ if uri:
35
+ cards.append(f'<img src="{uri}" alt="Try-on showcase {idx + 1}" />')
36
+ row = "".join(cards)
37
+ return f"""
38
+ <div class="showcase-marquee" aria-label="效果展示">
39
+ <div class="showcase-track">{row}{row}</div>
40
+ </div>
41
+ """
42
+
43
+
44
+ def _file_path(item) -> str | None:
45
+ if item is None:
46
+ return None
47
+ if isinstance(item, str):
48
+ return item
49
+ if isinstance(item, dict):
50
+ return item.get("path") or item.get("name")
51
+ return getattr(item, "path", None) or getattr(item, "name", None)
52
+
53
+
54
+ def _parse_user_box(message) -> tuple[Image.Image, list[Image.Image], str]:
55
+ if not message:
56
+ raise gr.Error("请在输入框里上传图片并写下试穿指令。")
57
+
58
+ text = ""
59
+ files = []
60
+ if isinstance(message, dict):
61
+ text = message.get("text") or ""
62
+ files = message.get("files") or []
63
+ else:
64
+ text = str(message)
65
+
66
+ paths = [path for path in (_file_path(item) for item in files) if path]
67
+ if len(paths) < 2:
68
+ raise gr.Error("请至少上传 2 张图:第 1 张人物图,后面的图作为服装参考图。")
69
+
70
+ person = Image.open(paths[0]).convert("RGB")
71
+ garments = [Image.open(path).convert("RGB") for path in paths[1:]]
72
+ return person, garments, text.strip()
73
+
74
+
75
+ def _font(size: int, bold: bool = False):
76
+ candidates = [
77
+ "/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc" if bold else "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
78
+ "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" if bold else "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
79
+ "/System/Library/Fonts/Supplemental/Arial Bold.ttf" if bold else "/System/Library/Fonts/Supplemental/Arial.ttf",
80
+ ]
81
+ for path in candidates:
82
+ try:
83
+ return ImageFont.truetype(path, size)
84
+ except OSError:
85
+ continue
86
+ return ImageFont.load_default()
87
+
88
+
89
+ def _cover(image: Image.Image, size: tuple[int, int]) -> Image.Image:
90
+ image = image.convert("RGB")
91
+ source_ratio = image.width / image.height
92
+ target_ratio = size[0] / size[1]
93
+ if source_ratio > target_ratio:
94
+ new_height = size[1]
95
+ new_width = int(new_height * source_ratio)
96
+ else:
97
+ new_width = size[0]
98
+ new_height = int(new_width / source_ratio)
99
+ resized = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
100
+ left = (new_width - size[0]) // 2
101
+ top = (new_height - size[1]) // 2
102
+ return resized.crop((left, top, left + size[0], top + size[1]))
103
+
104
+
105
+ def _rounded_image(image: Image.Image, radius: int) -> Image.Image:
106
+ mask = Image.new("L", image.size, 0)
107
+ draw = ImageDraw.Draw(mask)
108
+ draw.rounded_rectangle((0, 0, image.width, image.height), radius=radius, fill=255)
109
+ rounded = image.convert("RGBA")
110
+ rounded.putalpha(mask)
111
+ return rounded
112
+
113
+
114
+ def _draw_badge(draw: ImageDraw.ImageDraw, xy: tuple[int, int], text: str):
115
+ font = _font(18, bold=True)
116
+ left, top = xy
117
+ bbox = draw.textbbox((0, 0), text, font=font)
118
+ width = bbox[2] - bbox[0] + 28
119
+ height = bbox[3] - bbox[1] + 14
120
+ draw.rounded_rectangle((left, top, left + width, top + height), radius=18, fill=(17, 23, 46))
121
+ draw.text((left + 14, top + 6), text, font=font, fill=(255, 255, 255))
122
+
123
+
124
+ def _paste_panel(canvas: Image.Image, image: Image.Image, box: tuple[int, int, int, int], label: str):
125
+ left, top, right, bottom = box
126
+ panel = _cover(image, (right - left, bottom - top))
127
+ panel = _rounded_image(panel, radius=18)
128
+ canvas.alpha_composite(panel, (left, top))
129
+ draw = ImageDraw.Draw(canvas)
130
+ draw.rounded_rectangle((left, top, right, bottom), radius=18, outline=(228, 228, 228), width=2)
131
+ _draw_badge(draw, (left + 14, top + 12), label)
132
+
133
+
134
+ def _result_card(person: Image.Image, garment: Image.Image, result: Image.Image) -> Image.Image:
135
+ width, height = 1180, 680
136
+ card_left, card_top, card_right, card_bottom = 60, 58, width - 60, height - 58
137
+
138
+ canvas = Image.new("RGBA", (width, height), (255, 255, 255, 0))
139
+ shadow = Image.new("RGBA", (width, height), (255, 255, 255, 0))
140
+ shadow_draw = ImageDraw.Draw(shadow)
141
+ shadow_draw.rounded_rectangle(
142
+ (card_left, card_top + 18, card_right, card_bottom + 18),
143
+ radius=58,
144
+ fill=(0, 0, 0, 55),
145
+ )
146
+ shadow = shadow.filter(ImageFilter.GaussianBlur(22))
147
+ canvas.alpha_composite(shadow)
148
+
149
+ draw = ImageDraw.Draw(canvas)
150
+ draw.rounded_rectangle((card_left, card_top, card_right, card_bottom), radius=58, fill=(250, 250, 250, 255))
151
+
152
+ gap = 24
153
+ left_col = (card_left + 34, card_top + 34, card_left + 420, card_bottom - 34)
154
+ right_panel = (left_col[2] + gap, left_col[1], card_right - 34, left_col[3])
155
+ left_h = (left_col[3] - left_col[1] - gap) // 2
156
+ person_panel = (left_col[0], left_col[1], left_col[2], left_col[1] + left_h)
157
+ garment_panel = (left_col[0], person_panel[3] + gap, left_col[2], left_col[3])
158
+
159
+ _paste_panel(canvas, person, person_panel, "User Image")
160
+ _paste_panel(canvas, garment, garment_panel, "Clothing Image")
161
+ _paste_panel(canvas, result, right_panel, "Virtual Try-On Result")
162
+
163
+ draw = ImageDraw.Draw(canvas)
164
+ draw.ellipse((card_left + 32, card_bottom - 98, card_left + 132, card_bottom + 2), fill=(194, 194, 194, 220))
165
+ draw.text((card_left + 62, card_bottom - 63), "编辑", font=_font(28, bold=True), fill=(255, 255, 255))
166
+ draw.ellipse((card_right - 116, card_bottom - 98, card_right - 16, card_bottom + 2), fill=(178, 178, 178, 230))
167
+ draw.text((card_right - 82, card_bottom - 73), "↥", font=_font(42, bold=True), fill=(255, 255, 255))
168
+
169
+ return canvas.convert("RGB")
170
 
171
 
172
  @spaces.GPU(size="xlarge", duration=180)
173
+ def predict_from_box(message):
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  try:
175
+ started_at = time.time()
176
+ person_image, garment_images, prompt = _parse_user_box(message)
177
+ seed = random.randint(0, 2**31 - 1)
 
178
 
179
  person = resize_for_demo(person_image, max_side=1024)
180
  garments = [resize_for_demo(img, max_side=1024) for img in garment_images]
 
182
  result, used_index, enhanced_prompt, pe_status, status = run_tryon(
183
  person_image=person,
184
  garment_images=garments,
185
+ garment_type="full_body",
186
+ primary_garment_index=0,
187
  seed=int(seed),
188
+ num_inference_steps=30,
189
+ guidance_scale=6.0,
190
+ auto_crop=False,
191
  prompt=prompt,
192
+ prompt_enhancer=True,
193
+ negative_prompt="",
194
  )
195
 
196
+ result_card = _result_card(person, garments[used_index], result)
197
+ elapsed = max(1, int(time.time() - started_at))
198
+ display_status = f"Thought for {elapsed}s > {status}"
199
+ return result_card, int(seed), used_index, enhanced_prompt, pe_status, display_status
200
 
201
  except gr.Error:
202
  raise
 
205
  raise gr.Error(f"Inference failed: {exc}") from exc
206
 
207
 
208
+ CSS = """
209
+ :root {
210
+ --tryon-border: #dedede;
211
+ --tryon-muted: #6f6f6f;
212
+ }
213
+ body, .gradio-container {
214
+ background: #fff !important;
215
+ color: #111 !important;
216
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
217
+ }
218
+ .gradio-container {
219
+ max-width: none !important;
220
+ min-height: 100vh;
221
+ overflow-x: hidden;
222
+ }
223
+ .tryon-topbar {
224
+ position: fixed;
225
+ z-index: 20;
226
+ top: 24px;
227
+ left: 34px;
228
+ display: flex;
229
+ align-items: center;
230
+ }
231
+ .tryon-logo {
232
+ width: 124px;
233
+ height: 124px;
234
+ border-radius: 26px;
235
+ object-fit: cover;
236
+ box-shadow: 0 10px 30px rgba(0,0,0,.08);
237
+ }
238
+ .tryon-hero {
239
+ min-height: auto;
240
+ display: flex;
241
+ flex-direction: column;
242
+ justify-content: center;
243
+ align-items: center;
244
+ padding: 92px 20px 24px;
245
+ }
246
+ .tryon-title {
247
+ font-size: 32px;
248
+ line-height: 1.2;
249
+ font-weight: 760;
250
+ letter-spacing: 0;
251
+ text-align: center;
252
+ margin-bottom: 18px;
253
+ }
254
+ .prompt-shell {
255
+ max-width: 920px;
256
+ width: min(920px, calc(100vw - 36px));
257
+ margin: 0 auto;
258
+ }
259
+ .prompt-shell .wrap,
260
+ .prompt-shell .block {
261
+ border-radius: 32px !important;
262
+ border-color: #b9d2ff !important;
263
+ box-shadow: 0 16px 44px rgba(0,0,0,.07) !important;
264
+ }
265
+ .prompt-shell textarea,
266
+ .prompt-shell [contenteditable="true"] {
267
+ min-height: 138px !important;
268
+ border-radius: 32px !important;
269
+ border: 1px solid #b9d2ff !important;
270
+ box-shadow: 0 16px 44px rgba(0,0,0,.07) !important;
271
+ font-size: 22px !important;
272
+ line-height: 1.45 !important;
273
+ padding: 28px 88px 56px 28px !important;
274
+ }
275
+ .prompt-shell label {
276
+ display: none !important;
277
+ }
278
+ .prompt-shell button[title*="Upload"],
279
+ .prompt-shell button[aria-label*="Upload"],
280
+ .prompt-shell button[title*="上传"],
281
+ .prompt-shell button[aria-label*="上传"] {
282
+ position: absolute !important;
283
+ left: 26px !important;
284
+ bottom: 20px !important;
285
+ width: 42px !important;
286
+ height: 42px !important;
287
+ border-radius: 999px !important;
288
+ background: transparent !important;
289
+ border: 0 !important;
290
+ font-size: 28px !important;
291
+ }
292
+ .prompt-shell button[type="submit"],
293
+ .prompt-shell button[aria-label*="Submit"],
294
+ .prompt-shell button[title*="Submit"] {
295
+ position: absolute !important;
296
+ right: 24px !important;
297
+ bottom: 18px !important;
298
+ width: 48px !important;
299
+ height: 48px !important;
300
+ border-radius: 999px !important;
301
+ font-size: 22px !important;
302
+ background: #f3f3f3 !important;
303
+ color: #111 !important;
304
+ border: 0 !important;
305
+ }
306
+ .workspace {
307
+ max-width: 1180px;
308
+ margin: 0 auto;
309
+ padding: 8px 24px 18px;
310
+ }
311
+ .result-card-output {
312
+ border: 0 !important;
313
+ background: transparent !important;
314
+ }
315
+ .result-card-output img {
316
+ border-radius: 34px !important;
317
+ max-height: 620px !important;
318
+ object-fit: contain !important;
319
+ box-shadow: none !important;
320
+ }
321
+ .result-meta textarea,
322
+ .result-meta input {
323
+ color: #8b8b8b !important;
324
+ border: 0 !important;
325
+ background: transparent !important;
326
+ }
327
+ .section-title {
328
+ max-width: 1360px;
329
+ margin: 0 auto 14px;
330
+ padding: 0 24px;
331
+ font-size: 22px;
332
+ font-weight: 700;
333
+ }
334
+ .showcase-wrap {
335
+ width: 100%;
336
+ overflow: hidden;
337
+ padding: 0 0 30px;
338
+ }
339
+ .mode-preview img, .showcase-marquee img {
340
+ border-radius: 14px !important;
341
+ }
342
+ .showcase-marquee {
343
+ width: 100%;
344
+ overflow: hidden;
345
+ mask-image: linear-gradient(to right, transparent, black 7%, black 93%, transparent);
346
+ }
347
+ .showcase-track {
348
+ display: flex;
349
+ width: max-content;
350
+ gap: 24px;
351
+ animation: showcase-scroll 48s linear infinite;
352
+ }
353
+ .showcase-track:hover {
354
+ animation-play-state: paused;
355
+ }
356
+ .showcase-track img {
357
+ width: min(42vw, 560px);
358
+ min-width: 420px;
359
+ aspect-ratio: 16 / 9;
360
+ object-fit: cover;
361
+ box-shadow: 0 12px 36px rgba(0,0,0,.08);
362
+ }
363
+ @keyframes showcase-scroll {
364
+ from { transform: translateX(0); }
365
+ to { transform: translateX(calc(-50% - 12px)); }
366
+ }
367
+ @media (max-width: 760px) {
368
+ .tryon-topbar { top: 14px; left: 16px; }
369
+ .tryon-logo { width: 78px; height: 78px; border-radius: 18px; }
370
+ .tryon-hero { padding-top: 104px; }
371
+ .tryon-title { font-size: 24px; }
372
+ .prompt-shell textarea, .prompt-shell [contenteditable="true"] { font-size: 18px !important; min-height: 126px !important; }
373
+ .showcase-track img { width: 320px; min-width: 320px; }
374
+ }
375
+ """
376
+
377
 
378
+ with gr.Blocks(title="JoyAI Virtual Try-On", css=CSS) as demo:
379
+ logo_uri = _image_data_uri(LOGO_PATH)
380
+ gr.HTML(
381
+ f"""
382
+ <div class="tryon-topbar">
383
+ <img class="tryon-logo" src="{logo_uri}" alt="JoyAI Try-On" />
384
+ </div>
385
  """
386
  )
387
 
388
+ with gr.Column(elem_classes=["tryon-hero"]):
389
+ gr.HTML('<div class="tryon-title">开始虚拟试穿之旅,放入图像或写指令来看效果</div>')
390
+ user_box = gr.MultimodalTextbox(
391
+ label="",
392
+ file_count="multiple",
393
+ file_types=["image"],
394
+ placeholder="描述你想要的试穿效果",
395
+ submit_btn="↑",
396
+ elem_classes=["prompt-shell"],
397
+ )
398
+
399
+ with gr.Column(elem_classes=["workspace"]):
400
+ status = gr.Textbox(label="", interactive=False, elem_classes=["result-meta"])
401
+ output_image = gr.Image(label="", type="pil", elem_classes=["result-card-output"])
402
+ enhanced_prompt = gr.Textbox(label="PE 后完整文本", interactive=False, lines=3, elem_classes=["result-meta"])
403
+ pe_status = gr.Textbox(label="Prompt Enhancer 状态", interactive=False, elem_classes=["result-meta"])
404
+ with gr.Accordion("运行信息", open=False):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  used_seed = gr.Number(label="Used Seed", precision=0)
406
  used_garment_index = gr.Number(label="Used Garment Index", precision=0)
407
+
408
+ gr.HTML('<div class="section-title">效果展示</div>')
409
+ with gr.Column(elem_classes=["showcase-wrap"]):
410
+ gr.HTML(_showcase_marquee_html())
411
+
412
+ predict_outputs = [output_image, used_seed, used_garment_index, enhanced_prompt, pe_status, status]
413
+
414
+ user_box.submit(fn=predict_from_box, inputs=[user_box], outputs=predict_outputs, api_name="tryon")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
 
416
 
417
  if __name__ == "__main__":
app_v1.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ prompt: str,
32
+ prompt_enhancer: bool,
33
+ negative_prompt: str,
34
+ seed: int,
35
+ randomize_seed: bool,
36
+ num_inference_steps: int,
37
+ guidance_scale: float,
38
+ crop_and_paste: bool,
39
+ ):
40
+ try:
41
+ garment_images = _validate_inputs(person_image, garment_files)
42
+
43
+ if randomize_seed:
44
+ seed = random.randint(0, 2**31 - 1)
45
+
46
+ person = resize_for_demo(person_image, max_side=1024)
47
+ garments = [resize_for_demo(img, max_side=1024) for img in garment_images]
48
+
49
+ result, used_index, enhanced_prompt, pe_status, status = run_tryon(
50
+ person_image=person,
51
+ garment_images=garments,
52
+ garment_type=garment_type,
53
+ primary_garment_index=int(primary_garment_index),
54
+ seed=int(seed),
55
+ num_inference_steps=int(num_inference_steps),
56
+ guidance_scale=float(guidance_scale),
57
+ auto_crop=bool(crop_and_paste),
58
+ prompt=prompt,
59
+ prompt_enhancer=bool(prompt_enhancer),
60
+ negative_prompt=negative_prompt,
61
+ )
62
+
63
+ return result, int(seed), used_index, enhanced_prompt, pe_status, status
64
+
65
+ except gr.Error:
66
+ raise
67
+ except Exception as exc:
68
+ traceback.print_exc()
69
+ raise gr.Error(f"Inference failed: {exc}") from exc
70
+
71
+
72
+ with gr.Blocks(title="Virtual Try-On ZeroGPU Demo") as demo:
73
+ gr.Markdown(
74
+ """
75
+ # Virtual Try-On Demo
76
+
77
+ Upload one person image and one or more garment reference images.
78
+ The model uses Picture 1 as the garment reference and Picture 2 as the person image.
79
+ If you upload multiple garments, the selected primary garment index is used.
80
+ """
81
+ )
82
+
83
+ with gr.Row():
84
+ with gr.Column(scale=1):
85
+ person_image = gr.Image(
86
+ label="Person Image",
87
+ type="pil",
88
+ sources=["upload", "clipboard"],
89
+ )
90
+
91
+ garment_files = gr.File(
92
+ label="Garment Reference Images",
93
+ file_count="multiple",
94
+ file_types=["image"],
95
+ type="filepath",
96
+ )
97
+
98
+ garment_type = gr.Radio(
99
+ choices=["upper_body", "lower_body", "dress", "full_body"],
100
+ value="upper_body",
101
+ label="Garment Type",
102
+ )
103
+
104
+ primary_garment_index = gr.Slider(
105
+ minimum=0,
106
+ maximum=9,
107
+ value=0,
108
+ step=1,
109
+ label="Primary Garment Index",
110
+ )
111
+
112
+ prompt = gr.Textbox(
113
+ label="Original Try-On Prompt",
114
+ value="",
115
+ lines=3,
116
+ placeholder="Leave empty to use the default prompt for the selected garment type.",
117
+ )
118
+ prompt_enhancer = gr.Checkbox(value=True, label="Prompt Enhancer")
119
+
120
+ with gr.Accordion("Advanced Settings", open=False):
121
+ seed = gr.Number(value=42, precision=0, label="Seed")
122
+ randomize_seed = gr.Checkbox(value=True, label="Randomize Seed")
123
+ num_inference_steps = gr.Slider(
124
+ minimum=10,
125
+ maximum=80,
126
+ value=30,
127
+ step=1,
128
+ label="Inference Steps",
129
+ )
130
+ guidance_scale = gr.Slider(
131
+ minimum=0.0,
132
+ maximum=15.0,
133
+ value=6.0,
134
+ step=0.1,
135
+ label="Guidance Scale",
136
+ )
137
+ crop_and_paste = gr.Checkbox(
138
+ value=False,
139
+ label="Enable crop-and-paste when body bbox tools are available",
140
+ )
141
+ negative_prompt = gr.Textbox(
142
+ value="",
143
+ label="Negative Prompt",
144
+ lines=3,
145
+ placeholder="Leave empty to use the default negative prompt from tryon_infer.py.",
146
+ )
147
+
148
+ run_button = gr.Button("Generate Try-On", variant="primary")
149
+
150
+ with gr.Column(scale=1):
151
+ output_image = gr.Image(label="Try-On Result", type="pil")
152
+ used_seed = gr.Number(label="Used Seed", precision=0)
153
+ used_garment_index = gr.Number(label="Used Garment Index", precision=0)
154
+ enhanced_prompt = gr.Textbox(label="Enhanced Prompt Used", interactive=False, lines=5)
155
+ pe_status = gr.Textbox(label="Prompt Enhancer Status", interactive=False)
156
+ status = gr.Textbox(label="Status", interactive=False)
157
+
158
+ run_button.click(
159
+ fn=predict,
160
+ inputs=[
161
+ person_image,
162
+ garment_files,
163
+ garment_type,
164
+ primary_garment_index,
165
+ prompt,
166
+ prompt_enhancer,
167
+ negative_prompt,
168
+ seed,
169
+ randomize_seed,
170
+ num_inference_steps,
171
+ guidance_scale,
172
+ crop_and_paste,
173
+ ],
174
+ outputs=[output_image, used_seed, used_garment_index, enhanced_prompt, pe_status, status],
175
+ api_name="tryon",
176
+ )
177
+
178
+
179
+ if __name__ == "__main__":
180
+ demo.queue(max_size=20).launch()
assets/showcase/case_0.jpg ADDED

Git LFS Details

  • SHA256: 2a43de562ddfade9e67960db23440f92de4de71da6c29d8dd66c702955c3e924
  • Pointer size: 130 Bytes
  • Size of remote file: 82.9 kB
assets/showcase/case_1.jpg ADDED

Git LFS Details

  • SHA256: 0e81d9c2636b56e5d613d32b992f52a96509bd4b045b547c7edc79a8f0ca57e3
  • Pointer size: 130 Bytes
  • Size of remote file: 50.9 kB
assets/showcase/case_2.jpg ADDED

Git LFS Details

  • SHA256: 76e5a7771b0b6064e07fc3299b1a2658d4cab037c0158b720e63f7588c30aa7c
  • Pointer size: 130 Bytes
  • Size of remote file: 89.6 kB
assets/showcase/case_3.jpg ADDED

Git LFS Details

  • SHA256: b3154a9e28c011dd29d22551b7b402cf8edf7964e9e174bb128dba0eecfb03fd
  • Pointer size: 131 Bytes
  • Size of remote file: 101 kB
assets/tryon_logo_compact.png ADDED

Git LFS Details

  • SHA256: 0037101eeb8230c7376186d02b32d1819456deeb1a9aa79d7dcbe6ca4cdc0467
  • Pointer size: 131 Bytes
  • Size of remote file: 961 kB
packages.txt CHANGED
@@ -2,4 +2,4 @@ ffmpeg
2
  libgl1
3
  libglib2.0-0
4
  git
5
-
 
2
  libgl1
3
  libglib2.0-0
4
  git
5
+ fonts-noto-cjk