ArmanRV commited on
Commit
0cb8e39
·
verified ·
1 Parent(s): 9edb378

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +239 -88
app.py CHANGED
@@ -1,11 +1,30 @@
1
  # -*- coding: utf-8 -*-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import os
 
3
  import time
4
- from typing import List, Optional, Tuple
 
5
 
6
  import spaces
7
  import gradio as gr
8
- from PIL import Image
9
 
10
  # =========================
11
  # FIX: gradio 4.24 / gradio_client crashes on boolean JSON Schemas in /api_info
@@ -179,10 +198,18 @@ def clamp_int(x, lo, hi):
179
  return max(lo, min(hi, x))
180
 
181
 
 
 
 
 
 
 
 
 
182
  _last_call_ts = 0.0
183
 
184
 
185
- def allow_call(min_interval_sec: float = 2.5) -> Tuple[bool, str]:
186
  global _last_call_ts
187
  now = time.time()
188
  if now - _last_call_ts < min_interval_sec:
@@ -192,6 +219,113 @@ def allow_call(min_interval_sec: float = 2.5) -> Tuple[bool, str]:
192
  return True, ""
193
 
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  # =========================
196
  # Model init (local IDM-VTON)
197
  # =========================
@@ -201,9 +335,7 @@ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
201
  DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
202
  print("DEVICE:", DEVICE, "DTYPE:", DTYPE, flush=True)
203
 
204
- tensor_transfrom = transforms.Compose(
205
- [transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]
206
- )
207
 
208
  # Components
209
  unet = UNet2DConditionModel.from_pretrained(base_path, subfolder="unet", torch_dtype=DTYPE)
@@ -253,11 +385,17 @@ pipe.unet_encoder = UNet_Encoder
253
  def start_tryon(
254
  human_pil: Image.Image,
255
  garm_img: Image.Image,
 
256
  auto_mask: bool = True,
257
- crop_center: bool = True,
258
- denoise_steps: int = 25,
259
- seed: int = 42,
 
 
 
 
260
  ) -> Image.Image:
 
261
  device = "cuda" if torch.cuda.is_available() else "cpu"
262
  dtype = torch.float16 if device == "cuda" else torch.float32
263
 
