ArmanRV commited on
Commit
e2cc592
·
verified ·
1 Parent(s): c624c4c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +449 -252
app.py CHANGED
@@ -1,252 +1,449 @@
1
- # -*- coding: utf-8 -*-
2
- import os
3
- import time
4
- import tempfile
5
- import gradio as gr
6
- from PIL import Image, ImageFilter, ImageEnhance
7
- from gradio_client import Client, handle_file
8
- from huggingface_hub import login
9
-
10
- SPACE = "yisol/IDM-VTON"
11
- API_NAME = "/tryon"
12
-
13
- HF_TOKEN = "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"
14
-
15
- print("HF_TOKEN set:", bool(HF_TOKEN), "len:", len(HF_TOKEN) if HF_TOKEN else 0)
16
-
17
- if HF_TOKEN and HF_TOKEN != "PASTE_YOUR_HF_TOKEN_HERE":
18
- try:
19
- login(token=HF_TOKEN, add_to_git_credential=False)
20
- print("HF login: OK")
21
- except Exception as e:
22
- print("HF login: FAILED:", str(e)[:200])
23
- else:
24
- print("HF login: skipped (token placeholder or empty)")
25
-
26
- _client = None
27
-
28
-
29
- def get_client():
30
- global _client
31
- if _client is None:
32
- _client = Client(SPACE)
33
- return _client
34
-
35
-
36
- def pil_save_temp(pil_img: Image.Image, suffix: str) -> str:
37
- f = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
38
- path = f.name
39
- f.close()
40
- pil_img.save(path)
41
- return path
42
-
43
-
44
- def clamp_int(x, lo, hi):
45
- try:
46
- x = int(x)
47
- except Exception:
48
- x = lo
49
- return max(lo, min(hi, x))
50
-
51
-
52
- def postprocess_text_boost(img: Image.Image, boost: int) -> Image.Image:
53
- boost = clamp_int(boost, 0, 100)
54
- if boost <= 0:
55
- return img
56
-
57
- t = boost / 100.0
58
- out = img.convert("RGB")
59
-
60
- out = out.filter(
61
- ImageFilter.UnsharpMask(
62
- radius=1.2 + 1.0 * t,
63
- percent=int(120 + 80 * t),
64
- threshold=2
65
- )
66
- )
67
- out = ImageEnhance.Contrast(out).enhance(1.0 + 0.10 * t)
68
- out = ImageEnhance.Sharpness(out).enhance(1.0 + 0.30 * t)
69
-
70
- w, h = out.size
71
- box = (int(w * 0.25), int(h * 0.22), int(w * 0.75), int(h * 0.72))
72
-
73
- region = out.crop(box)
74
- region = region.filter(
75
- ImageFilter.UnsharpMask(
76
- radius=1.8 + 1.6 * t,
77
- percent=int(160 + 120 * t),
78
- threshold=2
79
- )
80
- )
81
- region = ImageEnhance.Contrast(region).enhance(1.0 + 0.15 * t)
82
- region = ImageEnhance.Sharpness(region).enhance(1.0 + 0.45 * t)
83
-
84
- blended = Image.blend(out.crop(box), region, alpha=min(0.55 + 0.35 * t, 0.9))
85
- out.paste(blended, box)
86
- return out
87
-
88
-
89
- def apply_mode_defaults(mode: str, denoise_steps: int, seed: int, crop_center: bool, garment_desc: str):
90
- denoise_steps = clamp_int(denoise_steps, 10, 40)
91
- seed = clamp_int(seed, 0, 999999)
92
-
93
- if mode == "Balanced":
94
- denoise_steps = max(denoise_steps, 28)
95
- elif mode == "Quality":
96
- denoise_steps = max(denoise_steps, 38)
97
- elif mode == "Text/Logo":
98
- denoise_steps = 40
99
- crop_center = False
100
- if seed == 0:
101
- seed = 42
102
- if garment_desc and "text" not in garment_desc.lower() and "logo" not in garment_desc.lower():
103
- garment_desc = garment_desc.strip() + ", with clear text/logo print"
104
-
105
- return denoise_steps, seed, crop_center, garment_desc
106
-
107
-
108
- def tryon_remote(
109
- person_pil,
110
- garment_pil,
111
- garment_desc,
112
- auto_mask,
113
- crop_center,
114
- denoise_steps,
115
- seed,
116
- mode,
117
- text_boost,
118
- ):
119
- if person_pil is None:
120
- return None, "❌ Загрузите фото человека"
121
- if garment_pil is None:
122
- return None, "❌ Загрузите одежду"
123
-
124
- denoise_steps, seed, crop_center, garment_desc = apply_mode_defaults(
125
- mode=mode,
126
- denoise_steps=denoise_steps,
127
- seed=seed,
128
- crop_center=crop_center,
129
- garment_desc=garment_desc or "a t-shirt"
130
- )
131
-
132
- p_path = pil_save_temp(person_pil.convert("RGB"), ".png")
133
- g_path = pil_save_temp(garment_pil.convert("RGB"), ".png")
134
-
135
- try:
136
- client = get_client()
137
-
138
- last_err = None
139
- for attempt in range(1, 4):
140
- try:
141
- result = client.predict(
142
- dict={
143
- "background": handle_file(p_path),
144
- "layers": [],
145
- "composite": None
146
- },
147
- garm_img=handle_file(g_path),
148
- garment_des=garment_desc,
149
- is_checked=bool(auto_mask),
150
- is_checked_crop=bool(crop_center),
151
- denoise_steps=int(denoise_steps),
152
- seed=int(seed),
153
- api_name=API_NAME
154
- )
155
-
156
- if isinstance(result, (list, tuple)):
157
- result = result[0]
158
-
159
- out = Image.open(result).convert("RGB")
160
- out = postprocess_text_boost(out, text_boost)
161
-
162
- return out, f"✅ Готово (mode={mode}, steps={denoise_steps}, seed={seed}, crop={crop_center})"
163
-
164
- except Exception as e:
165
- last_err = e
166
- msg = str(e)
167
-
168
- if "ZeroGPU" in msg or "Unlogged user" in msg or "quota" in msg.lower():
169
- return None, (
170
- "❌ ZeroGPU-квота/нагрузка Hugging Face.\n"
171
- "Если HF login = OK, то это лимит/очередь Space. Попробуйте позже или смените seed."
172
- )
173
-
174
- time.sleep(1.2 * attempt)
175
-
176
- return None, f"❌ Ошибка Space: {str(last_err)[:250]}"
177
-
178
- finally:
179
- for path in (p_path, g_path):
180
- try:
181
- os.remove(path)
182
- except Exception:
183
- pass
184
-
185
-
186
- def reset_ui():
187
- return None, None, None, "Ожидание..."
188
-
189
-
190
- CUSTOM_CSS = """
191
- footer {display:none !important;}
192
- #api-info {display:none !important;}
193
- div[class*="footer"] {display:none !important;}
194
- button[aria-label="Settings"] {display:none !important;}
195
- """
196
-
197
- with gr.Blocks(title="Virtual Try-On Rendez-vous", css=CUSTOM_CSS) as demo:
198
- gr.Markdown(
199
- "# Virtual Try-On Rendez-vous\n\n"
200
- "**Для текста/логотипов:** режим *Text/Logo*, steps=40, попробуйте разные *seed*."
201
- )
202
-
203
- with gr.Row():
204
- with gr.Column():
205
- person = gr.Image(label="Фото человека", type="pil", height=420)
206
- garment = gr.Image(label="Одежда", type="pil", height=320)
207
-
208
- garment_desc = gr.Textbox(
209
- label="Описание одежды (чем точнее — тем лучше)",
210
- value="a t-shirt with clear text/logo print"
211
- )
212
-
213
- mode = gr.Radio(
214
- ["Balanced", "Quality", "Text/Logo"],
215
- value="Text/Logo",
216
- label="Режим качества"
217
- )
218
-
219
- with gr.Accordion("Настройки", open=False):
220
- auto_mask = gr.Checkbox(label="Auto-mask", value=True)
221
- crop_center = gr.Checkbox(label="Crop по центру", value=True)
222
- denoise_steps = gr.Slider(10, 40, value=28, step=1, label="Denoise steps")
223
- seed = gr.Slider(0, 999999, value=42, step=1, label="Seed")
224
- text_boost = gr.Slider(0, 100, value=55, step=1, label="Text boost (резкость/контраст принта)")
225
-
226
- with gr.Row():
227
- run = gr.Button("Примерить", variant="primary")
228
- reset = gr.Button("Сбросить", variant="secondary")
229
-
230
- status = gr.Textbox(value="Ожидание...", interactive=False)
231
-
232
- with gr.Column():
233
- out = gr.Image(label="Результат", type="pil", height=760)
234
-
235
- run.click(
236
- fn=tryon_remote,
237
- inputs=[person, garment, garment_desc, auto_mask, crop_center, denoise_steps, seed, mode, text_boost],
238
- outputs=[out, status]
239
- )
240
-
241
- reset.click(
242
- fn=reset_ui,
243
- inputs=[],
244
- outputs=[person, garment, out, status]
245
- )
246
-
247
- if __name__ == "__main__":
248
- demo.launch(
249
- server_name="0.0.0.0",
250
- server_port=7863,
251
- share=False
252
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ import time
4
+ import tempfile
5
+
6
+ import gradio as gr
7
+ import numpy as np
8
+ from PIL import Image, ImageFilter, ImageEnhance
9
+
10
+ from gradio_client import Client, handle_file
11
+ from huggingface_hub import login
12
+
13
+ SPACE = "yisol/IDM-VTON"
14
+ API_NAME = "/tryon"
15
+
16
+ # ✅ IMPORTANT: do NOT hardcode tokens in a Space repo
17
+ HF_TOKEN = os.getenv("HF_TOKEN", "")
18
+
19
+ print("HF_TOKEN set:", bool(HF_TOKEN), "len:", len(HF_TOKEN) if HF_TOKEN else 0)
20
+
21
+ if HF_TOKEN:
22
+ try:
23
+ login(token=HF_TOKEN, add_to_git_credential=False)
24
+ print("HF login: OK")
25
+ except Exception as e:
26
+ print("HF login: FAILED:", str(e)[:200])
27
+ else:
28
+ print("HF login: skipped (no token in env)")
29
+
30
+ _client = None
31
+
32
+
33
+ def reset_client():
34
+ global _client
35
+ _client = None
36
+
37
+
38
+ def get_client():
39
+ """
40
+ Keep it compatible with old gradio_client (no hf_token=...).
41
+ We rely on huggingface_hub login() above.
42
+ """
43
+ global _client
44
+ if _client is None:
45
+ _client = Client(SPACE)
46
+ return _client
47
+
48
+
49
+ # ---------------- IO helpers ----------------
50
+
51
+ def clamp_int(x, lo, hi):
52
+ try:
53
+ x = int(x)
54
+ except Exception:
55
+ x = lo
56
+ return max(lo, min(hi, x))
57
+
58
+
59
+ def clamp_float(x, lo, hi):
60
+ try:
61
+ x = float(x)
62
+ except Exception:
63
+ x = lo
64
+ return max(lo, min(hi, x))
65
+
66
+
67
+ def pil_save_temp(
68
+ pil_img: Image.Image,
69
+ suffix: str = ".jpg",
70
+ max_side: int = 1400,
71
+ quality: int = 92
72
+ ) -> str:
73
+ """
74
+ Reduce upload timeouts:
75
+ - convert to RGB
76
+ - downscale to max_side
77
+ - save JPEG optimized
78
+ """
79
+ img = pil_img.convert("RGB")
80
+ w, h = img.size
81
+ m = max(w, h)
82
+ if m > max_side:
83
+ scale = max_side / m
84
+ img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS)
85
+
86
+ f = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
87
+ path = f.name
88
+ f.close()
89
+ img.save(path, format="JPEG", quality=int(quality), optimize=True)
90
+ return path
91
+
92
+
93
+ # ---------------- Quality / Mode ----------------
94
+
95
+ def apply_mode_defaults(mode: str, denoise_steps: int, seed: int, crop_center: bool, garment_desc: str):
96
+ denoise_steps = clamp_int(denoise_steps, 10, 40)
97
+ seed = clamp_int(seed, 0, 999999)
98
+
99
+ if mode == "Balanced":
100
+ denoise_steps = max(denoise_steps, 28)
101
+ elif mode == "Quality":
102
+ denoise_steps = max(denoise_steps, 38)
103
+ elif mode == "Text/Logo":
104
+ denoise_steps = 40
105
+ crop_center = False
106
+ if seed == 0:
107
+ seed = 42
108
+ if garment_desc and "text" not in garment_desc.lower() and "logo" not in garment_desc.lower():
109
+ garment_desc = garment_desc.strip() + ", with clear text/logo print"
110
+
111
+ return denoise_steps, seed, crop_center, garment_desc
112
+
113
+
114
+ # ---------------- Overlay (preserve print/texture) ----------------
115
+
116
+ def _to_np(img: Image.Image) -> np.ndarray:
117
+ return np.asarray(img.convert("RGB"), dtype=np.float32) / 255.0
118
+
119
+
120
+ def _to_pil(arr: np.ndarray) -> Image.Image:
121
+ arr = np.clip(arr * 255.0, 0, 255).astype(np.uint8)
122
+ return Image.fromarray(arr, mode="RGB")
123
+
124
+
125
+ def make_auto_mask_from_garment(garment_rgb: Image.Image) -> Image.Image:
126
+ """
127
+ Heuristic mask for print/text: edge density + center focus.
128
+ """
129
+ g = garment_rgb.convert("L")
130
+ edges = g.filter(ImageFilter.FIND_EDGES).filter(ImageFilter.GaussianBlur(radius=1.2))
131
+ arr = np.asarray(edges, dtype=np.uint8)
132
+
133
+ m = float(arr.mean())
134
+ s = float(arr.std())
135
+ thr = int(min(255, max(25, m + 0.7 * s)))
136
+
137
+ mask = (arr > thr).astype(np.uint8) * 255
138
+ mask_img = Image.fromarray(mask, mode="L").filter(ImageFilter.GaussianBlur(radius=2.0))
139
+
140
+ w, h = mask_img.size
141
+ focus = Image.new("L", (w, h), 0)
142
+ fx0, fy0, fx1, fy1 = int(w * 0.18), int(h * 0.12), int(w * 0.82), int(h * 0.88)
143
+ focus.paste(255, (fx0, fy0, fx1, fy1))
144
+ focus = focus.filter(ImageFilter.GaussianBlur(radius=12.0))
145
+
146
+ mask_arr = (np.asarray(mask_img, dtype=np.float32) / 255.0) * (np.asarray(focus, dtype=np.float32) / 255.0)
147
+ return Image.fromarray(np.clip(mask_arr * 255.0, 0, 255).astype(np.uint8), mode="L")
148
+
149
+
150
+ def apply_overlay_preserve(
151
+ out_rgb: Image.Image,
152
+ garment_rgb: Image.Image,
153
+ strength: float,
154
+ detail_strength: float,
155
+ box_scale: float,
156
+ offset_x: int,
157
+ offset_y: int,
158
+ use_drawn_mask: bool,
159
+ garment_mask_draw: Image.Image | None,
160
+ ) -> Image.Image:
161
+ """
162
+ Blend garment colors + high-frequency detail into torso region.
163
+ If a drawn mask is provided, it wins (best for exact logos/text).
164
+ """
165
+ strength = clamp_float(strength, 0.0, 1.0)
166
+ detail_strength = clamp_float(detail_strength, 0.0, 2.0)
167
+ box_scale = clamp_float(box_scale, 0.6, 1.4)
168
+
169
+ if strength <= 0.0 and detail_strength <= 0.0:
170
+ return out_rgb
171
+
172
+ out = out_rgb.convert("RGB")
173
+ g = garment_rgb.convert("RGB")
174
+
175
+ ow, oh = out.size
176
+
177
+ # approximate torso box
178
+ x0 = int(ow * 0.22)
179
+ x1 = int(ow * 0.78)
180
+ y0 = int(oh * 0.20)
181
+ y1 = int(oh * 0.78)
182
+
183
+ bw, bh = (x1 - x0), (y1 - y0)
184
+
185
+ cx = (x0 + x1) // 2 + int(offset_x)
186
+ cy = (y0 + y1) // 2 + int(offset_y)
187
+ bw2 = int(bw * box_scale)
188
+ bh2 = int(bh * box_scale)
189
+
190
+ x0b = max(0, cx - bw2 // 2)
191
+ x1b = min(ow, cx + bw2 // 2)
192
+ y0b = max(0, cy - bh2 // 2)
193
+ y1b = min(oh, cy + bh2 // 2)
194
+
195
+ box_w, box_h = (x1b - x0b), (y1b - y0b)
196
+ if box_w <= 10 or box_h <= 10:
197
+ return out
198
+
199
+ g_fit = g.resize((box_w, box_h), Image.LANCZOS)
200
+
201
+ # mask
202
+ if use_drawn_mask and garment_mask_draw is not None:
203
+ gm = garment_mask_draw
204
+ if gm.mode == "RGBA":
205
+ mask = gm.split()[-1]
206
+ else:
207
+ mask = gm.convert("L")
208
+ mask = mask.resize((box_w, box_h), Image.LANCZOS).filter(ImageFilter.GaussianBlur(radius=2.0))
209
+ else:
210
+ mask = make_auto_mask_from_garment(g).resize((box_w, box_h), Image.LANCZOS)
211
+
212
+ m = np.asarray(mask, dtype=np.float32) / 255.0
213
+ m = np.clip(m, 0.0, 1.0)
214
+
215
+ out_arr = _to_np(out)
216
+ crop = out_arr[y0b:y1b, x0b:x1b, :]
217
+
218
+ g_arr = _to_np(g_fit)
219
+
220
+ # 1) color/print transfer
221
+ if strength > 0:
222
+ alpha = strength * (m[..., None])
223
+ crop = crop * (1.0 - alpha) + g_arr * alpha
224
+
225
+ # 2) texture/detail transfer
226
+ if detail_strength > 0:
227
+ g_blur = _to_np(g_fit.filter(ImageFilter.GaussianBlur(radius=3.0)))
228
+ detail = g_arr - g_blur
229
+ alpha2 = detail_strength * (m[..., None]) * 0.65
230
+ crop = np.clip(crop + detail * alpha2, 0.0, 1.0)
231
+
232
+ out_arr[y0b:y1b, x0b:x1b, :] = crop
233
+ return _to_pil(out_arr)
234
+
235
+
236
+ # ---------------- Main inference ----------------
237
+
238
+ def tryon_remote(
239
+ person_pil,
240
+ garment_pil,
241
+ garment_desc,
242
+ auto_mask,
243
+ crop_center,
244
+ denoise_steps,
245
+ seed,
246
+ mode_quality,
247
+ overlay_enable,
248
+ overlay_strength,
249
+ overlay_detail,
250
+ overlay_box_scale,
251
+ overlay_offset_x,
252
+ overlay_offset_y,
253
+ use_garment_mask_draw,
254
+ garment_mask_draw,
255
+ ):
256
+ if person_pil is None:
257
+ return None, "❌ Загрузите фото человека"
258
+ if garment_pil is None:
259
+ return None, "❌ Загрузите одежду"
260
+
261
+ denoise_steps, seed, crop_center, garment_desc = apply_mode_defaults(
262
+ mode=mode_quality,
263
+ denoise_steps=denoise_steps,
264
+ seed=seed,
265
+ crop_center=crop_center,
266
+ garment_desc=garment_desc or "a t-shirt"
267
+ )
268
+
269
+ # Save smaller for upload stability (still good enough for text with overlay)
270
+ p_path = pil_save_temp(person_pil, ".jpg", max_side=1400, quality=92)
271
+ g_path = pil_save_temp(garment_pil, ".jpg", max_side=1400, quality=95)
272
+
273
+ try:
274
+ last_err = None
275
+
276
+ # more attempts to survive ZeroGPU queue
277
+ for attempt in range(1, 7):
278
+ try:
279
+ client = get_client()
280
+
281
+ result = client.predict(
282
+ dict={"background": handle_file(p_path), "layers": [], "composite": None},
283
+ garm_img=handle_file(g_path),
284
+ garment_des=garment_desc,
285
+ is_checked=bool(auto_mask),
286
+ is_checked_crop=bool(crop_center),
287
+ denoise_steps=int(denoise_steps),
288
+ seed=int(seed),
289
+ api_name=API_NAME
290
+ )
291
+
292
+ if isinstance(result, (list, tuple)):
293
+ result = result[0]
294
+
295
+ out = Image.open(result).convert("RGB")
296
+
297
+ if overlay_enable:
298
+ out = apply_overlay_preserve(
299
+ out_rgb=out,
300
+ garment_rgb=garment_pil.convert("RGB"),
301
+ strength=float(overlay_strength),
302
+ detail_strength=float(overlay_detail),
303
+ box_scale=float(overlay_box_scale),
304
+ offset_x=int(overlay_offset_x),
305
+ offset_y=int(overlay_offset_y),
306
+ use_drawn_mask=bool(use_garment_mask_draw),
307
+ garment_mask_draw=garment_mask_draw,
308
+ )
309
+
310
+ return out, (
311
+ f"✅ Готово | mode={mode_quality}, steps={denoise_steps}, seed={seed}, crop={crop_center} | "
312
+ f"overlay={'on' if overlay_enable else 'off'}"
313
+ )
314
+
315
+ except Exception as e:
316
+ last_err = e
317
+ msg = str(e).lower()
318
+
319
+ is_timeout = ("write operation timed out" in msg) or ("timed out" in msg)
320
+ is_queue = ("queue" in msg) or ("too busy" in msg) or ("overloaded" in msg) or ("capacity" in msg)
321
+ is_zerogpu = ("zerogpu" in msg)
322
+
323
+ # Handle queue/timeouts: reset client and wait longer
324
+ if is_timeout or is_queue or is_zerogpu:
325
+ reset_client()
326
+
327
+ # escalate waiting
328
+ wait_s = 6.0 * attempt # 6s, 12s, 18s, ...
329
+ # on later attempts, compress more to reduce upload chance of timeout
330
+ if attempt >= 3:
331
+ try:
332
+ os.remove(p_path); os.remove(g_path)
333
+ except Exception:
334
+ pass
335
+ p_path = pil_save_temp(person_pil, ".jpg", max_side=1100, quality=85)
336
+ g_path = pil_save_temp(garment_pil, ".jpg", max_side=1100, quality=88)
337
+
338
+ time.sleep(wait_s)
339
+ continue
340
+
341
+ # generic backoff
342
+ time.sleep(1.2 * attempt)
343
+
344
+ return None, f"❌ Ошибка Space: {str(last_err)[:250]}"
345
+
346
+ finally:
347
+ for path in (p_path, g_path):
348
+ try:
349
+ os.remove(path)
350
+ except Exception:
351
+ pass
352
+
353
+
354
+ def reset_ui():
355
+ return None, None, None, "Ожидание..."
356
+
357
+
358
+ # ---------------- UI ----------------
359
+
360
+ CUSTOM_CSS = """
361
+ footer {display:none !important;}
362
+ #api-info {display:none !important;}
363
+ div[class*="footer"] {display:none !important;}
364
+ button[aria-label="Settings"] {display:none !important;}
365
+ """
366
+
367
+ with gr.Blocks(title="Virtual Try-On Rendez-vous") as demo:
368
+ gr.Markdown(
369
+ "# Virtual Try-On Rendez-vous\n\n"
370
+ "**Максимум по тексту/фактуре:** выбери *Text/Logo* и включи *Overlay*.\n"
371
+ "Если авто-маска промахивается — включи *Use garment mask (draw)* и нарисуй маску по тексту/логотипу."
372
+ )
373
+
374
+ with gr.Row():
375
+ with gr.Column():
376
+ person = gr.Image(label="Фото человека", type="pil", height=420)
377
+ garment = gr.Image(label="Одежда", type="pil", height=320)
378
+
379
+ garment_desc = gr.Textbox(
380
+ label="Описание одежды (чем точнее — тем лучше)",
381
+ value="a t-shirt with clear text/logo print"
382
+ )
383
+
384
+ mode_quality = gr.Radio(
385
+ ["Balanced", "Quality", "Text/Logo"],
386
+ value="Text/Logo",
387
+ label="Режим качества"
388
+ )
389
+
390
+ with gr.Accordion("Настройки генерации", open=False):
391
+ auto_mask = gr.Checkbox(label="Auto-mask (Space)", value=True)
392
+ crop_center = gr.Checkbox(label="Crop по центру", value=True)
393
+ denoise_steps = gr.Slider(10, 40, value=28, step=1, label="Denoise steps")
394
+ seed = gr.Slider(0, 999999, value=42, step=1, label="Seed")
395
+
396
+ with gr.Accordion("Сохранение текста/текстуры (Overlay)", open=True):
397
+ overlay_enable = gr.Checkbox(label="Enable Overlay", value=True)
398
+ overlay_strength = gr.Slider(0, 1, value=0.55, step=0.01, label="Overlay strength (цвет/принт)")
399
+ overlay_detail = gr.Slider(0, 2, value=1.10, step=0.05, label="Overlay detail (фактура/буквы)")
400
+ overlay_box_scale = gr.Slider(0.6, 1.4, value=1.0, step=0.02, label="Overlay box scale")
401
+ overlay_offset_x = gr.Slider(-300, 300, value=0, step=1, label="Overlay offset X (px)")
402
+ overlay_offset_y = gr.Slider(-300, 300, value=0, step=1, label="Overlay offset Y (px)")
403
+
404
+ use_garment_mask_draw = gr.Checkbox(
405
+ label="Use garment mask (draw) — нарисуй маску по тексту/принту",
406
+ value=False
407
+ )
408
+ garment_mask_draw = gr.Image(
409
+ label="Garment mask (опционально): нарисуй белым по тексту/принту",
410
+ type="pil",
411
+ height=260,
412
+ )
413
+
414
+ with gr.Row():
415
+ run = gr.Button("Примерить", variant="primary")
416
+ reset = gr.Button("Сбросить", variant="secondary")
417
+
418
+ status = gr.Textbox(value="Ожидание...", interactive=False)
419
+
420
+ with gr.Column():
421
+ out = gr.Image(label="Результат", type="pil", height=760)
422
+
423
+ run.click(
424
+ fn=tryon_remote,
425
+ inputs=[
426
+ person, garment, garment_desc,
427
+ auto_mask, crop_center, denoise_steps, seed, mode_quality,
428
+ overlay_enable, overlay_strength, overlay_detail, overlay_box_scale,
429
+ overlay_offset_x, overlay_offset_y,
430
+ use_garment_mask_draw, garment_mask_draw
431
+ ],
432
+ outputs=[out, status]
433
+ )
434
+
435
+ reset.click(
436
+ fn=reset_ui,
437
+ inputs=[],
438
+ outputs=[person, garment, out, status]
439
+ )
440
+
441
+ if __name__ == "__main__":
442
+ demo.launch(
443
+ server_name="0.0.0.0",
444
+ server_port=7860, # Spaces default
445
+ share=False,
446
+ debug=False,
447
+ css=CUSTOM_CSS,
448
+ ssr_mode=False
449
+ )