prithivMLmods commited on
Commit
b14cb72
·
verified ·
1 Parent(s): 2b2693b

update app

Browse files
Files changed (1) hide show
  1. app.py +81 -1163
app.py CHANGED
@@ -1,13 +1,14 @@
1
  import os
2
  import gc
3
  import gradio as gr
 
 
4
  import numpy as np
5
  import spaces
6
  import torch
7
  import random
8
  import base64
9
  import json
10
- import html as html_lib
11
  from io import BytesIO
12
  from PIL import Image
13
  from transformers import pipeline as hf_pipeline
@@ -52,14 +53,10 @@ except Exception as e:
52
  print(f"Warning: Could not set FA3 processor: {e}")
53
 
54
  # ── NCII safety guard ────────────────────────────────────────────────────────
55
- # https://huggingface.co/hfmlsoc/ncii-light-guard-v01
56
- # Classifies the edit prompt for non-consensual intimate imagery (NCII) intent
57
- # before any image generation happens. Runs on CPU — it's a light text
58
- # classifier and doesn't need the GPU slot reserved for the diffusion pipeline.
59
  NCII_MODEL_ID = "hfmlsoc/ncii-light-guard-v01"
60
  NCII_UNSAFE_LABEL = "ncii"
61
  NCII_THRESHOLD = 0.5
62
- NCII_BLOCK_MESSAGE = "You entered prompt is NCII (non-consensual intimate imagery) and you're request not be processed. Try with Safe Prompts"
63
 
64
  print("Loading NCII safety guard model...")
65
  try:
@@ -69,17 +66,7 @@ except Exception as e:
69
  ncii_guard = None
70
  print(f"Warning: Could not load NCII guard model ({NCII_MODEL_ID}): {e}")
71
 
72
-
73
  def check_ncii_safety(prompt_text):
74
- """
75
- Runs the NCII guard classifier on the prompt.
76
- Returns (is_unsafe: bool, label: str, score: float).
77
-
78
- Fails OPEN (treats the prompt as safe) if the guard model itself failed
79
- to load or errors at inference time, so a guard outage doesn't take the
80
- whole app down. Flip this to fail-closed (return True on failure) if you
81
- want stricter behavior for a production deployment.
82
- """
83
  if ncii_guard is None or not prompt_text or not prompt_text.strip():
84
  return False, "unknown", 0.0
85
  try:
@@ -94,7 +81,6 @@ def check_ncii_safety(prompt_text):
94
  print(f"NCII guard inference error: {e}")
95
  return False, "unknown", 0.0
96
 