@@ -267,38 +405,33 @@ def start_tryon(
267
  pipe.to(device)
268
  pipe.unet_encoder.to(device)
269
 
270
- garm_img = garm_img.convert("RGB").resize((768, 1024))
271
  human_img_orig = human_pil.convert("RGB")
 
 
272
 
273
- # Crop
274
- if crop_center:
275
- width, height = human_img_orig.size
276
- target_width = int(min(width, height * (3 / 4)))
277
- target_height = int(min(height, width * (4 / 3)))
278
- left = (width - target_width) / 2
279
- top = (height - target_height) / 2
280
- right = (width + target_width) / 2
281
- bottom = (height + target_height) / 2
282
- cropped_img = human_img_orig.crop((left, top, right, bottom))
283
- crop_size = cropped_img.size
284
- human_img = cropped_img.resize((768, 1024))
285
- else:
286
- human_img = human_img_orig.resize((768, 1024))
287
- crop_size = None
288
- left = top = 0
289
 
290
- # Mask (как раньше: upper_body всегда)
291
  if auto_mask:
292
- keypoints = openpose_model(human_img.resize((384, 512)))
293
- model_parse, _ = parsing_model(human_img.resize((384, 512)))
294
- mask, _ = get_mask_location("hd", "upper_body", model_parse, keypoints)
295
- mask = mask.resize((768, 1024))
 
 
 
 
 
296
  else:
297
- mask = Image.new("L", (768, 1024), 0)
298
 
299
- # DensePose
300
- human_img_arg = _apply_exif_orientation(human_img.resize((384, 512)))
301
- human_img_arg = convert_PIL_to_numpy(human_img_arg, format="BGR")
302
 
303
  args = apply_net.create_argument_parser().parse_args(
304
  (
@@ -312,18 +445,28 @@ def start_tryon(
312
  "cuda" if device == "cuda" else "cpu",
313
  )
314
  )
315
- pose_img = args.func(args, human_img_arg)
316
  pose_img = pose_img[:, :, ::-1]
317
- pose_img = Image.fromarray(pose_img).resize((768, 1024))
318
-
319
- # Fixed prompts (как раньше)
320
- garment_des = "a garment"
321
- prompt_main = "model is wearing " + garment_des
322
- prompt_cloth = "a photo of " + garment_des
323
- negative_prompt = "monochrome, lowres, bad anatomy, worst quality, low quality"
324
-
325
- denoise_steps = clamp_int(denoise_steps, 20, 40)
326
- seed = clamp_int(seed, 0, 999999)
 
 
 
 
 
 
 
 
 
 
327
 
328
  with torch.no_grad():
329
  if device == "cuda":
@@ -352,12 +495,7 @@ def start_tryon(
352
  negative_prompt=negative_prompt,
353
  )
354
 
355
- (
356
- prompt_embeds_c,
357
- _,
358
- _,
359
- _,
360
- ) = pipe.encode_prompt(
361
  [prompt_cloth],
362
  num_images_per_prompt=1,
363
  do_classifier_free_guidance=False,
@@ -365,7 +503,7 @@ def start_tryon(
365
  )
366
 
367
  pose_t = tensor_transfrom(pose_img).unsqueeze(0).to(device=device, dtype=dtype)
368
- garm_t = tensor_transfrom(garm_img).unsqueeze(0).to(device=device, dtype=dtype)
369
 
370
  generator = torch.Generator(device).manual_seed(seed)
371
 
@@ -376,24 +514,24 @@ def start_tryon(
376
  negative_pooled_prompt_embeds=negative_pooled_prompt_embeds.to(device=device, dtype=dtype),
377
  num_inference_steps=denoise_steps,
378
  generator=generator,
379
- strength=1.0,
380
  pose_img=pose_t,
381
  text_embeds_cloth=prompt_embeds_c.to(device=device, dtype=dtype),
382
  cloth=garm_t,
383
  mask_image=mask,
384
- image=human_img,
385
- height=1024,
386
- width=768,
387
- ip_adapter_image=garm_img.resize((768, 1024)),
388
- guidance_scale=2.0,
389
  )[0]
390
 
391
- out_img = images[0]
392
- if crop_center and crop_size is not None:
393
- out_img_rs = out_img.resize(crop_size)
394
- human_img_orig.paste(out_img_rs, (int(left), int(top)))
395
- return human_img_orig
396
- return out_img
397
 
398
 
399
  # =========================
@@ -406,17 +544,6 @@ div[class*="footer"] {display:none !important;}
406
  button[aria-label="Settings"] {display:none !important;}
407
  """
408
 
409
- PHOTO_TIPS_MD = """
410
- ### Какое фото подойдёт
411
- ✅ В полный рост или по пояс
412
- ✅ Руки �� предметы не закрывают тело
413
- ✅ Одежда по фигуре
414
- ✅ Вы стоите прямо и смотрите в камеру
415
- ✅ Хорошее освещение
416
- ✅ В кадре нет других людей
417
- """
418
-
419
-
420
  def refresh_catalog():
421
  ensure_garments_downloaded()
422
  files = list_garments()
@@ -433,10 +560,20 @@ def on_gallery_select(files_list: List[str], evt: gr.SelectData):
433
  return files_list[idx], f"👕 Выбрано: {files_list[idx]}"
434
 
435
 
436
- def tryon_ui(person_pil, selected_filename):
 
 
 
 
 
 
 
 
 
 
437
  yield None, "⏳ Обработка... (первый запуск может быть дольше)"
438
 
439
- ok, msg = allow_call(2.5)
440
  if not ok:
441
  yield None, msg
442
  return
@@ -457,10 +594,14 @@ def tryon_ui(person_pil, selected_filename):
457
  out_img = start_tryon(
458
  human_pil=person_pil,
459
  garm_img=garm,
460
- auto_mask=True,
461
- crop_center=True,
462
- denoise_steps=25,
463
- seed=42,
 
 
 
 
464
  )
465
  yield out_img, "✅ Готово"
466
  except Exception as e:
@@ -482,9 +623,6 @@ with gr.Blocks(title="Virtual Try-On Rendez-vous", css=CUSTOM_CSS) as demo:
482
  with gr.Column():
483
  person = gr.Image(label="Фото человека", type="pil", height=420)
484
 
485
- # Подсказка под загрузкой фото
486
- gr.Markdown(PHOTO_TIPS_MD)
487
-
488
  with gr.Row():
489
  refresh_btn = gr.Button("🔄 Обновить каталог одежды", variant="secondary")
490
  selected_label = gr.Markdown("👕 Выберите одежду ниже")
@@ -497,6 +635,19 @@ with gr.Blocks(title="Virtual Try-On Rendez-vous", css=CUSTOM_CSS) as demo:
497
  allow_preview=True,
498
  )
499
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  run = gr.Button("Примерить", variant="primary")
501
  status = gr.Textbox(value="Ожидание...", interactive=False)
502
 
@@ -517,7 +668,7 @@ with gr.Blocks(title="Virtual Try-On Rendez-vous", css=CUSTOM_CSS) as demo:
517
 
518
  run.click(
519
  fn=tryon_ui,
520
- inputs=[person, selected_garment_state],
521
  outputs=[out, status],
522
  concurrency_limit=1,
523
  )
@@ -532,5 +683,5 @@ if __name__ == "__main__":
532
  auth=APP_AUTH,
533
  max_threads=4,
534
  show_error=True,
535
- show_api=False, # важно: не показываем API, но /api_info могут дергать — патч это чинит
536
- )
 
1
  # -*- coding: utf-8 -*-
2
+ """
3
+ Virtual Try-On Rendez-vous — production wrapper for IDM-VTON (SDXL)
4
+
5
+ Что изменено по твоему запросу (убрано/исправлено):
6
+ 1) НЕТ “жёстко upper_body для всего” — маска выбирается АВТО по имени/папке одежды (dress/lower/upper),
7
+ либо можно отключить авто-маску полностью.
8
+ 2) НЕТ fixed strength=1.0 — strength настраиваемый (по умолчанию 0.9).
9
+ 3) НЕТ фиксированных промптов “a garment” — промпт генерируется из имени файла/папки одежды + эвристики,
10
+ можно переопределить вручную.
11
+ 4) НЕТ crop-center + paste обратно — используется letterbox (масштаб с сохранением пропорций + padding),
12
+ затем padding убирается, и результат возвращается в исходный размер.
13
+ 5) НЕТ принудительного 768×1024 “всегда” — размер выбирается ДИНАМИЧЕСКИ от входного фото (с ограничением max_side),
14
+ кратно 8.
15
+ 6) НЕТ низких/фиксированных CFG/steps/seed — все параметры управляемые в UI; seed может быть -1 (рандом).
16
+
17
+ Остальное (датасет одежды, галерея, queue, patch gradio_client) оставлено как инфраструктура.
18
+ """
19
  import os
20
+ import re
21
  import time
22
+ import math
23
+ from typing import List, Optional, Tuple, Dict, Any
24
 
25
  import spaces
26
  import gradio as gr
27
+ from PIL import Image, ImageOps
28
 
29
  # =========================
30
  # FIX: gradio 4.24 / gradio_client crashes on boolean JSON Schemas in /api_info
 
198
  return max(lo, min(hi, x))
199
 
200
 
201
+ def clamp_float(x, lo, hi):
202
+ try:
203
+ x = float(x)
204
+ except Exception:
205
+ x = lo
206
+ return max(lo, min(hi, x))
207
+
208
+
209
  _last_call_ts = 0.0
210
 
211
 
212
+ def allow_call(min_interval_sec: float = 2.0) -> Tuple[bool, str]:
213
  global _last_call_ts
214
  now = time.time()
215
  if now - _last_call_ts < min_interval_sec:
 
219
  return True, ""
220
 
221
 
222
+ def round_to_multiple(x: int, m: int = 8) -> int:
223
+ return max(m, int(round(x / m) * m))
224
+
225
+
226
+ def pick_target_size_keep_aspect(w: int, h: int, max_side: int) -> Tuple[int, int]:
227
+ """
228
+ Возвращает (tw, th) <= max_side по большей стороне, кратно 8.
229
+ """
230
+ if w <= 0 or h <= 0:
231
+ return 768, 1024
232
+ scale = min(max_side / float(max(w, h)), 1.0)
233
+ tw = round_to_multiple(int(w * scale), 8)
234
+ th = round_to_multiple(int(h * scale), 8)
235
+ # защитимся от слишком маленьких
236
+ tw = max(512, tw)
237
+ th = max(512, th)
238
+ # еще раз не превышать max_side
239
+ if max(tw, th) > max_side:
240
+ scale2 = max_side / float(max(tw, th))
241
+ tw = round_to_multiple(int(tw * scale2), 8)
242
+ th = round_to_multiple(int(th * scale2), 8)
243
+ return tw, th
244
+
245
+
246
+ def letterbox(img: Image.Image, target_w: int, target_h: int, fill=(0, 0, 0)) -> Tuple[Image.Image, Dict[str, int]]:
247
+ """
248
+ Масштабирует с сохранением пропорций + padding до target_w/target_h.
249
+ Возвращает (img_lb, meta) где meta содержит offset/size для обратного unletterbox.
250
+ """
251
+ src_w, src_h = img.size
252
+ if src_w <= 0 or src_h <= 0:
253
+ out = img.resize((target_w, target_h))
254
+ return out, {"x": 0, "y": 0, "w": target_w, "h": target_h, "src_w": src_w, "src_h": src_h}
255
+
256
+ scale = min(target_w / src_w, target_h / src_h)
257
+ new_w = max(1, int(src_w * scale))
258
+ new_h = max(1, int(src_h * scale))
259
+
260
+ img_rs = img.resize((new_w, new_h), Image.LANCZOS)
261
+ canvas = Image.new("RGB", (target_w, target_h), fill)
262
+ x = (target_w - new_w) // 2
263
+ y = (target_h - new_h) // 2
264
+ canvas.paste(img_rs, (x, y))
265
+ meta = {"x": x, "y": y, "w": new_w, "h": new_h, "src_w": src_w, "src_h": src_h}
266
+ return canvas, meta
267
+
268
+
269
+ def unletterbox(img_lb: Image.Image, meta: Dict[str, int]) -> Image.Image:
270
+ """
271
+ Вырезает область без padding и возвращает как есть (потом можно resize к исходнику).
272
+ """
273
+ x, y, w, h = meta["x"], meta["y"], meta["w"], meta["h"]
274
+ return img_lb.crop((x, y, x + w, y + h))
275
+
276
+
277
+ def infer_garment_class_from_path(relpath: str) -> str:
278
+ """
279
+ Возвращает тип для get_mask_location: 'upper_body' | 'lower_body' | 'dresses'
280
+ Это НЕ “жестко upper_body” — эвристика по папке/имени.
281
+ """
282
+ s = (relpath or "").lower()
283
+ # папки/имена под платья
284
+ if any(k in s for k in ["dress", "dresses", "suk", "plate", "плать", "sarafan"]):
285
+ return "dresses"
286
+ # низ
287
+ if any(k in s for k in ["pants", "trouser", "jeans", "skirt", "short", "брюк", "джин", "юбк", "шорт"]):
288
+ return "lower_body"
289
+ # верх по умолчанию
290
+ return "upper_body"
291
+
292
+
293
+ def guess_garment_description(relpath: str) -> str:
294
+ """
295
+ Генерирует более полезное текстовое описание одежды из имени файла/папки.
296
+ (Это замена твоего фиксированного 'a garment'.)
297
+ """
298
+ s = (relpath or "").replace("\\", "/").lower()
299
+ # словарь эвристик
300
+ mapping = [
301
+ (["shearling", "dub", "дублен", "sheepskin"], "a shearling jacket"),
302
+ (["coat", "пальт", "overcoat"], "a coat"),
303
+ (["jacket", "куртк", "bomber", "парка", "parka"], "a jacket"),
304
+ (["blazer", "пидж", "suit"], "a blazer"),
305
+ (["hoodie", "худи"], "a hoodie"),
306
+ (["sweater", "свит", "jumper"], "a sweater"),
307
+ (["shirt", "рубаш"], "a shirt"),
308
+ (["tshirt", "tee", "футбол"], "a t-shirt"),
309
+ (["dress", "плать", "sarafan"], "a dress"),
310
+ (["pants", "jeans", "брюк", "джин"], "pants"),
311
+ (["skirt", "юбк"], "a skirt"),
312
+ ]
313
+ for keys, desc in mapping:
314
+ if any(k in s for k in keys):
315
+ return desc
316
+
317
+ # иначе — попытка вытащить “человеческое” имя
318
+ base = os.path.splitext(os.path.basename(s))[0]
319
+ base = re.sub(r"[_\-]+", " ", base)
320
+ base = re.sub(r"\d+", " ", base)
321
+ base = re.sub(r"\s+", " ", base).strip()
322
+ if len(base) >= 3:
323
+ # ограничим длину
324
+ words = base.split()[:4]
325
+ return "a " + " ".join(words)
326
+ return "a piece of clothing"
327
+
328
+
329
  # =========================
330
  # Model init (local IDM-VTON)
331
  # =========================
 
335
  DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
336
  print("DEVICE:", DEVICE, "DTYPE:", DTYPE, flush=True)
337
 
338
+ tensor_transfrom = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
 
 
339
 
340
  # Components
341
  unet = UNet2DConditionModel.from_pretrained(base_path, subfolder="unet", torch_dtype=DTYPE)
 
385
  def start_tryon(
386
  human_pil: Image.Image,
387
  garm_img: Image.Image,
388
+ garm_relpath: str = "",
389
  auto_mask: bool = True,
390
+ denoise_steps: int = 30,
391
+ guidance_scale: float = 3.5,
392
+ strength: float = 0.90,
393
+ seed: int = -1,
394
+ max_side: int = 1024,
395
+ prompt_override: str = "",
396
+ negative_prompt: str = "monochrome, lowres, bad anatomy, worst quality, low quality",
397
  ) -> Image.Image:
398
+ # pick device/dtype
399
  device = "cuda" if torch.cuda.is_available() else "cpu"
400
  dtype = torch.float16 if device == "cuda" else torch.float32
401
 
 
405
  pipe.to(device)
406
  pipe.unet_encoder.to(device)
407
 
408
+ # --- sizes (dynamic, no forced 768x1024) ---
409
  human_img_orig = human_pil.convert("RGB")
410
+ src_w, src_h = human_img_orig.size
411
+ target_w, target_h = pick_target_size_keep_aspect(src_w, src_h, max_side=max_side)
412
 
413
+ # letterbox to target size (no crop-center, no paste-back)
414
+ human_lb, lb_meta = letterbox(human_img_orig, target_w, target_h, fill=(0, 0, 0))
415
+ garm_img = garm_img.convert("RGB")
416
+ garm_lb, _ = letterbox(garm_img, target_w, target_h, fill=(0, 0, 0))
 
 
 
 
 
 
 
 
 
 
 
 
417
 
418
+ # --- Mask (not fixed upper_body) ---
419
  if auto_mask:
420
+ # preprocess runs on 384x512; use letterbox to avoid distortion
421
+ human_384, _m = letterbox(human_lb, 384, 512, fill=(0, 0, 0))
422
+ keypoints = openpose_model(human_384)
423
+ model_parse, _ = parsing_model(human_384)
424
+
425
+ cloth_class = infer_garment_class_from_path(garm_relpath)
426
+ mask, _ = get_mask_location("hd", cloth_class, model_parse, keypoints)
427
+ # upscale mask back to target size
428
+ mask = mask.resize((target_w, target_h), Image.BILINEAR)
429
  else:
430
+ mask = Image.new("L", (target_w, target_h), 0)
431
 
432
+ # --- DensePose ---
433
+ human_dp = _apply_exif_orientation(human_lb.resize((384, 512)))
434
+ human_dp = convert_PIL_to_numpy(human_dp, format="BGR")
435
 
436
  args = apply_net.create_argument_parser().parse_args(
437
  (
 
445
  "cuda" if device == "cuda" else "cpu",
446
  )
447
  )
448
+ pose_img = args.func(args, human_dp)
449
  pose_img = pose_img[:, :, ::-1]
450
+ pose_img = Image.fromarray(pose_img).resize((target_w, target_h), Image.BILINEAR)
451
+
452
+ # --- prompts (not fixed “a garment”) ---
453
+ garment_desc = guess_garment_description(garm_relpath)
454
+ if prompt_override and prompt_override.strip():
455
+ garment_desc = prompt_override.strip()
456
+
457
+ prompt_main = f"model is wearing {garment_desc}"
458
+ prompt_cloth = f"a photo of {garment_desc}"
459
+
460
+ # --- params (no fixed low steps/cfg/seed) ---
461
+ denoise_steps = clamp_int(denoise_steps, 15, 60)
462
+ guidance_scale = clamp_float(guidance_scale, 0.0, 12.0)
463
+ strength = clamp_float(strength, 0.50, 1.00)
464
+ if seed is None:
465
+ seed = -1
466
+ seed = int(seed)
467
+ if seed < 0:
468
+ # random but reproducible per call if needed
469
+ seed = int.from_bytes(os.urandom(2), "big") + int(time.time() * 1000) % 1000000
470
 
471
  with torch.no_grad():
472
  if device == "cuda":
 
495
  negative_prompt=negative_prompt,
496
  )
497
 
498
+ (prompt_embeds_c, _, _, _) = pipe.encode_prompt(
 
 
 
 
 
499
  [prompt_cloth],
500
  num_images_per_prompt=1,
501
  do_classifier_free_guidance=False,
 
503
  )
504
 
505
  pose_t = tensor_transfrom(pose_img).unsqueeze(0).to(device=device, dtype=dtype)
506
+ garm_t = tensor_transfrom(garm_lb).unsqueeze(0).to(device=device, dtype=dtype)
507
 
508
  generator = torch.Generator(device).manual_seed(seed)
509
 
 
514
  negative_pooled_prompt_embeds=negative_pooled_prompt_embeds.to(device=device, dtype=dtype),
515
  num_inference_steps=denoise_steps,
516
  generator=generator,
517
+ strength=strength, # <-- not fixed 1.0
518
  pose_img=pose_t,
519
  text_embeds_cloth=prompt_embeds_c.to(device=device, dtype=dtype),
520
  cloth=garm_t,
521
  mask_image=mask,
522
+ image=human_lb,
523
+ height=target_h,
524
+ width=target_w,
525
+ ip_adapter_image=garm_lb, # keep conditioning, but not hard-resized 768x1024
526
+ guidance_scale=guidance_scale, # <-- not fixed low value
527
  )[0]
528
 
529
+ out_img_lb = images[0].convert("RGB")
530
+
531
+ # remove letterbox padding and resize back to original size (no crop-center paste)
532
+ out_core = unletterbox(out_img_lb, lb_meta)
533
+ out_final = out_core.resize((src_w, src_h), Image.LANCZOS)
534
+ return out_final
535
 
536
 
537
  # =========================
 
544
  button[aria-label="Settings"] {display:none !important;}
545
  """