97
-
98
  EXAMPLES_CONFIG = [
99
  {
100
  "images": ["examples/1.jpg"],
@@ -122,7 +108,6 @@ EXAMPLES_CONFIG = [
122
  },
123
  ]
124
 
125
-
126
  def make_thumb_b64(path, max_dim=220):
127
  if not os.path.exists(path):
128
  return ""
@@ -136,7 +121,6 @@ def make_thumb_b64(path, max_dim=220):
136
  print(f"Thumbnail error for {path}: {e}")
137
  return ""
138
 
139
-
140
  def encode_full_image(path):
141
  if not os.path.exists(path):
142
  return ""
@@ -150,51 +134,22 @@ def encode_full_image(path):
150
  print(f"Encode error for {path}: {e}")
151
  return ""
152
 
153
-
154
- def build_example_cards_html():
155
- cards = ""
156
  for i, ex in enumerate(EXAMPLES_CONFIG):
157
- thumbs_html = ""
158
- for path in ex["images"]:
159
- thumb = make_thumb_b64(path)
160
- if thumb:
161
- thumbs_html += f'<img src="{thumb}" alt="">'
162
- else:
163
- thumbs_html += '<div class="example-thumb-placeholder">Preview</div>'
164
- n = len(ex["images"])
165
- badge = f'{n} image{"s" if n > 1 else ""}'
166
- prompt_short = html_lib.escape(ex["prompt"][:90])
167
- if len(ex["prompt"]) > 90:
168
- prompt_short += "..."
169
- cards += f'''<div class="example-card" data-idx="{i}">
170
- <div class="example-thumbs">{thumbs_html}</div>
171
- <div class="example-meta"><span class="example-badge">{badge}</span></div>
172
- <div class="example-prompt-text">{prompt_short}</div>
173
- </div>'''
174
- return cards
175
-
176
-
177
- def load_example_data(idx_str):
178
- try:
179
- idx = int(float(idx_str)) if idx_str and idx_str.strip() else -1
180
- except (ValueError, TypeError):
181
- idx = -1
182
- if idx < 0 or idx >= len(EXAMPLES_CONFIG):
183
- return json.dumps({"images": [], "prompt": "", "names": [], "status": "error"})
184
- ex = EXAMPLES_CONFIG[idx]
185
- b64_list, names = [], []
186
- for path in ex["images"]:
187
- b64 = encode_full_image(path)
188
- if b64:
189
- b64_list.append(b64)
190
- names.append(os.path.basename(path))
191
- return json.dumps({"images": b64_list, "prompt": ex["prompt"], "names": names, "status": "ok"})
192
-
193
-
194
- print("Building example thumbnails...")
195
- EXAMPLE_CARDS_HTML = build_example_cards_html()
196
- print(f"Built {len(EXAMPLES_CONFIG)} example cards.")
197
 
 
 
 
198
 
199
  def b64_to_pil_list(b64_json_str):
200
  if not b64_json_str or b64_json_str.strip() in ("", "[]"):
@@ -218,6 +173,10 @@ def b64_to_pil_list(b64_json_str):
218
  print(f"Error decoding image: {e}")
219
  return pil_images
220
 
 
 
 
 
221
 
222
  def update_dimensions_on_upload(image):
223
  if image is None:
@@ -231,1132 +190,91 @@ def update_dimensions_on_upload(image):
231
  nw = int(nh * w / h)
232
  return (nw // 8) * 8, (nh // 8) * 8
233
 
 
 
234
 
 
 
235
  @spaces.GPU(size="xlarge")
236
- def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps, progress=gr.Progress(track_tqdm=True)):
 
 
 
 
 
 
 
 
 
 
 
237
  gc.collect()
238
  torch.cuda.empty_cache()
 
239
  pil_images = b64_to_pil_list(images_b64_json)
240
  if not pil_images:
241
- gr.Warning("Please upload at least one image to edit.")
242
- return None, seed, "error"
243
  if not prompt or prompt.strip() == "":
244
- gr.Warning("Please enter an edit prompt.")
245
- return None, seed, "error"
246
 
247
- # ── NCII safety check ──
248
  is_unsafe, _, _ = check_ncii_safety(prompt)
249
  if is_unsafe:
250
- gr.Warning(NCII_BLOCK_MESSAGE)
251
- gc.collect()
252
- torch.cuda.empty_cache()
253
- return None, seed, "blocked"
254
 
255
  if randomize_seed:
256
  seed = random.randint(0, MAX_SEED)
 
257
  generator = torch.Generator(device=device).manual_seed(seed)
258
  negative_prompt = "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
259
  width, height = update_dimensions_on_upload(pil_images[0])
 
260
  try:
261
  result_image = pipe(
262
- image=pil_images, prompt=prompt, negative_prompt=negative_prompt,
263
- height=height, width=width, num_inference_steps=steps,
264
- generator=generator, true_cfg_scale=guidance_scale,
 
 
 
 
 
265
  ).images[0]
266
- return result_image, seed, "success"
267
  except Exception as e:
268
- print(f"Generation error: {e}")
269
- return None, seed, "error"
270
  finally:
271
  gc.collect()
272
  torch.cuda.empty_cache()
273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
- css = r"""
276
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
277
- *{box-sizing:border-box;margin:0;padding:0}
278
- body,.gradio-container{
279
- background:#0f0f13!important;font-family:'Inter',system-ui,-apple-system,sans-serif!important;
280
- font-size:14px!important;color:#e4e4e7!important;min-height:100vh;
281
- }
282
- .dark body,.dark .gradio-container{background:#0f0f13!important;color:#e4e4e7!important}
283
- footer{display:none!important}
284
- .hidden-input{display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important}
285
-
286
- #example-load-btn{
287
- position:absolute!important;left:-9999px!important;top:-9999px!important;
288
- width:1px!important;height:1px!important;opacity:0.01!important;
289
- pointer-events:none!important;overflow:hidden!important;
290
- }
291
- #gradio-run-btn{
292
- position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;
293
- opacity:0.01;pointer-events:none;overflow:hidden;
294
- }
295
-
296
- /* ── App shell ── */
297
- .app-shell{
298
- background:#18181b;border:1px solid #27272a;border-radius:16px;
299
- margin:12px auto;max-width:1400px;overflow:hidden;
300
- box-shadow:0 25px 50px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.03);
301
- }
302
-
303
- /* ── Header ── */
304
- .app-header{
305
- background:linear-gradient(135deg,#18181b,#1e1e24);border-bottom:1px solid #27272a;
306
- padding:14px 24px;display:flex;align-items:center;justify-content:space-between;
307
- flex-wrap:wrap;gap:12px;
308
- }
309
- .app-header-left{display:flex;align-items:center;gap:12px}
310
- .app-logo{
311
- width:36px;height:36px;background:linear-gradient(135deg,#FF0000,#FF3333,#FF8080);
312
- border-radius:10px;display:flex;align-items:center;justify-content:center;
313
- box-shadow:0 4px 12px rgba(255,0,0,.35);flex-shrink:0;
314
- }
315
- .app-logo svg{width:20px;height:20px;fill:#fff;flex-shrink:0}
316
- .app-title{
317
- font-size:18px;font-weight:700;background:linear-gradient(135deg,#e4e4e7,#a1a1aa);
318
- -webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.3px;
319
- }
320
- .app-badge{
321
- font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px;
322
- background:rgba(255,0,0,.15);color:#FF3333;border:1px solid rgba(255,0,0,.25);letter-spacing:.3px;
323
- }
324
- .app-badge.fast{background:rgba(34,197,94,.12);color:#4ade80;border:1px solid rgba(34,197,94,.25)}
325
-
326
- /* ── GitHub button ── */
327
- .gh-btn{
328
- display:inline-flex!important;align-items:center!important;gap:7px!important;
329
- padding:7px 16px!important;border-radius:8px!important;text-decoration:none!important;
330
- font-family:'Inter',sans-serif!important;font-size:13px!important;font-weight:700!important;
331
- letter-spacing:.1px!important;background:#FF0000!important;
332
- color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
333
- border:1px solid rgba(255,255,255,.18)!important;
334
- box-shadow:0 2px 10px rgba(255,0,0,.45),0 1px 0 rgba(255,255,255,.1) inset!important;
335
- transition:transform .15s ease,box-shadow .15s ease,background .15s ease!important;
336
- cursor:pointer!important;flex-shrink:0!important;
337
- }
338
- .gh-btn:hover{
339
- background:#FF3333!important;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
340
- transform:translateY(-1px)!important;
341
- box-shadow:0 5px 18px rgba(255,0,0,.6),0 1px 0 rgba(255,255,255,.12) inset!important;
342
- }
343
- .gh-btn:active{
344
- background:#CC0000!important;transform:translateY(0)!important;
345
- box-shadow:0 1px 5px rgba(255,0,0,.35)!important;
346
- }
347
- .gh-btn svg{fill:#ffffff!important;flex-shrink:0;width:15px!important;height:15px!important}
348
- .gh-btn span{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
349
-
350
- /* ── Toolbar ── */
351
- .app-toolbar{
352
- background:#18181b;border-bottom:1px solid #27272a;padding:8px 16px;
353
- display:flex;gap:4px;align-items:center;flex-wrap:wrap;
354
- }
355
- .tb-sep{width:1px;height:28px;background:#27272a;margin:0 8px}
356
- .modern-tb-btn{
357
- display:inline-flex;align-items:center;justify-content:center;gap:6px;
358
- min-width:32px;height:34px;background:transparent;border:1px solid transparent;
359
- border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;padding:0 12px;
360
- font-family:'Inter',sans-serif;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
361
- transition:all .15s ease;
362
- }
363
- .modern-tb-btn:hover{background:rgba(255,0,0,.15);border-color:rgba(255,0,0,.3)}
364
- .modern-tb-btn:active,.modern-tb-btn.active{background:rgba(255,0,0,.25);border-color:rgba(255,0,0,.45)}
365
- .modern-tb-btn .tb-label{font-size:13px;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;font-weight:600}
366
- .modern-tb-btn .tb-svg{width:15px;height:15px;flex-shrink:0;color:#ffffff!important}
367
- .modern-tb-btn .tb-svg,
368
- .modern-tb-btn .tb-svg *{stroke:#ffffff!important;fill:none!important}
369
- .tb-info{font-family:'JetBrains Mono',monospace;font-size:12px;color:#71717a;padding:0 8px;display:flex;align-items:center}
370
-
371
- body:not(.dark) .modern-tb-btn,body:not(.dark) .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
372
- body:not(.dark) .modern-tb-btn .tb-svg,body:not(.dark) .modern-tb-btn .tb-svg *{stroke:#ffffff!important}
373
- .dark .modern-tb-btn,.dark .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
374
- .dark .modern-tb-btn .tb-svg,.dark .modern-tb-btn .tb-svg *{stroke:#ffffff!important}
375
- .gradio-container .modern-tb-btn,.gradio-container .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
376
- .gradio-container .modern-tb-btn .tb-svg,.gradio-container .modern-tb-btn .tb-svg *{stroke:#ffffff!important}
377
-
378
- /* ── Main layout ── */
379
- .app-main-row{display:flex;gap:0;flex:1;overflow:hidden}
380
- .app-main-left{flex:1;display:flex;flex-direction:column;min-width:0;border-right:1px solid #27272a}
381
- .app-main-right{width:420px;display:flex;flex-direction:column;flex-shrink:0;background:#18181b}
382
-
383
- /* ── Drop zone ── */
384
- #gallery-drop-zone{position:relative;background:#09090b;min-height:440px;overflow:auto}
385
- #gallery-drop-zone.drag-over{outline:2px solid #FF0000;outline-offset:-2px;background:rgba(255,0,0,.04)}
386
-
387
- .upload-prompt-modern{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:20}
388
- .upload-click-area{
389
- display:flex;flex-direction:column;align-items:center;justify-content:center;
390
- cursor:pointer;padding:36px 52px;border:2px dashed #3f3f46;border-radius:16px;
391
- background:rgba(255,0,0,.03);transition:all .2s ease;gap:8px;
392
- }
393
- .upload-click-area:hover{background:rgba(255,0,0,.08);border-color:#FF0000;transform:scale(1.03)}
394
- .upload-click-area:active{background:rgba(255,0,0,.12);transform:scale(.98)}
395
- .upload-click-area svg{width:80px;height:80px}
396
- .upload-main-text{color:#71717a;font-size:14px;font-weight:500;margin-top:4px}
397
- .upload-sub-text{color:#52525b;font-size:12px;text-align:center;max-width:280px;line-height:1.5}
398
-
399
- /* ── Gallery grid ── */
400
- .image-gallery-grid{
401
- display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
402
- gap:12px;padding:16px;align-content:start;
403
- }
404
- .gallery-thumb{
405
- position:relative;aspect-ratio:1;border-radius:10px;overflow:hidden;
406
- cursor:pointer;border:2px solid #27272a;transition:all .2s ease;background:#18181b;
407
- }
408
- .gallery-thumb:hover{border-color:#3f3f46;transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.4)}
409
- .gallery-thumb.selected{border-color:#FF0000!important;box-shadow:0 0 0 3px rgba(255,0,0,.2)}
410
- .gallery-thumb img{width:100%;height:100%;object-fit:cover}
411
- .thumb-badge{
412
- position:absolute;top:6px;left:6px;background:#FF0000;color:#fff;
413
- padding:2px 8px;border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:600;
414
- }
415
- .thumb-remove{
416
- position:absolute;top:6px;right:6px;width:24px;height:24px;background:rgba(0,0,0,.75);
417
- color:#fff;border:1px solid rgba(255,255,255,.15);border-radius:50%;cursor:pointer;
418
- display:none;align-items:center;justify-content:center;font-size:12px;transition:all .15s;line-height:1;
419
- }
420
- .gallery-thumb:hover .thumb-remove{display:flex}
421
- .thumb-remove:hover{background:#FF0000;border-color:#FF0000}
422
- .gallery-add-card{
423
- aspect-ratio:1;border-radius:10px;border:2px dashed #3f3f46;
424
- display:flex;flex-direction:column;align-items:center;justify-content:center;
425
- cursor:pointer;transition:all .2s ease;background:rgba(255,0,0,.03);gap:4px;
426
- }
427
- .gallery-add-card:hover{border-color:#FF0000;background:rgba(255,0,0,.08)}
428
- .gallery-add-card .add-icon{font-size:28px;color:#71717a;font-weight:300}
429
- .gallery-add-card .add-text{font-size:12px;color:#71717a;font-weight:500}
430
-
431
- /* ── Hint bar ── */
432
- .hint-bar{
433
- background:rgba(255,0,0,.06);border-top:1px solid #27272a;border-bottom:1px solid #27272a;
434
- padding:10px 20px;font-size:13px;color:#a1a1aa;line-height:1.7;
435
- }
436
- .hint-bar b{color:#FF8080;font-weight:600}
437
- .hint-bar kbd{
438
- display:inline-block;padding:1px 6px;background:#27272a;border:1px solid #3f3f46;
439
- border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;color:#a1a1aa;
440
- }
441
-
442
- /* ── Suggestions ── */
443
- .suggestions-section{border-top:1px solid #27272a;padding:12px 16px}
444
- .suggestions-title,.examples-title{
445
- font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;
446
- letter-spacing:.8px;margin-bottom:10px;
447
- }
448
- .suggestions-wrap{display:flex;flex-wrap:wrap;gap:6px}
449
- .suggestion-chip{
450
- display:inline-flex;align-items:center;gap:4px;padding:5px 12px;
451
- background:rgba(255,0,0,.08);border:1px solid rgba(255,0,0,.2);border-radius:20px;
452
- color:#FF8080;font-size:12px;font-weight:500;font-family:'Inter',sans-serif;
453
- cursor:pointer;transition:all .15s;white-space:nowrap;
454
- }
455
- .suggestion-chip:hover{background:rgba(255,0,0,.15);border-color:rgba(255,0,0,.35);color:#FF3333;transform:translateY(-1px)}
456
-
457
- /* ── Examples ── */
458
- .examples-section{border-top:1px solid #27272a;padding:12px 16px}
459
- .examples-scroll{display:flex;gap:10px;overflow-x:auto;padding-bottom:8px}
460
- .examples-scroll::-webkit-scrollbar{height:6px}
461
- .examples-scroll::-webkit-scrollbar-track{background:#09090b;border-radius:3px}
462
- .examples-scroll::-webkit-scrollbar-thumb{background:#27272a;border-radius:3px}
463
- .examples-scroll::-webkit-scrollbar-thumb:hover{background:#3f3f46}
464
- .example-card{
465
- flex-shrink:0;width:210px;background:#09090b;border:1px solid #27272a;
466
- border-radius:10px;overflow:hidden;cursor:pointer;transition:all .2s ease;
467
- }
468
- .example-card:hover{border-color:#FF0000;transform:translateY(-2px);box-shadow:0 4px 12px rgba(255,0,0,.15)}
469
- .example-card.loading{opacity:.5;pointer-events:none}
470
- .example-thumbs{display:flex;height:110px;overflow:hidden;background:#18181b}
471
- .example-thumbs img{flex:1;object-fit:cover;min-width:0;border-bottom:1px solid #27272a}
472
- .example-thumb-placeholder{
473
- flex:1;display:flex;align-items:center;justify-content:center;
474
- background:#18181b;color:#3f3f46;font-size:11px;min-width:0;
475
- }
476
- .example-meta{padding:6px 10px;display:flex;align-items:center;gap:6px}
477
- .example-badge{
478
- display:inline-flex;padding:2px 7px;background:rgba(255,0,0,.1);border-radius:4px;
479
- font-size:10px;font-weight:600;color:#FF3333;font-family:'JetBrains Mono',monospace;white-space:nowrap;
480
- }
481
- .example-prompt-text{
482
- padding:0 10px 8px;font-size:11px;color:#a1a1aa;line-height:1.4;
483
- display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
484
- }
485
-
486
- /* ── Right panel ── */
487
- .panel-card{border-bottom:1px solid #27272a}
488
- .panel-card-title{
489
- padding:12px 20px;font-size:12px;font-weight:600;color:#71717a;
490
- text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6);
491
- }
492
- .panel-card-body{padding:16px 20px;display:flex;flex-direction:column;gap:8px}
493
- .modern-label{font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block}
494
- .modern-textarea{
495
- width:100%;background:#09090b;border:1px solid #27272a;border-radius:8px;
496
- padding:10px 14px;font-family:'Inter',sans-serif;font-size:14px;color:#e4e4e7;
497
- resize:vertical;outline:none;min-height:42px;transition:border-color .2s;
498
- }
499
- .modern-textarea:focus{border-color:#FF0000;box-shadow:0 0 0 3px rgba(255,0,0,.15)}
500
- .modern-textarea::placeholder{color:#3f3f46}
501
- .modern-textarea.error-flash{
502
- border-color:#ef4444!important;box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;animation:shake .4s ease;
503
- }
504
- @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}}
505
-
506
- /* ── Toast ── */
507
- .toast-notification{
508
- position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-120%);
509
- z-index:9999;padding:10px 24px;border-radius:10px;font-family:'Inter',sans-serif;
510
- font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px;
511
- box-shadow:0 8px 24px rgba(0,0,0,.5);
512
- transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none;
513
- }
514
- .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}
515
- .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)}
516
- .toast-notification.warning{background:linear-gradient(135deg,#d97706,#b45309);color:#fff;border:1px solid rgba(255,255,255,.15)}
517
- .toast-notification.info{background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;border:1px solid rgba(255,255,255,.15)}
518
- .toast-notification .toast-icon{font-size:16px;line-height:1}
519
- .toast-notification .toast-text{line-height:1.3}
520
-
521
- /* ── Run button ── */
522
- .btn-run{
523
- display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
524
- background:linear-gradient(135deg,#FF0000,#CC0000);border:none;border-radius:10px;
525
- padding:12px 24px;cursor:pointer;font-size:15px;font-weight:600;font-family:'Inter',sans-serif;
526
- color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;transition:all .2s ease;letter-spacing:-.2px;
527
- box-shadow:0 4px 16px rgba(255,0,0,.3),inset 0 1px 0 rgba(255,255,255,.1);
528
- }
529
- .btn-run:hover{
530
- background:linear-gradient(135deg,#FF3333,#FF0000);transform:translateY(-1px);
531
- box-shadow:0 6px 24px rgba(255,0,0,.45),inset 0 1px 0 rgba(255,255,255,.15);
532
- }
533
- .btn-run:active{transform:translateY(0);box-shadow:0 2px 8px rgba(255,0,0,.3)}
534
- .btn-run svg{width:18px;height:18px;fill:#ffffff!important}
535
- .btn-run svg path{fill:#ffffff!important}
536
- #custom-run-btn,#custom-run-btn *,#custom-run-btn span,#custom-run-btn svg,
537
- #custom-run-btn svg path,#run-btn-label,.btn-run,.btn-run *{
538
- color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
539
- }
540
- body:not(.dark) .btn-run,body:not(.dark) .btn-run *,body:not(.dark) #custom-run-btn,
541
- body:not(.dark) #custom-run-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important}
542
- .dark .btn-run,.dark .btn-run *,.dark #custom-run-btn,.dark #custom-run-btn *{
543
- color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
544
- }
545
- .gradio-container .btn-run,.gradio-container .btn-run *,.gradio-container #custom-run-btn,
546
- .gradio-container #custom-run-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important}
547
-
548
- /* ── Output ── */
549
- .output-frame{border-bottom:1px solid #27272a;display:flex;flex-direction:column;position:relative}
550
- .output-frame .out-title{
551
- padding:10px 20px;font-size:13px;font-weight:700;color:#ffffff!important;
552
- -webkit-text-fill-color:#ffffff!important;text-transform:uppercase;letter-spacing:.8px;
553
- border-bottom:1px solid rgba(39,39,42,.6);display:flex;align-items:center;justify-content:space-between;
554
- }
555
- .output-frame .out-title span{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
556
- .output-frame .out-body{
557
- flex:1;background:#09090b;display:flex;align-items:center;justify-content:center;
558
- overflow:hidden;min-height:240px;position:relative;
559
- }
560
- .output-frame .out-body img{max-width:100%;max-height:460px;image-rendering:auto}
561
- .output-frame .out-placeholder{color:#3f3f46;font-size:13px;text-align:center;padding:20px}
562
- .out-download-btn{
563
- display:none;align-items:center;justify-content:center;background:rgba(255,0,0,.1);
564
- border:1px solid rgba(255,0,0,.2);border-radius:6px;cursor:pointer;padding:3px 10px;
565
- font-size:11px;font-weight:500;color:#FF8080!important;gap:4px;height:24px;transition:all .15s;
566
- }
567
- .out-download-btn:hover{background:rgba(255,0,0,.2);border-color:rgba(255,0,0,.35);color:#ffffff!important}
568
- .out-download-btn.visible{display:inline-flex}
569
- .out-download-btn svg{width:12px;height:12px;fill:#FF8080}
570
-
571
- /* ── Loader ── */
572
- .modern-loader{
573
- display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(9,9,11,.92);
574
- z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(4px);
575
- }
576
- .modern-loader.active{display:flex}
577
- .modern-loader .loader-spinner{
578
- width:36px;height:36px;border:3px solid #27272a;border-top-color:#FF0000;
579
- border-radius:50%;animation:spin .8s linear infinite;
580
- }
581
- @keyframes spin{to{transform:rotate(360deg)}}
582
- .modern-loader .loader-text{font-size:13px;color:#a1a1aa;font-weight:500}
583
- .loader-bar-track{width:200px;height:4px;background:#27272a;border-radius:2px;overflow:hidden}
584
- .loader-bar-fill{
585
- height:100%;background:linear-gradient(90deg,#FF0000,#FF3333,#FF0000);
586
- background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px;
587
- }
588
- @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
589
-
590
- /* ── Settings ── */
591
- .settings-group{border:1px solid #27272a;border-radius:10px;margin:12px 16px;padding:0;overflow:hidden}
592
- .settings-group-title{
593
- font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;
594
- padding:10px 16px;border-bottom:1px solid #27272a;background:rgba(24,24,27,.5);
595
- }
596
- .settings-group-body{padding:14px 16px;display:flex;flex-direction:column;gap:12px}
597
- .slider-row{display:flex;align-items:center;gap:10px;min-height:28px}
598
- .slider-row label{font-size:13px;font-weight:500;color:#a1a1aa;min-width:72px;flex-shrink:0}
599
- .slider-row input[type="range"]{
600
- flex:1;-webkit-appearance:none;appearance:none;height:6px;background:#27272a;
601
- border-radius:3px;outline:none;min-width:0;
602
- }
603
- .slider-row input[type="range"]::-webkit-slider-thumb{
604
- -webkit-appearance:none;width:16px;height:16px;background:linear-gradient(135deg,#FF0000,#CC0000);
605
- border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(255,0,0,.4);transition:transform .15s;
606
- }
607
- .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)}
608
- .slider-row input[type="range"]::-moz-range-thumb{
609
- width:16px;height:16px;background:linear-gradient(135deg,#FF0000,#CC0000);
610
- border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(255,0,0,.4);
611
- }
612
- .slider-row .slider-val{
613
- min-width:52px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:12px;
614
- font-weight:500;padding:3px 8px;background:#09090b;border:1px solid #27272a;
615
- border-radius:6px;color:#a1a1aa;flex-shrink:0;
616
- }
617
- .checkbox-row{display:flex;align-items:center;gap:8px;font-size:13px;color:#a1a1aa}
618
- .checkbox-row input[type="checkbox"]{accent-color:#FF0000;width:16px;height:16px;cursor:pointer}
619
- .checkbox-row label{color:#a1a1aa;font-size:13px;cursor:pointer}
620
-
621
- /* ── Status bar ── */
622
- .app-statusbar{
623
- background:#18181b;border-top:1px solid #27272a;padding:6px 20px;
624
- display:flex;gap:12px;height:34px;align-items:center;font-size:12px;
625
- }
626
- .app-statusbar .sb-section{
627
- padding:0 12px;flex:1;display:flex;align-items:center;font-family:'JetBrains Mono',monospace;
628
- font-size:12px;color:#52525b;overflow:hidden;white-space:nowrap;
629
- }
630
- .app-statusbar .sb-section.sb-fixed{
631
- flex:0 0 auto;min-width:90px;text-align:center;justify-content:center;
632
- padding:3px 12px;background:rgba(255,0,0,.08);border-radius:6px;color:#FF3333;font-weight:500;
633
- }
634
-
635
- /* ── Footer note ── */
636
- .exp-note{
637
- padding:10px 20px;font-size:12px;color:#52525b;
638
- border-top:1px solid #27272a;text-align:center;font-weight:500;
639
- background:#18181b;font-family:'Inter',sans-serif;
640
- }
641
- .exp-note a{color:#FF3333;text-decoration:none}
642
- .exp-note a:hover{text-decoration:underline}
643
-
644
- /* ── Dark overrides ── */
645
- .dark .app-shell{background:#18181b}
646
- .dark .upload-prompt-modern{background:transparent}
647
- .dark .panel-card{background:#18181b}
648
- .dark .settings-group{background:#18181b}
649
- .dark .output-frame .out-title{color:#ffffff!important}
650
- .dark .output-frame .out-title span{color:#ffffff!important}
651
- .dark .out-download-btn{color:#FF8080!important}
652
- .dark .out-download-btn:hover{color:#ffffff!important}
653
-
654
- /* ── Scrollbars ── */
655
- ::-webkit-scrollbar{width:8px;height:8px}
656
- ::-webkit-scrollbar-track{background:#09090b}
657
- ::-webkit-scrollbar-thumb{background:#27272a;border-radius:4px}
658
- ::-webkit-scrollbar-thumb:hover{background:#3f3f46}
659
-
660
- /* ── Responsive ── */
661
- @media(max-width:840px){
662
- .app-main-row{flex-direction:column}
663
- .app-main-right{width:100%}
664
- .app-main-left{border-right:none;border-bottom:1px solid #27272a}
665
- }
666
- """
667
-
668
- gallery_js = r"""
669
- () => {
670
- function init() {
671
- if (window.__fireRedInitDone) return;
672
-
673
- const galleryGrid = document.getElementById('image-gallery-grid');
674
- const dropZone = document.getElementById('gallery-drop-zone');
675
- const uploadPrompt = document.getElementById('upload-prompt');
676
- const uploadClick = document.getElementById('upload-click-area');
677
- const fileInput = document.getElementById('custom-file-input');
678
- const btnUpload = document.getElementById('tb-upload');
679
- const btnRemove = document.getElementById('tb-remove');
680
- const btnClear = document.getElementById('tb-clear');
681
- const promptInput = document.getElementById('custom-prompt-input');
682
- const runBtnEl = document.getElementById('custom-run-btn');
683
- const imgCountTb = document.getElementById('tb-image-count');
684
- const imgCountSb = document.getElementById('sb-image-count');
685
-
686
- if (!galleryGrid || !fileInput || !dropZone) {
687
- setTimeout(init, 250);
688
- return;
689
- }
690
-
691
- window.__fireRedInitDone = true;
692
-
693
- let images = [];
694
- window.__uploadedImages = images;
695
- let selectedIdx = -1;
696
- let toastTimer = null;
697
-
698
- /* ── GitHub button hover ── */
699
- function enforceGhBtn() {
700
- const ghBtn = document.querySelector('.gh-btn');
701
- if (ghBtn && !ghBtn.__hoverBound) {
702
- ghBtn.__hoverBound = true;
703
- ghBtn.addEventListener('mouseenter', () => {
704
- ghBtn.style.setProperty('background','#FF3333','important');
705
- ghBtn.style.setProperty('transform','translateY(-1px)','important');
706
- ghBtn.style.setProperty('box-shadow','0 5px 18px rgba(255,0,0,.6)','important');
707
- });
708
- ghBtn.addEventListener('mouseleave', () => {
709
- ghBtn.style.setProperty('background','#FF0000','important');
710
- ghBtn.style.setProperty('transform','translateY(0)','important');
711
- ghBtn.style.setProperty('box-shadow','0 2px 10px rgba(255,0,0,.45)','important');
712
- });
713
- ghBtn.addEventListener('mousedown', () => ghBtn.style.setProperty('background','#CC0000','important'));
714
- ghBtn.addEventListener('mouseup', () => ghBtn.style.setProperty('background','#FF3333','important'));
715
- }
716
- }
717
- enforceGhBtn();
718
- setInterval(enforceGhBtn, 1000);
719
-
720
- function showToast(message, type) {
721
- let toast = document.getElementById('app-toast');
722
- if (!toast) {
723
- toast = document.createElement('div');
724
- toast.id = 'app-toast';
725
- toast.className = 'toast-notification';
726
- toast.innerHTML = '<span class="toast-icon"></span><span class="toast-text"></span>';
727
- document.body.appendChild(toast);
728
- }
729
- const icon = toast.querySelector('.toast-icon');
730
- const text = toast.querySelector('.toast-text');
731
- toast.className = 'toast-notification ' + (type || 'error');
732
- if (type === 'warning') icon.textContent = '\u26A0';
733
- else if (type === 'info') icon.textContent = '\u2139';
734
- else icon.textContent = '\u2717';
735
- text.textContent = message;
736
- if (toastTimer) clearTimeout(toastTimer);
737
- void toast.offsetWidth;
738
- toast.classList.add('visible');
739
- toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500);
740
- }
741
- window.__showToast = showToast;
742
-
743
- function flashPromptError() {
744
- if (!promptInput) return;
745
- promptInput.classList.add('error-flash');
746
- promptInput.focus();
747
- setTimeout(() => promptInput.classList.remove('error-flash'), 800);
748
- }
749
-
750
- function setGradioValue(containerId, value) {
751
- const container = document.getElementById(containerId);
752
- if (!container) return;
753
- container.querySelectorAll('input, textarea').forEach(el => {
754
- if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return;
755
- const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
756
- const ns = Object.getOwnPropertyDescriptor(proto, 'value');
757
- if (ns && ns.set) {
758
- ns.set.call(el, value);
759
- el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
760
- el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
761
- }
762
- });
763
- }
764
- window.__setGradioValue = setGradioValue;
765
-
766
- function syncImagesToGradio() {
767
- window.__uploadedImages = images;
768
- const b64Array = images.map(img => img.b64);
769
- setGradioValue('hidden-images-b64', JSON.stringify(b64Array));
770
- updateCounts();
771
- }
772
-
773
- function syncPromptToGradio() {
774
- if (promptInput) setGradioValue('prompt-gradio-input', promptInput.value);
775
- }
776
-
777
- function updateCounts() {
778
- const n = images.length;
779
- const txt = n > 0 ? n + ' image' + (n > 1 ? 's' : '') : 'No images';
780
- if (imgCountTb) imgCountTb.textContent = txt;
781
- if (imgCountSb) imgCountSb.textContent = n > 0 ? txt + ' uploaded' : 'No images uploaded';
782
- }
783
-
784
- function addImage(b64, name) {
785
- images.push({id: Date.now() + Math.random(), b64: b64, name: name});
786
- renderGallery();
787
- syncImagesToGradio();
788
- }
789
- window.__addImage = addImage;
790
-
791
- function removeImage(idx) {
792
- images.splice(idx, 1);
793
- if (selectedIdx === idx) selectedIdx = -1;
794
- else if (selectedIdx > idx) selectedIdx--;
795
- renderGallery();
796
- syncImagesToGradio();
797
- }
798
-
799
- function clearAll() {
800
- images = [];
801
- window.__uploadedImages = images;
802
- selectedIdx = -1;
803
- renderGallery();
804
- syncImagesToGradio();
805
- }
806
- window.__clearAll = clearAll;
807
-
808
- function selectImage(idx) {
809
- selectedIdx = (selectedIdx === idx) ? -1 : idx;
810
- renderGallery();
811
- }
812
-
813
- function renderGallery() {
814
- if (images.length === 0) {
815
- galleryGrid.innerHTML = '';
816
- galleryGrid.style.display = 'none';
817
- if (uploadPrompt) uploadPrompt.style.display = '';
818
- return;
819
- }
820
- if (uploadPrompt) uploadPrompt.style.display = 'none';
821
- galleryGrid.style.display = 'grid';
822
-
823
- let html = '';
824
- images.forEach((img, i) => {
825
- const sel = i === selectedIdx ? ' selected' : '';
826
- html += '<div class="gallery-thumb' + sel + '" data-idx="' + i + '">'
827
- + '<img src="' + img.b64 + '" alt="' + (img.name||'image') + '">'
828
- + '<span class="thumb-badge">#' + (i+1) + '</span>'
829
- + '<button class="thumb-remove" data-remove="' + i + '">\u2715</button>'
830
- + '</div>';
831
- });
832
- html += '<div class="gallery-add-card" id="gallery-add-card">'
833
- + '<span class="add-icon">+</span>'
834
- + '<span class="add-text">Add</span>'
835
- + '</div>';
836
- galleryGrid.innerHTML = html;
837
-
838
- galleryGrid.querySelectorAll('.gallery-thumb').forEach(thumb => {
839
- thumb.addEventListener('click', (e) => {
840
- if (e.target.closest('.thumb-remove')) return;
841
- selectImage(parseInt(thumb.dataset.idx));
842
- });
843
- });
844
- galleryGrid.querySelectorAll('.thumb-remove').forEach(btn => {
845
- btn.addEventListener('click', (e) => {
846
- e.stopPropagation();
847
- removeImage(parseInt(btn.dataset.remove));
848
- });
849
- });
850
- const addCard = document.getElementById('gallery-add-card');
851
- if (addCard) addCard.addEventListener('click', () => fileInput.click());
852
- }
853
-
854
- function processFiles(files) {
855
- Array.from(files).forEach(file => {
856
- if (!file.type.startsWith('image/')) return;
857
- const reader = new FileReader();
858
- reader.onload = (e) => addImage(e.target.result, file.name);
859
- reader.readAsDataURL(file);
860
- });
861
- }
862
-
863
- fileInput.addEventListener('change', (e) => { processFiles(e.target.files); e.target.value = ''; });
864
- if (uploadClick) uploadClick.addEventListener('click', () => fileInput.click());
865
- if (btnUpload) btnUpload.addEventListener('click', () => fileInput.click());
866
- if (btnRemove) btnRemove.addEventListener('click', () => {
867
- if (selectedIdx >= 0 && selectedIdx < images.length) removeImage(selectedIdx);
868
- });
869
- if (btnClear) btnClear.addEventListener('click', clearAll);
870
-
871
- dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
872
- dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); });
873
- dropZone.addEventListener('drop', (e) => {
874
- e.preventDefault(); dropZone.classList.remove('drag-over');
875
- if (e.dataTransfer.files.length) processFiles(e.dataTransfer.files);
876
- });
877
-
878
- if (promptInput) promptInput.addEventListener('input', syncPromptToGradio);
879
-
880
- window.__setPrompt = function(text) {
881
- if (promptInput) { promptInput.value = text; syncPromptToGradio(); }
882
- };
883
-
884
- document.querySelectorAll('.example-card[data-idx]').forEach(card => {
885
- card.addEventListener('click', () => {
886
- const idx = card.getAttribute('data-idx');
887
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
888
- card.classList.add('loading');
889
- showToast('Loading example...', 'info');
890
- setGradioValue('example-result-data', '');
891
- setGradioValue('example-idx-input', idx);
892
- setTimeout(() => {
893
- const btn = document.getElementById('example-load-btn');
894
- if (btn) {
895
- const b = btn.querySelector('button');
896
- if (b) b.click(); else btn.click();
897
- }
898
- }, 150);
899
- setTimeout(() => card.classList.remove('loading'), 12000);
900
- });
901
- });
902
-
903
- function syncSlider(customId, gradioId) {
904
- const slider = document.getElementById(customId);
905
- const valSpan = document.getElementById(customId + '-val');
906
- if (!slider) return;
907
- slider.addEventListener('input', () => {
908
- if (valSpan) valSpan.textContent = slider.value;
909
- const container = document.getElementById(gradioId);
910
- if (!container) return;
911
- container.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => {
912
- const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
913
- if (ns && ns.set) {
914
- ns.set.call(el, slider.value);
915
- el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
916
- el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
917
- }
918
- });
919
- });
920
- }
921
- syncSlider('custom-seed', 'gradio-seed');
922
- syncSlider('custom-guidance', 'gradio-guidance');
923
- syncSlider('custom-steps', 'gradio-steps');
924
-
925
- const randCheck = document.getElementById('custom-randomize');
926
- if (randCheck) {
927
- randCheck.addEventListener('change', () => {
928
- const container = document.getElementById('gradio-randomize');
929
- if (!container) return;
930
- const cb = container.querySelector('input[type="checkbox"]');
931
- if (cb && cb.checked !== randCheck.checked) cb.click();
932
- });
933
- }
934
-
935
- function showLoader() {
936
- const l = document.getElementById('output-loader');
937
- if (l) l.classList.add('active');
938
- const sb = document.querySelector('.sb-fixed');
939
- if (sb) sb.textContent = 'Processing...';
940
- }
941
- function hideLoader() {
942
- const l = document.getElementById('output-loader');
943
- if (l) l.classList.remove('active');
944
- const sb = document.querySelector('.sb-fixed');
945
- if (sb) sb.textContent = 'Done';
946
- }
947
- window.__showLoader = showLoader;
948
- window.__hideLoader = hideLoader;
949
-
950
- function validateBeforeRun() {
951
- const promptVal = promptInput ? promptInput.value.trim() : '';
952
- const hasImages = images.length > 0;
953
- if (!hasImages && !promptVal) { showToast('Please upload an image and enter a prompt', 'error'); flashPromptError(); return false; }
954
- if (!hasImages) { showToast('Please upload at least one image', 'error'); return false; }
955
- if (!promptVal) { showToast('Please enter an edit prompt', 'warning'); flashPromptError(); return false; }
956
- return true;
957
- }
958
-
959
- window.__clickGradioRunBtn = function() {
960
- if (!validateBeforeRun()) return;
961
- syncPromptToGradio(); syncImagesToGradio(); showLoader();
962
- setTimeout(() => {
963
- const gradioBtn = document.getElementById('gradio-run-btn');
964
- if (!gradioBtn) return;
965
- const btn = gradioBtn.querySelector('button');
966
- if (btn) btn.click(); else gradioBtn.click();
967
- }, 200);
968
- };
969
-
970
- if (runBtnEl) runBtnEl.addEventListener('click', () => window.__clickGradioRunBtn());
971
-
972
- renderGallery();
973
- updateCounts();
974
- }
975
- init();
976
- }
977
- """
978
-
979
- wire_outputs_js = r"""
980
- () => {
981
- function watchOutputs() {
982
- const resultContainer = document.getElementById('gradio-result');
983
- const outBody = document.getElementById('output-image-container');
984
- const outPh = document.getElementById('output-placeholder');
985
- const dlBtn = document.getElementById('dl-btn-output');
986
-
987
- if (!resultContainer || !outBody) { setTimeout(watchOutputs, 500); return; }
988
-
989
- if (dlBtn) {
990
- dlBtn.addEventListener('click', (e) => {
991
- e.stopPropagation();
992
- const img = outBody.querySelector('img.modern-out-img');
993
- if (img && img.src) {
994
- const a = document.createElement('a');
995
- a.href = img.src; a.download = 'firered_output.png';
996
- document.body.appendChild(a); a.click(); document.body.removeChild(a);
997
- }
998
- });
999
- }
1000
-
1001
- function syncImage() {
1002
- const resultImg = resultContainer.querySelector('img');
1003
- // Only hide the loader and show the image if a valid, non-blank image is present.
1004
- // Gradio sometimes uses a 'blank.png' placeholder internally.
1005
- if (resultImg && resultImg.src && !resultImg.src.endsWith('blank.png')) {
1006
- if (outPh) outPh.style.display = 'none';
1007
- let existing = outBody.querySelector('img.modern-out-img');
1008
- if (!existing) {
1009
- existing = document.createElement('img');
1010
- existing.className = 'modern-out-img';
1011
- outBody.appendChild(existing);
1012
- }
1013
- if (existing.src !== resultImg.src) {
1014
- existing.src = resultImg.src;
1015
- if (dlBtn) dlBtn.classList.add('visible');
1016
- if (window.__hideLoader) window.__hideLoader();
1017
- }
1018
- }
1019
- }
1020
- const observer = new MutationObserver(syncImage);
1021
- observer.observe(resultContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']});
1022
- setInterval(syncImage, 800);
1023
- }
1024
- watchOutputs();
1025
-
1026
- function watchProcessState() {
1027
- const stateContainer = document.getElementById('gradio-process-state');
1028
- if (!stateContainer) { setTimeout(watchProcessState, 500); return; }
1029
-
1030
- let lastState = 'idle';
1031
-
1032
- function checkState() {
1033
- const el = stateContainer.querySelector('textarea') || stateContainer.querySelector('input');
1034
- if (!el) return;
1035
- const val = el.value;
1036
- if (val !== lastState) {
1037
- lastState = val;
1038
- // If the state is 'blocked' or 'error', hide the loader and reset the output view.
1039
- if (val === 'blocked' || val === 'error') {
1040
- if (window.__hideLoader) window.__hideLoader();
1041
- const outBody = document.getElementById('output-image-container');
1042
- const existing = outBody.querySelector('img.modern-out-img');
1043
- if (existing) existing.remove();
1044
- const dlBtn = document.getElementById('dl-btn-output');
1045
- if (dlBtn) dlBtn.classList.remove('visible');
1046
- const outPh = document.getElementById('output-placeholder');
1047
- if (outPh) outPh.style.display = '';
1048
-
1049
- if (val === 'error' && window.__showToast) {
1050
- window.__showToast('An error occurred during generation. Please try again.', 'error');
1051
- }
1052
- }
1053
-
1054
- // Reset the state to 'idle' so it can trigger again on the next run
1055
- if (val === 'blocked' || val === 'error' || val === 'success') {
1056
- el.value = 'idle';
1057
- el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
1058
- el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
1059
- }
1060
- }
1061
- }
1062
-
1063
- const obs = new MutationObserver(checkState);
1064
- obs.observe(stateContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']});
1065
- setInterval(checkState, 500);
1066
- }
1067
- watchProcessState();
1068
-
1069
- function watchSeed() {
1070
- const seedContainer = document.getElementById('gradio-seed');
1071
- const seedSlider = document.getElementById('custom-seed');
1072
- const seedVal = document.getElementById('custom-seed-val');
1073
- if (!seedContainer || !seedSlider) { setTimeout(watchSeed, 500); return; }
1074
- function sync() {
1075
- const el = seedContainer.querySelector('input[type="range"],input[type="number"]');
1076
- if (el && el.value) { seedSlider.value = el.value; if (seedVal) seedVal.textContent = el.value; }
1077
- }
1078
- const obs = new MutationObserver(sync);
1079
- obs.observe(seedContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']});
1080
- setInterval(sync, 1000);
1081
- }
1082
- watchSeed();
1083
-
1084
- function watchExampleResults() {
1085
- const container = document.getElementById('example-result-data');
1086
- if (!container) { setTimeout(watchExampleResults, 500); return; }
1087
-
1088
- let lastProcessed = '';
1089
-
1090
- function checkResult() {
1091
- const el = container.querySelector('textarea') || container.querySelector('input');
1092
- if (!el) return;
1093
- const val = el.value;
1094
- if (!val || val === lastProcessed || val.length < 20) return;
1095
-
1096
- try {
1097
- const data = JSON.parse(val);
1098
- if (data.status === 'ok' && data.images && data.images.length > 0) {
1099
- lastProcessed = val;
1100
- if (window.__clearAll) window.__clearAll();
1101
- if (window.__setPrompt && data.prompt) window.__setPrompt(data.prompt);
1102
- data.images.forEach((b64, i) => {
1103
- if (b64 && window.__addImage) {
1104
- const name = (data.names && data.names[i]) ? data.names[i] : ('example_' + (i+1) + '.jpg');
1105
- window.__addImage(b64, name);
1106
- }
1107
- });
1108
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1109
- if (window.__showToast) window.__showToast('Example loaded \u2014 ' + data.images.length + ' image(s)', 'info');
1110
- } else if (data.status === 'error') {
1111
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1112
- if (window.__showToast) window.__showToast('Could not load example images', 'error');
1113
- }
1114
- } catch(e) {
1115
- console.error('Example parse error:', e);
1116
- }
1117
- }
1118
-
1119
- const obs = new MutationObserver(checkResult);
1120
- obs.observe(container, {childList:true, subtree:true, characterData:true, attributes:true});
1121
- setInterval(checkResult, 500);
1122
- }
1123
- watchExampleResults();
1124
- }
1125
- """
1126
-
1127
- # ── SVG assets ─────────────────────────────────────────────────────────────────
1128
- DOWNLOAD_SVG = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 16l-5-5h3V4h4v7h3l-5 5z"/><path d="M20 18H4v2h16v-2z"/></svg>'
1129
-
1130
- UPLOAD_SVG = '<svg class="tb-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>'
1131
-
1132
- REMOVE_SVG = '<svg class="tb-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>'
1133
-
1134
- CLEAR_SVG = '<svg class="tb-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>'
1135
-
1136
- GITHUB_SVG = '<svg width="15" height="15" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path fill="#ffffff" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>'
1137
-
1138
- FIRE_LOGO_SVG = '<svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M12 23c-3.6 0-8-2.69-8-7.5 0-3.5 3-6.5 4.5-8 .27-.27.75-.08.75.28v2.44c0 .42.5.63.72.28C12.28 7.5 13 3 13 1c0-.42.48-.64.8-.35C18 4.5 20 9 20 12c0 5.5-3.5 11-8 11z"/></svg>'
1139
-
1140
- # ── Gradio app ─────────────────────────────────────────────────────────────────
1141
- with gr.Blocks() as demo:
1142
-
1143
- hidden_images_b64 = gr.Textbox(value="[]", elem_id="hidden-images-b64", elem_classes="hidden-input", container=False)
1144
- prompt = gr.Textbox(value="", elem_id="prompt-gradio-input", elem_classes="hidden-input", container=False)
1145
- seed = gr.Slider(minimum=0, maximum=MAX_SEED, step=1, value=0, elem_id="gradio-seed", elem_classes="hidden-input", container=False)
1146
- randomize_seed = gr.Checkbox(value=True, elem_id="gradio-randomize", elem_classes="hidden-input", container=False)
1147
- guidance_scale = gr.Slider(minimum=1.0, maximum=10.0, step=0.1, value=1.0, elem_id="gradio-guidance", elem_classes="hidden-input", container=False)
1148
- steps = gr.Slider(minimum=1, maximum=50, step=1, value=4, elem_id="gradio-steps", elem_classes="hidden-input", container=False)
1149
- result = gr.Image(elem_id="gradio-result", elem_classes="hidden-input", container=False, format="png")
1150
- process_state = gr.Textbox(value="idle", elem_id="gradio-process-state", elem_classes="hidden-input", container=False)
1151
-
1152
- example_idx = gr.Textbox(value="", elem_id="example-idx-input", elem_classes="hidden-input", container=False)
1153
- example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False)
1154
- example_load_btn = gr.Button("Load Example", elem_id="example-load-btn")
1155
-
1156
- gr.HTML(f"""
1157
- <div class="app-shell">
1158
-
1159
- <!-- Header with GitHub top-right -->
1160
- <div class="app-header">
1161
- <div class="app-header-left">
1162
- <div class="app-logo">{FIRE_LOGO_SVG}</div>
1163
- <span class="app-title">FireRed-Image-Edit</span>
1164
- <span class="app-badge">v1.1</span>
1165
- <span class="app-badge fast">4-Step Fast</span>
1166
- </div>
1167
- <a href="https://github.com/PRITHIVSAKTHIUR/FireRed-Image-Edit-1.0-Fast"
1168
- target="_blank" class="gh-btn">
1169
- {GITHUB_SVG}
1170
- <span>GitHub</span>
1171
- </a>
1172
- </div>
1173
-
1174
- <!-- Toolbar -->
1175
- <div class="app-toolbar">
1176
- <button id="tb-upload" class="modern-tb-btn" title="Upload images">
1177
- {UPLOAD_SVG}<span class="tb-label">Upload</span>
1178
- </button>
1179
- <button id="tb-remove" class="modern-tb-btn" title="Remove selected image">
1180
- {REMOVE_SVG}<span class="tb-label">Remove</span>
1181
- </button>
1182
- <button id="tb-clear" class="modern-tb-btn" title="Clear all images">
1183
- {CLEAR_SVG}<span class="tb-label">Clear All</span>
1184
- </button>
1185
- <div class="tb-sep"></div>
1186
- <span id="tb-image-count" class="tb-info">No images</span>
1187
- </div>
1188
-
1189
- <!-- Main row -->
1190
- <div class="app-main-row">
1191
-
1192
- <!-- Left panel -->
1193
- <div class="app-main-left">
1194
- <div id="gallery-drop-zone">
1195
- <div id="upload-prompt" class="upload-prompt-modern">
1196
- <div id="upload-click-area" class="upload-click-area">
1197
- <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
1198
- <rect x="8" y="14" width="64" height="52" rx="6" fill="none"
1199
- stroke="#FF0000" stroke-width="2" stroke-dasharray="4 3"/>
1200
- <polygon points="12,62 30,40 42,50 54,34 68,62"
1201
- fill="rgba(255,0,0,0.15)" stroke="#FF0000" stroke-width="1.5"/>
1202
- <circle cx="28" cy="30" r="6"
1203
- fill="rgba(255,0,0,0.2)" stroke="#FF0000" stroke-width="1.5"/>
1204
- </svg>
1205
- <span class="upload-main-text">Click or drag images here</span>
1206
- <span class="upload-sub-text">Supports multiple images for reference-based editing and guided manipulation</span>
1207
- </div>
1208
- </div>
1209
- <input id="custom-file-input" type="file" accept="image/*" multiple style="display:none;" />
1210
- <div id="image-gallery-grid" class="image-gallery-grid" style="display:none;"></div>
1211
- </div>
1212
-
1213
- <div class="hint-bar">
1214
- <b>Upload:</b> Click or drag to add images &nbsp;&middot;&nbsp;
1215
- <b>Multi-image:</b> Upload multiple images for reference-based editing &nbsp;&middot;&nbsp;
1216
- <kbd>Remove</kbd> deletes selected &nbsp;&middot;&nbsp;
1217
- <kbd>Clear All</kbd> removes everything
1218
- </div>
1219
-
1220
- <div class="suggestions-section">
1221
- <div class="suggestions-title">Quick Prompts</div>
1222
- <div class="suggestions-wrap">
1223
- <button class="suggestion-chip" onclick="window.__setPrompt('Transform the image into a dotted cartoon style.')">Cartoon Style</button>
1224
- <button class="suggestion-chip" onclick="window.__setPrompt('Convert it to black and white.')">Black and White</button>
1225
- <button class="suggestion-chip" onclick="window.__setPrompt('Add cinematic lighting with warm orange tones and film grain.')">Cinematic</button>
1226
- <button class="suggestion-chip" onclick="window.__setPrompt('Transform into anime style illustration.')">Anime Style</button>
1227
- <button class="suggestion-chip" onclick="window.__setPrompt('Apply oil painting effect with visible brush strokes.')">Oil Painting</button>
1228
- <button class="suggestion-chip" onclick="window.__setPrompt('Enhance and upscale with more detail and clarity.')">Enhance</button>
1229
- <button class="suggestion-chip" onclick="window.__setPrompt('Make it look like a watercolor painting with soft edges.')">Watercolor</button>
1230
- <button class="suggestion-chip" onclick="window.__setPrompt('Add dramatic sunset sky and warm lighting.')">Sunset Glow</button>
1231
- <button class="suggestion-chip" onclick="window.__setPrompt('Convert to detailed pencil sketch with cross-hatching and shading.')">Pencil Sketch</button>
1232
- <button class="suggestion-chip" onclick="window.__setPrompt('Apply pop art style with bold colors and halftone patterns.')">Pop Art</button>
1233
- <button class="suggestion-chip" onclick="window.__setPrompt('Apply a vintage retro film look with faded colors and light leaks.')">Vintage Retro</button>
1234
- <button class="suggestion-chip" onclick="window.__setPrompt('Add neon glow effects with vibrant colors against a dark background.')">Neon Glow</button>
1235
- <button class="suggestion-chip" onclick="window.__setPrompt('Convert to pixel art style with a retro 16-bit aesthetic.')">Pixel Art</button>
1236
- <button class="suggestion-chip" onclick="window.__setPrompt('Simplify into a clean minimalist illustration with flat colors.')">Minimalist</button>
1237
- <button class="suggestion-chip" onclick="window.__setPrompt('Convert to low poly 3D geometric art style.')">Low Poly 3D</button>
1238
- <button class="suggestion-chip" onclick="window.__setPrompt('Transform into comic book style with bold outlines and cel shading.')">Comic Book</button>
1239
- </div>
1240
- </div>
1241
-
1242
- <div class="examples-section">
1243
- <div class="examples-title">Quick Examples &mdash; click to load</div>
1244
- <div class="examples-scroll">
1245
- {EXAMPLE_CARDS_HTML}
1246
- </div>
1247
- </div>
1248
- </div>
1249
-
1250
- <!-- Right panel -->
1251
- <div class="app-main-right">
1252
- <div class="panel-card">
1253
- <div class="panel-card-title">Edit Instruction</div>
1254
- <div class="panel-card-body">
1255
- <label class="modern-label" for="custom-prompt-input">Prompt</label>
1256
- <textarea id="custom-prompt-input" class="modern-textarea" rows="3"
1257
- placeholder="e.g., transform into anime, upscale, change lighting..."></textarea>
1258
- </div>
1259
- </div>
1260
-
1261
- <div style="padding:12px 20px;">
1262
- <button id="custom-run-btn" class="btn-run">
1263
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="18" height="18">
1264
- <path d="M12 23c-3.6 0-8-2.69-8-7.5 0-3.5 3-6.5 4.5-8 .27-.27.75-.08.75.28v2.44c0 .42.5.63.72.28C12.28 7.5 13 3 13 1c0-.42.48-.64.8-.35C18 4.5 20 9 20 12c0 5.5-3.5 11-8 11z" fill="white"/>
1265
- </svg>
1266
- <span id="run-btn-label">Edit Image</span>
1267
- </button>
1268
- </div>
1269
-
1270
- <div class="output-frame" style="flex:1">
1271
- <div class="out-title">
1272
- <span>Output</span>
1273
- <span id="dl-btn-output" class="out-download-btn" title="Download">
1274
- {DOWNLOAD_SVG} Save
1275
- </span>
1276
- </div>
1277
- <div class="out-body" id="output-image-container">
1278
- <div class="modern-loader" id="output-loader">
1279
- <div class="loader-spinner"></div>
1280
- <div class="loader-text">Processing image...</div>
1281
- <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1282
- </div>
1283
- <div class="out-placeholder" id="output-placeholder">Result will appear here</div>
1284
- </div>
1285
- </div>
1286
-
1287
- <div class="settings-group">
1288
- <div class="settings-group-title">Advanced Settings</div>
1289
- <div class="settings-group-body">
1290
- <div class="slider-row">
1291
- <label>Seed</label>
1292
- <input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0">
1293
- <span class="slider-val" id="custom-seed-val">0</span>
1294
- </div>
1295
- <div class="checkbox-row">
1296
- <input type="checkbox" id="custom-randomize" checked>
1297
- <label for="custom-randomize">Randomize seed</label>
1298
- </div>
1299
- <div class="slider-row">
1300
- <label>Guidance</label>
1301
- <input type="range" id="custom-guidance" min="1" max="10" step="0.1" value="1.0">
1302
- <span class="slider-val" id="custom-guidance-val">1.0</span>
1303
- </div>
1304
- <div class="slider-row">
1305
- <label>Steps</label>
1306
- <input type="range" id="custom-steps" min="1" max="50" step="1" value="4">
1307
- <span class="slider-val" id="custom-steps-val">4</span>
1308
- </div>
1309
- </div>
1310
- </div>
1311
- </div>
1312
- </div>
1313
-
1314
- <!-- Footer: only model credit, no GitHub link -->
1315
- <div class="exp-note">
1316
- Experimental Space for
1317
- <a href="https://huggingface.co/FireRedTeam/FireRed-Image-Edit-1.1" target="_blank">FireRed-Image-Edit-1.1</a>
1318
- </div>
1319
-
1320
- <!-- Status bar -->
1321
- <div class="app-statusbar">
1322
- <div class="sb-section" id="sb-image-count">No images uploaded</div>
1323
- <div class="sb-section sb-fixed">Ready</div>
1324
- </div>
1325
-
1326
- </div><!-- /app-shell -->
1327
- """)
1328
-
1329
- run_btn = gr.Button("Run", elem_id="gradio-run-btn")
1330
-
1331
- demo.load(fn=None, js=gallery_js)
1332
- demo.load(fn=None, js=wire_outputs_js)
1333
-
1334
- run_btn.click(
1335
- fn=infer,
1336
- inputs=[hidden_images_b64, prompt, seed, randomize_seed, guidance_scale, steps],
1337
- outputs=[result, seed, process_state],
1338
- js=r"""(imgs, p, s, rs, gs, st) => {
1339
- const images = window.__uploadedImages || [];
1340
- const b64Array = images.map(img => img.b64);
1341
- const imgsJson = JSON.stringify(b64Array);
1342
- const promptEl = document.getElementById('custom-prompt-input');
1343
- const promptVal = promptEl ? promptEl.value : p;
1344
- return [imgsJson, promptVal, s, rs, gs, st];
1345
- }""",
1346
- )
1347
 
1348
- example_load_btn.click(
1349
- fn=load_example_data,
1350
- inputs=[example_idx],
1351
- outputs=[example_result],
1352
- queue=False,
1353
- )
1354
 
1355
  if __name__ == "__main__":
1356
- demo.queue(max_size=50).launch(
1357
- css=css,
1358
- mcp_server=True,
1359
- ssr_mode=False,
1360
- show_error=True,
1361
- allowed_paths=["examples"],
1362
- )
 
1
  import os
2
  import gc
3
  import gradio as gr
4
+ from gradio import Server
5
+ from fastapi.responses import HTMLResponse
6
  import numpy as np
7
  import spaces
8
  import torch
9
  import random
10
  import base64
11
  import json
 
12
  from io import BytesIO
13
  from PIL import Image
14
  from transformers import pipeline as hf_pipeline
 
53
  print(f"Warning: Could not set FA3 processor: {e}")
54
 
55
  # ── NCII safety guard ────────────────────────────────────────────────────────
 
 
 
 
56
  NCII_MODEL_ID = "hfmlsoc/ncii-light-guard-v01"
57
  NCII_UNSAFE_LABEL = "ncii"
58
  NCII_THRESHOLD = 0.5
59
+ NCII_BLOCK_MESSAGE = "Your entered prompt is flagged as NCII (non-consensual intimate imagery) and your request will not be processed. Try with safe prompts."
60
 