546
 
 
 
 
 
 
 
 
 
 
 
 
547
  def refresh_catalog():
548
  ensure_garments_downloaded()
549
  files = list_garments()
 
560
  return files_list[idx], f"👕 Выбрано: {files_list[idx]}"
561
 
562
 
563
+ def tryon_ui(
564
+ person_pil,
565
+ selected_filename,
566
+ auto_mask,
567
+ steps,
568
+ cfg,
569
+ strength,
570
+ seed,
571
+ max_side,
572
+ prompt_override,
573
+ ):
574
  yield None, "⏳ Обработка... (первый запуск может быть дольше)"
575
 
576
+ ok, msg = allow_call(2.0)
577
  if not ok:
578
  yield None, msg
579
  return
 
594
  out_img = start_tryon(
595
  human_pil=person_pil,
596
  garm_img=garm,
597
+ garm_relpath=selected_filename,
598
+ auto_mask=bool(auto_mask),
599
+ denoise_steps=int(steps),
600
+ guidance_scale=float(cfg),
601
+ strength=float(strength),
602
+ seed=int(seed),
603
+ max_side=int(max_side),
604
+ prompt_override=str(prompt_override or "").strip(),
605
  )
606
  yield out_img, "✅ Готово"
607
  except Exception as e:
 
623
  with gr.Column():
624
  person = gr.Image(label="Фото человека", type="pil", height=420)
625
 
 
 
 
626
  with gr.Row():
627
  refresh_btn = gr.Button("🔄 Обновить каталог одежды", variant="secondary")
628
  selected_label = gr.Markdown("👕 Выберите одежду ниже")
 
635
  allow_preview=True,
636
  )
637
 
638
+ with gr.Accordion("⚙️ Настройки качества", open=False):
639
+ auto_mask = gr.Checkbox(value=True, label="Auto mask (парсинг + поза)")
640
+ steps = gr.Slider(15, 60, value=30, step=1, label="Шаги (num_inference_steps)")
641
+ cfg = gr.Slider(0.0, 12.0, value=3.5, step=0.1, label="Guidance scale (CFG)")
642
+ strength = gr.Slider(0.50, 1.00, value=0.90, step=0.01, label="Strength (насколько сильно перерисовывать)")
643
+ seed = gr.Number(value=-1, precision=0, label="Seed (-1 = случайный)")
644
+ max_side = gr.Slider(768, 1408, value=1024, step=64, label="Максимальный размер стороны (динамический)")
645
+ prompt_override = gr.Textbox(
646
+ value="",
647
+ label="Описание одежды (опц.)",
648
+ placeholder="Напр.: a black leather jacket / a blazer / a coat ... (если пусто — авто по имени файла)",
649
+ )
650
+
651
  run = gr.Button("Примерить", variant="primary")
652
  status = gr.Textbox(value="Ожидание...", interactive=False)
653
 
 
668
 
669
  run.click(
670
  fn=tryon_ui,
671
+ inputs=[person, selected_garment_state, auto_mask, steps, cfg, strength, seed, max_side, prompt_override],
672
  outputs=[out, status],
673
  concurrency_limit=1,
674
  )
 
683
  auth=APP_AUTH,
684
  max_threads=4,
685
  show_error=True,
686
+ show_api=False,
687
+ )