61
  print("Loading NCII safety guard model...")
62
  try:
 
66
  ncii_guard = None
67
  print(f"Warning: Could not load NCII guard model ({NCII_MODEL_ID}): {e}")
68
 
 
69
  def check_ncii_safety(prompt_text):
 
 
 
 
 
 
 
 
 
70
  if ncii_guard is None or not prompt_text or not prompt_text.strip():
71
  return False, "unknown", 0.0
72
  try:
 
81
  print(f"NCII guard inference error: {e}")
82
  return False, "unknown", 0.0
83
 
 
84
  EXAMPLES_CONFIG = [
85
  {
86
  "images": ["examples/1.jpg"],
 
108
  },
109
  ]
110
 
 
111
  def make_thumb_b64(path, max_dim=220):
112
  if not os.path.exists(path):
113
  return ""
 
121
  print(f"Thumbnail error for {path}: {e}")
122
  return ""
123
 
 
124
  def encode_full_image(path):
125
  if not os.path.exists(path):
126
  return ""
 
134
  print(f"Encode error for {path}: {e}")
135
  return ""
136
 
137
+ def build_client_config():
138
+ examples = []
 
139
  for i, ex in enumerate(EXAMPLES_CONFIG):
140
+ examples.append({
141
+ "idx": i,
142
+ "thumbs": [make_thumb_b64(p) for p in ex["images"]],
143
+ "n_images": len(ex["images"]),
144
+ "prompt": ex["prompt"],
145
+ })
146
+ return {
147
+ "examples": examples,
148
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
150
+ print("Building client config (example thumbnails)…")
151
+ CLIENT_CONFIG = build_client_config()
152
+ print(f"Built config with {len(EXAMPLES_CONFIG)} examples.")
153
 
154
  def b64_to_pil_list(b64_json_str):
155
  if not b64_json_str or b64_json_str.strip() in ("", "[]"):
 
173
  print(f"Error decoding image: {e}")
174
  return pil_images
175
 
176
+ def pil_to_b64_png(image: Image.Image) -> str:
177
+ buf = BytesIO()
178
+ image.save(buf, format="PNG")
179
+ return f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"
180
 
181
  def update_dimensions_on_upload(image):
182
  if image is None:
 
190
  nw = int(nh * w / h)
191
  return (nw // 8) * 8, (nh // 8) * 8
192
 
193
+ # ── Gradio Server (Server mode): FastAPI + Gradio queue/API engine ────────────
194
+ app = Server(title="FireRed-Image-Edit-1.0-Fast")
195
 
196
+ @app.mcp.tool(name="edit_image")
197
+ @app.api(name="edit_image")
198
  @spaces.GPU(size="xlarge")
199
+ def infer(
200
+ images_b64_json: str,
201
+ prompt: str,
202
+ seed: int,
203
+ randomize_seed: bool,
204
+ guidance_scale: float,
205
+ steps: int,
206
+ ) -> dict:
207
+ """Edit one or more images with FireRed-Image-Edit + NCII safety guard.
208
+
209
+ Returns {"image": <base64 PNG data URL>, "seed": <seed used>}.
210
+ """
211
  gc.collect()
212
  torch.cuda.empty_cache()
213
+
214
  pil_images = b64_to_pil_list(images_b64_json)
215
  if not pil_images:
216
+ raise gr.Error("Please upload at least one image to edit.")
 
217
  if not prompt or prompt.strip() == "":
218
+ raise gr.Error("Please enter an edit prompt.")
 
219
 
220
+ # NCII safety check
221
  is_unsafe, _, _ = check_ncii_safety(prompt)
222
  if is_unsafe:
223
+ raise gr.Error(NCII_BLOCK_MESSAGE)
 
 
 
224
 
225
  if randomize_seed:
226
  seed = random.randint(0, MAX_SEED)
227
+
228
  generator = torch.Generator(device=device).manual_seed(seed)
229
  negative_prompt = "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
230
  width, height = update_dimensions_on_upload(pil_images[0])
231
+
232
  try:
233
  result_image = pipe(
234
+ image=pil_images,
235
+ prompt=prompt,
236
+ negative_prompt=negative_prompt,
237
+ height=height,
238
+ width=width,
239
+ num_inference_steps=steps,
240
+ generator=generator,
241
+ true_cfg_scale=guidance_scale,
242
  ).images[0]
243
+ return {"image": pil_to_b64_png(result_image), "seed": seed}
244
  except Exception as e:
245
+ raise e
 
246
  finally:
247
  gc.collect()
248
  torch.cuda.empty_cache()
249
 
250
+ @app.api(name="load_example", queue=False)
251
+ def load_example(idx: float) -> dict:
252
+ """Return base64-encoded example images + prompt for a given example index."""
253
+ try:
254
+ i = int(idx)
255
+ except (ValueError, TypeError):
256
+ i = -1
257
+ if i < 0 or i >= len(EXAMPLES_CONFIG):
258
+ return {"images": [], "prompt": "", "names": [], "status": "error"}
259
+ ex = EXAMPLES_CONFIG[i]
260
+ b64_list, names = [], []
261
+ for path in ex["images"]:
262
+ b64 = encode_full_image(path)
263
+ if b64:
264
+ b64_list.append(b64)
265
+ names.append(os.path.basename(path))
266
+ return {"images": b64_list, "prompt": ex["prompt"], "names": names, "status": "ok"}
267
 
268
+ @app.get("/api/config")
269
+ def client_config():
270
+ """Plain FastAPI route: example card data for the frontend."""
271
+ return CLIENT_CONFIG
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
+ @app.get("/", response_class=HTMLResponse)
274
+ async def homepage():
275
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
276
+ with open(html_path, "r", encoding="utf-8") as f:
277
+ return f.read()
 
278
 
279
  if __name__ == "__main__":
280
+ app.launch(show_error=True, mcp_server=True)