prithivMLmods commited on
Commit
f88535d
·
verified ·
1 Parent(s): 739ba90

update app

Browse files
Files changed (1) hide show
  1. app.py +86 -1133
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
 
@@ -50,7 +51,7 @@ try:
50
  except Exception as e:
51
  print(f"Warning: Could not set FA3 processor: {e}")
52
 
53
- # ── LoRA adapter registry ──────────────────────────────────────────────────────
54
  ADAPTER_SPECS = {
55
  "Photo-to-Anime": {
56
  "repo": "autoweeb/Qwen-Image-Edit-2509-Photo-to-Anime",
@@ -131,7 +132,6 @@ EXAMPLES_CONFIG = [
131
  ]
132
 
133
 
134
- # ── Helpers ────────────────────────────────────────────────────────────────────
135
  def make_thumb_b64(path, max_dim=220):
136
  if not os.path.exists(path):
137
  return ""
@@ -160,41 +160,46 @@ def encode_full_image(path):
160
  return ""
161
 
162
 
163
- def build_example_cards_html():
164
- cards = ""
 
165
  for i, ex in enumerate(EXAMPLES_CONFIG):
166
- thumb = make_thumb_b64(ex["image"])
167
- thumb_html = f'<img src="{thumb}" alt="">' if thumb else '<div class="example-thumb-placeholder">Preview</div>'
168
- lora_badge = html_lib.escape(ex["lora"])
169
- prompt_short = html_lib.escape(ex["prompt"][:80])
170
- if len(ex["prompt"]) > 80:
171
- prompt_short += "…"
172
- cards += f'''<div class="example-card" data-idx="{i}">
173
- <div class="example-thumbs">{thumb_html}</div>
174
- <div class="example-meta">
175
- <span class="example-lora-badge">{lora_badge}</span>
176
- </div>
177
- <div class="example-prompt-text">{prompt_short}</div>
178
- </div>'''
179
- return cards
180
 
 
 
 
181
 
182
- def load_example_data(idx_str):
 
 
 
183
  try:
184
- idx = int(float(idx_str)) if idx_str and idx_str.strip() else -1
185
- except (ValueError, TypeError):
186
- idx = -1
187
- if idx < 0 or idx >= len(EXAMPLES_CONFIG):
188
- return json.dumps({"image": "", "prompt": "", "lora": "", "status": "error"})
189
- ex = EXAMPLES_CONFIG[idx]
190
- b64 = encode_full_image(ex["image"])
191
- return json.dumps({"image": b64, "prompt": ex["prompt"], "lora": ex["lora"],
192
- "name": os.path.basename(ex["image"]), "status": "ok" if b64 else "error"})
193
 
194
 
195
- print("Building example thumbnails…")
196
- EXAMPLE_CARDS_HTML = build_example_cards_html()
197
- print(f"Built {len(EXAMPLES_CONFIG)} example cards.")
 
198
 
199
 
200
  def update_dimensions_on_upload(image):
@@ -202,29 +207,34 @@ def update_dimensions_on_upload(image):
202
  return 1024, 1024
203
  w, h = image.size
204
  if w > h:
205
- nw, nh = 1024, int(1024 * h / w)
 
206
  else:
207
- nh, nw = 1024, int(1024 * w / h)
 
208
  return (nw // 8) * 8, (nh // 8) * 8
209
 
210
 
211
- def b64_to_pil(b64_str):
212
- if not b64_str:
213
- return None
214
- try:
215
- if b64_str.startswith("data:image"):
216
- _, data = b64_str.split(",", 1)
217
- else:
218
- data = b64_str
219
- return Image.open(BytesIO(base64.b64decode(data))).convert("RGB")
220
- except Exception as e:
221
- print(f"Error decoding image: {e}")
222
- return None
223
 
224
 
 
 
225
  @spaces.GPU(size="xlarge")
226
- def infer(image_b64, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps,
227
- progress=gr.Progress(track_tqdm=True)):
 
 
 
 
 
 
 
 
 
 
 
228
  gc.collect()
229
  torch.cuda.empty_cache()
230
 
@@ -272,7 +282,7 @@ def infer(image_b64, prompt, lora_adapter, seed, randomize_seed, guidance_scale,
272
  generator=generator,
273
  true_cfg_scale=guidance_scale,
274
  ).images[0]
275
- return result_image, seed
276
  except Exception as e:
277
  raise e
278
  finally:
@@ -280,1095 +290,38 @@ def infer(image_b64, prompt, lora_adapter, seed, randomize_seed, guidance_scale,
280
  torch.cuda.empty_cache()
281
 
282
 
283
- # ── CSS ────────────────────────────────────────────────────────────────────────
284
- css = r"""
285
- @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');
286
-
287
- *{ box-sizing:border-box; margin:0; padding:0; }
288
-
289
- :root{
290
- /* Iris palette - #5D3FD3 theme */
291
- --pk: #5D3FD3;
292
- --pk-dim: #4A2FA8;
293
- --pk-bright:#7B5FE0;
294
- --pk-glow: rgba(93,63,211,.28);
295
- --pk-soft: rgba(93,63,211,.12);
296
- --pk-xsoft: rgba(93,63,211,.06);
297
- --bg: #0d0d12;
298
- --bg1: #14141a;
299
- --bg2: #1a1a22;
300
- --bg3: #22222c;
301
- --border: #2a2a35;
302
- --border2: #33333f;
303
- --text: #e8e8ec;
304
- --text2: #9898a8;
305
- --text3: #555560;
306
- --mono: 'JetBrains Mono', monospace;
307
- }
308
-
309
- html, body, .gradio-container {
310
- color-scheme: dark !important;
311
- background: #0d0d12 !important;
312
- }
313
- body, .gradio-container {
314
- font-family: 'Inter', system-ui, -apple-system, sans-serif !important;
315
- font-size: 14px !important;
316
- color: #e8e8ec !important;
317
- min-height: 100vh;
318
- background: #0d0d12 !important;
319
- }
320
- footer { display:none !important; }
321
- .hidden-input { display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important; }
322
-
323
- #example-load-btn, #gradio-run-btn {
324
- position:absolute!important; left:-9999px!important; top:-9999px!important;
325
- width:1px!important; height:1px!important; opacity:.01!important;
326
- pointer-events:none!important; overflow:hidden!important;
327
- }
328
-
329
- /* ── App shell ── */
330
- .app-shell {
331
- background: #14141a !important;
332
- border: 1px solid #2a2a35 !important;
333
- border-radius: 18px;
334
- margin: 12px auto;
335
- max-width: 1440px;
336
- overflow: hidden;
337
- box-shadow:
338
- 0 32px 64px -16px rgba(0,0,0,.8),
339
- 0 0 0 1px rgba(255,255,255,.03),
340
- 0 0 60px -20px rgba(93,63,211,.22);
341
- }
342
-
343
- /* ── Header ── */
344
- .app-header {
345
- background: #1a1a22 !important;
346
- border-bottom: 1px solid #2a2a35 !important;
347
- padding: 14px 24px;
348
- display: flex; align-items: center; justify-content: space-between;
349
- flex-wrap: wrap; gap: 12px;
350
- }
351
- .app-header-left { display:flex; align-items:center; gap:12px; }
352
- .app-logo {
353
- width:38px; height:38px;
354
- background: linear-gradient(135deg, #5D3FD3, #7B5FE0);
355
- border-radius: 11px;
356
- display:flex; align-items:center; justify-content:center;
357
- box-shadow: 0 4px 14px rgba(93,63,211,.35);
358
- flex-shrink:0;
359
- }
360
- .app-logo svg { width:22px; height:22px; }
361
- .app-title {
362
- font-size:18px; font-weight:800; letter-spacing:-.4px;
363
- background: linear-gradient(135deg, #fff, #aaa);
364
- -webkit-background-clip: text; -webkit-text-fill-color: transparent;
365
- font-family: 'Inter', sans-serif;
366
- }
367
- .app-badge {
368
- font-size:11px; font-weight:700; padding:3px 10px;
369
- border-radius:20px; letter-spacing:.3px;
370
- background: rgba(93,63,211,.14);
371
- color: #7B5FE0 !important; -webkit-text-fill-color: #7B5FE0 !important;
372
- border: 1px solid rgba(93,63,211,.32);
373
- font-family: 'Inter', sans-serif;
374
- }
375
- .app-badge.fast {
376
- background: rgba(34,197,94,.1);
377
- color: #4ade80 !important; -webkit-text-fill-color: #4ade80 !important;
378
- border: 1px solid rgba(34,197,94,.25);
379
- }
380
-
381
- /* ── GitHub button ── */
382
- .gh-btn {
383
- display: inline-flex !important;
384
- align-items: center !important;
385
- gap: 7px !important;
386
- padding: 7px 16px !important;
387
- border-radius: 8px !important;
388
- text-decoration: none !important;
389
- font-family: 'Inter', sans-serif !important;
390
- font-size: 13px !important;
391
- font-weight: 700 !important;
392
- letter-spacing: .1px !important;
393
- background: #5D3FD3 !important;
394
- color: #ffffff !important; -webkit-text-fill-color: #ffffff !important;
395
- border: 1px solid rgba(255,255,255,.18) !important;
396
- box-shadow: 0 2px 10px rgba(93,63,211,.4), 0 1px 0 rgba(255,255,255,.1) inset !important;
397
- transition: transform .15s ease, box-shadow .15s ease, background .15s ease !important;
398
- cursor: pointer !important; flex-shrink:0 !important;
399
- }
400
- .gh-btn:hover {
401
- background: #7B5FE0 !important;
402
- color: #ffffff !important; -webkit-text-fill-color: #ffffff !important;
403
- transform: translateY(-1px) !important;
404
- box-shadow: 0 5px 18px rgba(93,63,211,.55), 0 1px 0 rgba(255,255,255,.12) inset !important;
405
- }
406
- .gh-btn:active {
407
- background: #4A2FA8 !important;
408
- transform: translateY(0) !important;
409
- box-shadow: 0 1px 5px rgba(93,63,211,.35) !important;
410
- }
411
- .gh-btn svg { fill:#ffffff !important; flex-shrink:0; width:15px!important; height:15px!important; }
412
- .gh-btn span { color:#ffffff!important; -webkit-text-fill-color:#ffffff!important; }
413
-
414
- @media (prefers-color-scheme: light) {
415
- .gh-btn { background:#5D3FD3!important; color:#ffffff!important; -webkit-text-fill-color:#ffffff!important; }
416
- .gh-btn:hover { background:#7B5FE0!important; }
417
- .gh-btn svg { fill:#ffffff!important; }
418
- .gh-btn span { color:#ffffff!important; -webkit-text-fill-color:#ffffff!important; }
419
- }
420
- @media (prefers-color-scheme: dark) {
421
- .gh-btn { background:#5D3FD3!important; color:#ffffff!important; -webkit-text-fill-color:#ffffff!important; }
422
- .gh-btn:hover { background:#7B5FE0!important; }
423
- .gh-btn svg { fill:#ffffff!important; }
424
- .gh-btn span { color:#ffffff!important; -webkit-text-fill-color:#ffffff!important; }
425
- }
426
-
427
- /* ── Toolbar ── */
428
- .app-toolbar {
429
- background: #14141a !important;
430
- border-bottom: 1px solid #2a2a35 !important;
431
- padding: 7px 16px;
432
- display:flex; gap:4px; align-items:center; flex-wrap:wrap;
433
- }
434
- .tb-sep { width:1px; height:28px; background:#2a2a35; margin:0 8px; }
435
- .modern-tb-btn {
436
- display:inline-flex; align-items:center; justify-content:center; gap:6px;
437
- min-width:32px; height:34px; background:transparent; border:1px solid transparent;
438
- border-radius:8px; cursor:pointer; font-size:13px; font-weight:700; padding:0 12px;
439
- font-family:'Inter',sans-serif;
440
- color:#ffffff!important; -webkit-text-fill-color:#ffffff!important;
441
- transition:background .15s ease, border-color .15s ease;
442
- }
443
- .modern-tb-btn:hover { background:rgba(93,63,211,.14); border-color:rgba(93,63,211,.4); }
444
- .modern-tb-btn:active { background:rgba(93,63,211,.22); border-color:#5D3FD3; }
445
- .modern-tb-btn .tb-label { font-size:13px; color:#fff!important; -webkit-text-fill-color:#fff!important; font-weight:700; }
446
- .modern-tb-btn .tb-svg { width:15px; height:15px; flex-shrink:0; }
447
- .modern-tb-btn .tb-svg, .modern-tb-btn .tb-svg * { stroke:#fff!important; fill:none!important; }
448
- .tb-info { font-family:var(--mono); font-size:12px; color:#555560; padding:0 8px; display:flex; align-items:center; }
449
-
450
- @media (prefers-color-scheme: light) {
451
- .modern-tb-btn { color:#fff!important; -webkit-text-fill-color:#fff!important; }
452
- .modern-tb-btn .tb-label { color:#fff!important; -webkit-text-fill-color:#fff!important; }
453
- .modern-tb-btn .tb-svg, .modern-tb-btn .tb-svg * { stroke:#fff!important; }
454
- }
455
-
456
- /* ── Main layout ── */
457
- .app-main-row { display:flex; gap:0; flex:1; overflow:hidden; }
458
- .app-main-left { flex:1; display:flex; flex-direction:column; min-width:0; border-right:1px solid #2a2a35; }
459
- .app-main-right { width:440px; display:flex; flex-direction:column; flex-shrink:0; background:#14141a!important; }
460
-
461
- /* ── Drop zone ── */
462
- #gallery-drop-zone { position:relative; background:#09090e!important; min-height:440px; overflow:auto; }
463
- #gallery-drop-zone.drag-over { outline:2px solid #5D3FD3; outline-offset:-2px; background:rgba(93,63,211,.05)!important; }
464
-
465
- .upload-prompt-modern { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); z-index:20; }
466
- .upload-click-area {
467
- display:flex; flex-direction:column; align-items:center; justify-content:center;
468
- cursor:pointer; padding:36px 52px; border:2px dashed #33333f; border-radius:16px;
469
- background:rgba(93,63,211,.05); transition:background .2s ease, border-color .2s ease, transform .2s ease; gap:8px;
470
- }
471
- .upload-click-area:hover { background:rgba(93,63,211,.1); border-color:#5D3FD3; transform:scale(1.03); }
472
- .upload-click-area:active { background:rgba(93,63,211,.15); transform:scale(.98); }
473
- .upload-click-area svg { width:80px; height:80px; }
474
- .upload-main-text { color:#9898a8; font-size:14px; font-weight:600; margin-top:4px; font-family:'Inter',sans-serif; }
475
- .upload-sub-text { color:#555560; font-size:12px; font-weight:400; text-align:center; max-width:280px; line-height:1.5; font-family:'Inter',sans-serif; }
476
-
477
- /* ── Gallery grid ── */
478
- .image-gallery-grid {
479
- display:grid; grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
480
- gap:12px; padding:16px; align-content:start;
481
- }
482
- .gallery-thumb {
483
- position:relative; aspect-ratio:1; border-radius:10px; overflow:hidden;
484
- cursor:pointer; border:2px solid #2a2a35; transition:all .2s ease; background:#1a1a22;
485
- }
486
- .gallery-thumb:hover { border-color:#33333f; transform:translateY(-2px); box-shadow:0 4px 12px rgba(0,0,0,.4); }
487
- .gallery-thumb.selected { border-color:#5D3FD3!important; box-shadow:0 0 0 3px rgba(93,63,211,.25); }
488
- .gallery-thumb img { width:100%; height:100%; object-fit:cover; }
489
- .thumb-badge {
490
- position:absolute; top:6px; left:6px; background:#5D3FD3; color:#fff;
491
- padding:2px 8px; border-radius:4px; font-family:var(--mono); font-size:11px; font-weight:600;
492
- }
493
- .thumb-remove {
494
- position:absolute; top:6px; right:6px; width:24px; height:24px;
495
- background:rgba(0,0,0,.75); color:#fff; border:1px solid rgba(255,255,255,.15);
496
- border-radius:50%; cursor:pointer; display:none;
497
- align-items:center; justify-content:center; font-size:12px; transition:all .15s; line-height:1;
498
- }
499
- .gallery-thumb:hover .thumb-remove { display:flex; }
500
- .thumb-remove:hover { background:#5D3FD3; border-color:#5D3FD3; }
501
-
502
- /* ── Hint bar ── */
503
- .hint-bar {
504
- background: rgba(93,63,211,.05) !important;
505
- border-top:1px solid #2a2a35; border-bottom:1px solid #2a2a35;
506
- padding:10px 20px; font-size:13px; color:#9898a8; line-height:1.7;
507
- font-weight:400; font-family:'Inter',sans-serif;
508
- }
509
- .hint-bar b { color:#7B5FE0; font-weight:700; }
510
- .hint-bar kbd {
511
- display:inline-block; padding:1px 6px; background:#22222c;
512
- border:1px solid #33333f; border-radius:4px;
513
- font-family:var(--mono); font-size:11px; color:#9898a8;
514
- }
515
-
516
- /* ── Suggestions ── */
517
- .suggestions-section { border-top:1px solid #2a2a35; padding:12px 16px; background:#14141a!important; }
518
- .suggestions-title, .examples-title {
519
- font-size:11px; font-weight:700; color:#555560;
520
- text-transform:uppercase; letter-spacing:1px; margin-bottom:10px;
521
- font-family:'Inter',sans-serif;
522
- }
523
- .suggestions-wrap { display:flex; flex-wrap:wrap; gap:6px; }
524
- .suggestion-chip {
525
- display:inline-flex; align-items:center; gap:4px; padding:5px 13px;
526
- background:rgba(93,63,211,.1); border:1px solid rgba(93,63,211,.28); border-radius:20px;
527
- color:#7B5FE0; font-size:12px; font-weight:600; font-family:'Inter',sans-serif;
528
- cursor:pointer; transition:background .15s, border-color .15s, color .15s, transform .15s; white-space:nowrap;
529
- }
530
- .suggestion-chip:hover { background:rgba(93,63,211,.2); border-color:#5D3FD3; color:#fff; transform:translateY(-1px); }
531
-
532
- /* ── Examples ── */
533
- .examples-section { border-top:1px solid #2a2a35; padding:14px 16px 18px; background:#14141a!important; }
534
- .examples-scroll { display:flex; gap:10px; overflow-x:auto; padding-bottom:10px; padding-top:2px; }
535
- .examples-scroll::-webkit-scrollbar { height:5px; }
536
- .examples-scroll::-webkit-scrollbar-track { background:#0d0d12; border-radius:3px; }
537
- .examples-scroll::-webkit-scrollbar-thumb { background:#33333f; border-radius:3px; }
538
- .examples-scroll::-webkit-scrollbar-thumb:hover { background:#4A2FA8; }
539
- .example-card {
540
- flex-shrink:0; width:200px; background:#1a1a22!important; border:1px solid #2a2a35;
541
- border-radius:12px; overflow:hidden; cursor:pointer; transition:all .2s ease;
542
- }
543
- .example-card:hover { border-color:#5D3FD3; transform:translateY(-3px); box-shadow:0 6px 20px rgba(93,63,211,.22); }
544
- .example-card.loading { opacity:.5; pointer-events:none; }
545
- .example-thumbs { display:flex; height:115px; overflow:hidden; background:#22222c!important; }
546
- .example-thumbs img { flex:1; object-fit:cover; min-width:0; width:100%; }
547
- .example-thumb-placeholder {
548
- flex:1; display:flex; align-items:center; justify-content:center;
549
- background:#22222c!important; color:#555560; font-size:11px;
550
- }
551
- .example-meta { padding:6px 10px 3px; display:flex; align-items:center; gap:5px; flex-wrap:wrap; }
552
- .example-lora-badge {
553
- display:inline-flex; padding:2px 7px; background:rgba(93,63,211,.12); border-radius:4px;
554
- font-size:10px; font-weight:700; color:#7B5FE0; font-family:var(--mono);
555
- white-space:nowrap; border:1px solid rgba(93,63,211,.25);
556
- }
557
- .example-prompt-text {
558
- padding:2px 10px 10px; font-size:11.5px; color:#9898a8; line-height:1.45;
559
- display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
560
- overflow:hidden; font-weight:400; font-family:'Inter',sans-serif;
561
- }
562
-
563
- /* ── Right panel ── */
564
- .panel-card { border-bottom:1px solid #2a2a35; background:#14141a!important; }
565
- .panel-card-title {
566
- padding:11px 20px; font-size:11px; font-weight:700; color:#555560;
567
- text-transform:uppercase; letter-spacing:1px;
568
- border-bottom:1px solid rgba(42,42,53,.6);
569
- background:#14141a!important; font-family:'Inter',sans-serif;
570
- }
571
- .panel-card-body { padding:14px 18px; display:flex; flex-direction:column; gap:8px; background:#14141a!important; }
572
- .modern-label { font-size:13px; font-weight:600; color:#9898a8; margin-bottom:4px; display:block; font-family:'Inter',sans-serif; }
573
- .modern-textarea {
574
- width:100%; background:#09090e!important; border:1px solid #2a2a35; border-radius:8px;
575
- padding:10px 14px; font-family:'Inter',sans-serif; font-size:14px;
576
- color:#e8e8ec!important; -webkit-text-fill-color:#e8e8ec!important;
577
- resize:vertical; outline:none; min-height:44px; transition:border-color .2s; font-weight:400;
578
- }
579
- .modern-textarea:focus { border-color:#5D3FD3; box-shadow:0 0 0 3px rgba(93,63,211,.22); }
580
- .modern-textarea::placeholder { color:#555560!important; -webkit-text-fill-color:#555560!important; }
581
- .modern-textarea.error-flash {
582
- border-color:#ef4444!important; box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;
583
- animation:shake .4s ease;
584
- }
585
- @keyframes shake {
586
- 0%,100%{transform:translateX(0)} 20%,60%{transform:translateX(-4px)} 40%,80%{transform:translateX(4px)}
587
- }
588
-
589
- /* ── LoRA selector ── */
590
- .lora-selector-card { border-bottom:1px solid #2a2a35!important; background:#0d0d12!important; }
591
- .lora-selector-body { padding:12px 18px!important; background:#0d0d12!important; }
592
- .lora-select-label {
593
- font-size:11px!important; font-weight:700!important;
594
- color:#555560!important; -webkit-text-fill-color:#555560!important;
595
- text-transform:uppercase!important; letter-spacing:1px!important;
596
- margin-bottom:8px!important; display:flex!important; align-items:center!important;
597
- gap:6px!important; font-family:'Inter',sans-serif!important;
598
- }
599
- .lora-select-label::before {
600
- content:''; display:inline-block; width:8px; height:8px;
601
- background:#5D3FD3; border-radius:50%; flex-shrink:0;
602
- }
603
- .lora-native-select {
604
- width:100%!important; background:#09090e!important;
605
- border:1px solid #33333f!important; border-radius:8px!important;
606
- padding:9px 36px 9px 14px!important;
607
- font-family:'Inter',sans-serif!important; font-size:13px!important; font-weight:600!important;
608
- color:#e8e8ec!important; -webkit-text-fill-color:#e8e8ec!important;
609
- outline:none!important; appearance:none!important; -webkit-appearance:none!important;
610
- background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%235D3FD3' d='M6 8L1 3h10z'/%3E%3C/svg%3E")!important;
611
- background-repeat:no-repeat!important; background-position:right 12px center!important;
612
- cursor:pointer!important; transition:border-color .2s!important; color-scheme:dark!important;
613
- }
614
- .lora-native-select:focus { border-color:#5D3FD3!important; box-shadow:0 0 0 3px rgba(93,63,211,.22)!important; }
615
- .lora-native-select option { background:#1a1a22!important; color:#e8e8ec!important; }
616
-
617
- @media (prefers-color-scheme: light) {
618
- .lora-selector-card { background:#0d0d12!important; }
619
- .lora-selector-body { background:#0d0d12!important; }
620
- .lora-select-label { color:#555560!important; -webkit-text-fill-color:#555560!important; }
621
- .lora-native-select {
622
- background:#09090e!important; color:#e8e8ec!important;
623
- -webkit-text-fill-color:#e8e8ec!important; border-color:#33333f!important; color-scheme:dark!important;
624
- }
625
- .lora-native-select option { background:#1a1a22!important; color:#e8e8ec!important; }
626
- }
627
-
628
- /* ── Toast ── */
629
- .toast-notification {
630
- position:fixed; top:24px; left:50%; transform:translateX(-50%) translateY(-120%);
631
- z-index:9999; padding:10px 24px; border-radius:10px;
632
- font-family:'Inter',sans-serif; font-size:14px; font-weight:700;
633
- display:flex; align-items:center; gap:8px; box-shadow:0 8px 24px rgba(0,0,0,.5);
634
- transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;
635
- opacity:0; pointer-events:none;
636
- }
637
- .toast-notification.visible { transform:translateX(-50%) translateY(0); opacity:1; pointer-events:auto; }
638
- .toast-notification.error { background:linear-gradient(135deg,#dc2626,#b91c1c); color:#fff; border:1px solid rgba(255,255,255,.15); }
639
- .toast-notification.warning { background:linear-gradient(135deg,#5D3FD3,#4A2FA8); color:#fff; border:1px solid rgba(255,255,255,.15); }
640
- .toast-notification.info { background:linear-gradient(135deg,#2563eb,#1d4ed8); color:#fff; border:1px solid rgba(255,255,255,.15); }
641
-
642
- /* ── Run button (polished: no outer color flicker) ── */
643
- .btn-run {
644
- display:flex; align-items:center; justify-content:center; gap:8px; width:100%;
645
- /* Solid background instead of gradient to avoid flicker */
646
- background:#5D3FD3;
647
- border:1px solid #4A2FA8;
648
- border-radius:10px;
649
- padding:13px 24px; cursor:pointer; font-size:15px; font-weight:800;
650
- font-family:'Inter',sans-serif;
651
- color:#fff!important; -webkit-text-fill-color:#fff!important;
652
- /* Smooth, single-property transitions only (background-color) to avoid outer ring flicker */
653
- transition:background-color .18s ease, transform .12s ease;
654
- /* Subtle inset highlight only — no outer glow that could flicker */
655
- box-shadow:inset 0 1px 0 rgba(255,255,255,.12), 0 2px 6px rgba(0,0,0,.25);
656
- outline:none;
657
- }
658
- .btn-run:hover {
659
- background:#7B5FE0;
660
- transform:translateY(-1px);
661
- box-shadow:inset 0 1px 0 rgba(255,255,255,.15), 0 4px 12px rgba(0,0,0,.3);
662
- }
663
- .btn-run:active {
664
- background:#4A2FA8;
665
- transform:translateY(0);
666
- box-shadow:inset 0 1px 0 rgba(255,255,255,.1), 0 1px 4px rgba(0,0,0,.25);
667
- }
668
- .btn-run:focus-visible {
669
- outline:2px solid rgba(123,95,224,.6);
670
- outline-offset:2px;
671
- }
672
- .btn-run svg { width:18px; height:18px; fill:#fff!important; }
673
- #custom-run-btn, #custom-run-btn *, #run-btn-label, .btn-run, .btn-run * {
674
- color:#fff!important; -webkit-text-fill-color:#fff!important; fill:#fff!important;
675
- }
676
-
677
- /* ── Output ── */
678
- .output-frame { border-bottom:1px solid #2a2a35; display:flex; flex-direction:column; position:relative; }
679
- .out-title {
680
- padding:10px 20px; font-size:11px; font-weight:700;
681
- color:#fff!important; -webkit-text-fill-color:#fff!important;
682
- text-transform:uppercase; letter-spacing:1px;
683
- border-bottom:1px solid rgba(42,42,53,.6);
684
- display:flex; align-items:center; justify-content:space-between;
685
- background:#14141a!important; font-family:'Inter',sans-serif;
686
- }
687
- .out-body {
688
- flex:1; background:#09090e!important;
689
- display:flex; align-items:center; justify-content:center;
690
- overflow:hidden; min-height:260px; position:relative;
691
- }
692
- .out-body img { max-width:100%; max-height:480px; image-rendering:auto; }
693
- .out-placeholder { color:#555560; font-size:13px; text-align:center; padding:20px; font-weight:500; font-family:'Inter',sans-serif; }
694
- .out-download-btn {
695
- display:none; align-items:center; justify-content:center;
696
- background:rgba(93,63,211,.14); border:1px solid rgba(93,63,211,.32); border-radius:6px;
697
- cursor:pointer; padding:3px 10px; font-size:11px; font-weight:700;
698
- color:#7B5FE0!important; -webkit-text-fill-color:#7B5FE0!important;
699
- gap:4px; height:24px; transition:background .15s, border-color .15s, color .15s; font-family:'Inter',sans-serif;
700
- }
701
- .out-download-btn:hover {
702
- background:#5D3FD3; border-color:#5D3FD3;
703
- color:#fff!important; -webkit-text-fill-color:#fff!important;
704
- }
705
- .out-download-btn.visible { display:inline-flex; }
706
- .out-download-btn svg { width:12px; height:12px; fill:#7B5FE0; }
707
- .out-download-btn:hover svg { fill:#fff; }
708
-
709
- /* ── Loader ── */
710
- .modern-loader {
711
- display:none; position:absolute; top:0; left:0; right:0; bottom:0;
712
- background:rgba(9,9,14,.93); z-index:15;
713
- flex-direction:column; align-items:center; justify-content:center;
714
- gap:16px; backdrop-filter:blur(4px);
715
- }
716
- .modern-loader.active { display:flex; }
717
- .modern-loader .loader-spinner {
718
- width:36px; height:36px; border:3px solid #2a2a35;
719
- border-top-color:#5D3FD3; border-radius:50%; animation:spin .8s linear infinite;
720
- }
721
- @keyframes spin { to{transform:rotate(360deg)} }
722
- .modern-loader .loader-text { font-size:13px; color:#9898a8; font-weight:600; font-family:'Inter',sans-serif; }
723
- .loader-bar-track { width:200px; height:4px; background:#2a2a35; border-radius:2px; overflow:hidden; }
724
- .loader-bar-fill {
725
- height:100%; background:linear-gradient(90deg,#5D3FD3,#7B5FE0,#5D3FD3);
726
- background-size:200% 100%; animation:shimmer 1.5s ease-in-out infinite; border-radius:2px;
727
- }
728
- @keyframes shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
729
-
730
- /* ── Settings ── */
731
- .settings-group { border:1px solid #2a2a35; border-radius:10px; margin:12px 16px; padding:0; overflow:hidden; background:#14141a!important; }
732
- .settings-group-title {
733
- font-size:11px; font-weight:700; color:#555560; text-transform:uppercase; letter-spacing:1px;
734
- padding:9px 16px; border-bottom:1px solid #2a2a35;
735
- background:rgba(26,26,34,.5)!important; font-family:'Inter',sans-serif;
736
- }
737
- .settings-group-body { padding:14px 16px; display:flex; flex-direction:column; gap:12px; background:#14141a!important; }
738
- .slider-row { display:flex; align-items:center; gap:10px; min-height:28px; }
739
- .slider-row label { font-size:13px; font-weight:600; color:#9898a8; min-width:72px; flex-shrink:0; font-family:'Inter',sans-serif; }
740
- .slider-row input[type="range"] {
741
- flex:1; -webkit-appearance:none; appearance:none;
742
- height:5px; background:#33333f; border-radius:3px; outline:none; min-width:0;
743
- }
744
- .slider-row input[type="range"]::-webkit-slider-thumb {
745
- -webkit-appearance:none; width:16px; height:16px;
746
- background:#5D3FD3;
747
- border-radius:50%; cursor:pointer;
748
- box-shadow:0 2px 6px rgba(93,63,211,.35); transition:transform .15s;
749
- }
750
- .slider-row input[type="range"]::-webkit-slider-thumb:hover { transform:scale(1.2); }
751
- .slider-row input[type="range"]::-moz-range-thumb {
752
- width:16px; height:16px; background:#5D3FD3;
753
- border-radius:50%; cursor:pointer; border:none;
754
- }
755
- .slider-row .slider-val {
756
- min-width:52px; text-align:right; font-family:var(--mono); font-size:12px;
757
- font-weight:500; padding:3px 8px; background:#09090e; border:1px solid #2a2a35;
758
- border-radius:6px; color:#9898a8; flex-shrink:0;
759
- }
760
- .checkbox-row { display:flex; align-items:center; gap:8px; font-size:13px; color:#9898a8; }
761
- .checkbox-row input[type="checkbox"] { accent-color:#5D3FD3; width:16px; height:16px; cursor:pointer; }
762
- .checkbox-row label { color:#9898a8; font-size:13px; cursor:pointer; font-weight:500; font-family:'Inter',sans-serif; }
763
-
764
- /* ── Status bar ── */
765
- .app-statusbar {
766
- background:#14141a!important; border-top:1px solid #2a2a35;
767
- padding:6px 20px; display:flex; gap:12px; height:34px; align-items:center;
768
- }
769
- .app-statusbar .sb-section {
770
- padding:0 12px; flex:1; display:flex; align-items:center;
771
- font-family:var(--mono); font-size:12px; color:#555560;
772
- overflow:hidden; white-space:nowrap;
773
- }
774
- .app-statusbar .sb-section.sb-fixed {
775
- flex:0 0 auto; min-width:90px; text-align:center; justify-content:center;
776
- padding:3px 12px; background:rgba(93,63,211,.12); border-radius:6px;
777
- color:#7B5FE0; font-weight:700; border:1px solid rgba(93,63,211,.25);
778
- }
779
-
780
- /* ── Footer ── */
781
- .exp-note {
782
- padding:10px 20px; font-size:12px; color:#555560;
783
- border-top:1px solid #2a2a35; text-align:center; font-weight:500;
784
- background:#14141a!important; font-family:'Inter',sans-serif;
785
- }
786
- .exp-note a { color:#7B5FE0!important; -webkit-text-fill-color:#7B5FE0!important; text-decoration:none; }
787
- .exp-note a:hover { text-decoration:underline; color:#fff!important; -webkit-text-fill-color:#fff!important; }
788
-
789
- /* ── Scrollbars ── */
790
- ::-webkit-scrollbar { width:7px; height:7px; }
791
- ::-webkit-scrollbar-track { background:#09090e; }
792
- ::-webkit-scrollbar-thumb { background:#33333f; border-radius:4px; }
793
- ::-webkit-scrollbar-thumb:hover { background:#4A2FA8; }
794
-
795
- /* ── Responsive ── */
796
- @media(max-width:860px){
797
- .app-main-row { flex-direction:column; }
798
- .app-main-right { width:100%; }
799
- .app-main-left { border-right:none; border-bottom:1px solid #2a2a35; }
800
- }
801
-
802
- /* ── Logo animation (thunder bolt subtle pulse) ── */
803
- @keyframes thunder-pulse {
804
- 0%, 100% { opacity: 1; transform: scale(1); }
805
- 50% { opacity: 0.85; transform: scale(1.08); }
806
- }
807
- @keyframes logo-glow {
808
- 0%, 100% { filter: drop-shadow(0 0 2px rgba(255,255,255,0.3)); }
809
- 50% { filter: drop-shadow(0 0 6px rgba(255,255,255,0.6)); }
810
- }
811
- .app-logo svg path { animation: thunder-pulse 2.2s ease-in-out infinite; transform-origin: center; }
812
- .app-logo svg { animation: logo-glow 2.4s ease-in-out infinite; }
813
- """
814
-
815
- # ── JavaScript ─────────────────────────────────────────────────────────────────
816
- gallery_js = r"""
817
- () => {
818
- function init() {
819
- if (window.__qwenInitDone) return;
820
-
821
- const dropZone = document.getElementById('gallery-drop-zone');
822
- const uploadPrompt = document.getElementById('upload-prompt');
823
- const uploadClick = document.getElementById('upload-click-area');
824
- const fileInput = document.getElementById('custom-file-input');
825
- const imgPreview = document.getElementById('single-img-preview');
826
- const imgContainer = document.getElementById('single-img-container');
827
- const btnUpload = document.getElementById('tb-upload');
828
- const btnClear = document.getElementById('tb-clear');
829
- const promptInput = document.getElementById('custom-prompt-input');
830
- const loraSelect = document.getElementById('custom-lora-select');
831
- const runBtnEl = document.getElementById('custom-run-btn');
832
- const imgCountTb = document.getElementById('tb-image-count');
833
- const imgCountSb = document.getElementById('sb-image-count');
834
-
835
- if (!dropZone || !fileInput) { setTimeout(init, 250); return; }
836
- window.__qwenInitDone = true;
837
-
838
- let currentImage = null;
839
- let toastTimer = null;
840
-
841
- /* ── Force dark styles ── */
842
- function enforceDark() {
843
- const loraCard = document.querySelector('.lora-selector-card');
844
- const loraBody = document.querySelector('.lora-selector-body');
845
- const loraLabel = document.querySelector('.lora-select-label');
846
- const loraEl = document.getElementById('custom-lora-select');
847
- if (loraCard) loraCard.style.setProperty('background','#0d0d12','important');
848
- if (loraBody) loraBody.style.setProperty('background','#0d0d12','important');
849
- if (loraLabel) { loraLabel.style.setProperty('color','#555560','important'); loraLabel.style.setProperty('-webkit-text-fill-color','#555560','important'); }
850
- if (loraEl) { loraEl.style.setProperty('background-color','#09090e','important'); loraEl.style.setProperty('color','#e8e8ec','important'); loraEl.style.setProperty('-webkit-text-fill-color','#e8e8ec','important'); loraEl.style.setProperty('border-color','#33333f','important'); }
851
- const ghBtn = document.querySelector('.gh-btn');
852
- if (ghBtn) {
853
- ghBtn.style.setProperty('background','#5D3FD3','important');
854
- ghBtn.style.setProperty('color','#ffffff','important');
855
- ghBtn.style.setProperty('-webkit-text-fill-color','#ffffff','important');
856
- const svg = ghBtn.querySelector('svg'); if (svg) svg.style.setProperty('fill','#ffffff','important');
857
- const span = ghBtn.querySelector('span'); if (span) { span.style.setProperty('color','#ffffff','important'); span.style.setProperty('-webkit-text-fill-color','#ffffff','important'); }
858
- }
859
- const shell = document.querySelector('.app-shell');
860
- const header = document.querySelector('.app-header');
861
- const bar = document.querySelector('.app-toolbar');
862
- if (shell) shell.style.setProperty('background','#14141a','important');
863
- if (header) header.style.setProperty('background','#1a1a22','important');
864
- if (bar) bar.style.setProperty('background','#14141a','important');
865
- }
866
- enforceDark();
867
- setInterval(enforceDark, 800);
868
-
869
- /* GitHub hover */
870
- const ghBtn = document.querySelector('.gh-btn');
871
- if (ghBtn) {
872
- ghBtn.addEventListener('mouseenter', () => { ghBtn.style.setProperty('background','#7B5FE0','important'); ghBtn.style.setProperty('transform','translateY(-1px)','important'); ghBtn.style.setProperty('box-shadow','0 5px 18px rgba(93,63,211,.55)','important'); });
873
- ghBtn.addEventListener('mouseleave', () => { ghBtn.style.setProperty('background','#5D3FD3','important'); ghBtn.style.setProperty('transform','translateY(0)','important'); ghBtn.style.setProperty('box-shadow','0 2px 10px rgba(93,63,211,.4)','important'); });
874
- ghBtn.addEventListener('mousedown', () => { ghBtn.style.setProperty('background','#4A2FA8','important'); });
875
- ghBtn.addEventListener('mouseup', () => { ghBtn.style.setProperty('background','#7B5FE0','important'); });
876
- }
877
-
878
- function showToast(message, type) {
879
- let toast = document.getElementById('app-toast');
880
- if (!toast) {
881
- toast = document.createElement('div'); toast.id = 'app-toast'; toast.className = 'toast-notification';
882
- toast.innerHTML = '<span class="toast-icon"></span><span class="toast-text"></span>';
883
- document.body.appendChild(toast);
884
- }
885
- const icon = toast.querySelector('.toast-icon'); const text = toast.querySelector('.toast-text');
886
- toast.className = 'toast-notification ' + (type || 'error');
887
- icon.textContent = type === 'warning' ? '\u26A0' : type === 'info' ? '\u2139' : '\u2717';
888
- text.textContent = message;
889
- if (toastTimer) clearTimeout(toastTimer);
890
- void toast.offsetWidth; toast.classList.add('visible');
891
- toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500);
892
- }
893
- window.__showToast = showToast;
894
-
895
- function flashPromptError() {
896
- if (!promptInput) return;
897
- promptInput.classList.add('error-flash'); promptInput.focus();
898
- setTimeout(() => promptInput.classList.remove('error-flash'), 800);
899
- }
900
-
901
- function setGradioValue(containerId, value) {
902
- const container = document.getElementById(containerId); if (!container) return;
903
- container.querySelectorAll('input, textarea').forEach(el => {
904
- if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return;
905
- const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
906
- const ns = Object.getOwnPropertyDescriptor(proto, 'value');
907
- if (ns && ns.set) { ns.set.call(el, value); el.dispatchEvent(new Event('input',{bubbles:true,composed:true})); el.dispatchEvent(new Event('change',{bubbles:true,composed:true})); }
908
- });
909
- }
910
- window.__setGradioValue = setGradioValue;
911
-
912
- function updateCount(hasImg) {
913
- const txt = hasImg ? '1 image' : 'No images';
914
- if (imgCountTb) imgCountTb.textContent = txt;
915
- if (imgCountSb) imgCountSb.textContent = hasImg ? '1 image uploaded' : 'No images uploaded';
916
- }
917
-
918
- function setImage(b64, name) {
919
- currentImage = {b64, name};
920
- if (imgPreview) { imgPreview.src = b64; imgPreview.style.display = 'block'; }
921
- if (imgContainer) imgContainer.style.display = 'block';
922
- if (uploadPrompt) uploadPrompt.style.display = 'none';
923
- setGradioValue('hidden-image-b64', b64);
924
- updateCount(true);
925
- }
926
- window.__setImage = setImage;
927
-
928
- function clearImage() {
929
- currentImage = null;
930
- if (imgPreview) { imgPreview.src = ''; imgPreview.style.display = 'none'; }
931
- if (imgContainer) imgContainer.style.display = 'none';
932
- if (uploadPrompt) uploadPrompt.style.display = '';
933
- setGradioValue('hidden-image-b64', '');
934
- updateCount(false);
935
- }
936
- window.__clearImage = clearImage;
937
-
938
- function syncPrompt() { if (promptInput) setGradioValue('prompt-gradio-input', promptInput.value); }
939
- function syncLora() {
940
- if (!loraSelect) return;
941
- const c = document.getElementById('gradio-lora'); if (!c) return;
942
- c.querySelectorAll('input').forEach(el => {
943
- const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value');
944
- if (ns && ns.set) { ns.set.call(el,loraSelect.value); el.dispatchEvent(new Event('input',{bubbles:true,composed:true})); el.dispatchEvent(new Event('change',{bubbles:true,composed:true})); }
945
- });
946
- }
947
-
948
- function processFile(file) {
949
- if (!file || !file.type.startsWith('image/')) return;
950
- const reader = new FileReader();
951
- reader.onload = (e) => setImage(e.target.result, file.name);
952
- reader.readAsDataURL(file);
953
- }
954
-
955
- fileInput.addEventListener('change', (e) => { if (e.target.files[0]) processFile(e.target.files[0]); e.target.value = ''; });
956
- if (uploadClick) uploadClick.addEventListener('click', () => fileInput.click());
957
- if (btnUpload) btnUpload.addEventListener('click', () => fileInput.click());
958
- if (btnClear) btnClear.addEventListener('click', clearImage);
959
-
960
- dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
961
- dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); });
962
- dropZone.addEventListener('drop', (e) => {
963
- e.preventDefault(); dropZone.classList.remove('drag-over');
964
- if (e.dataTransfer.files.length) processFile(e.dataTransfer.files[0]);
965
- });
966
-
967
- /* Click on preview to replace */
968
- if (imgContainer) imgContainer.addEventListener('click', () => fileInput.click());
969
-
970
- if (promptInput) promptInput.addEventListener('input', syncPrompt);
971
- if (loraSelect) loraSelect.addEventListener('change', syncLora);
972
-
973
- window.__setPrompt = function(text) { if (promptInput) { promptInput.value = text; syncPrompt(); } };
974
- window.__setLora = function(lora) {
975
- if (loraSelect) { loraSelect.value = lora; loraSelect.dispatchEvent(new Event('change',{bubbles:true})); syncLora(); }
976
- };
977
-
978
- /* Example card clicks */
979
- document.querySelectorAll('.example-card[data-idx]').forEach(card => {
980
- card.addEventListener('click', () => {
981
- const idx = card.getAttribute('data-idx');
982
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
983
- card.classList.add('loading'); showToast('Loading example\u2026','info');
984
- setGradioValue('example-result-data',''); setGradioValue('example-idx-input', idx);
985
- setTimeout(() => { const btn = document.getElementById('example-load-btn'); if (btn) { const b = btn.querySelector('button'); if (b) b.click(); else btn.click(); } }, 150);
986
- setTimeout(() => card.classList.remove('loading'), 12000);
987
- });
988
- });
989
-
990
- /* Sliders */
991
- function syncSlider(customId, gradioId) {
992
- const slider = document.getElementById(customId); const valSpan = document.getElementById(customId+'-val');
993
- if (!slider) return;
994
- slider.addEventListener('input', () => {
995
- if (valSpan) valSpan.textContent = slider.value;
996
- const c = document.getElementById(gradioId); if (!c) return;
997
- c.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => {
998
- const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value');
999
- if (ns && ns.set) { ns.set.call(el,slider.value); el.dispatchEvent(new Event('input',{bubbles:true,composed:true})); el.dispatchEvent(new Event('change',{bubbles:true,composed:true})); }
1000
- });
1001
- });
1002
- }
1003
- syncSlider('custom-seed','gradio-seed'); syncSlider('custom-guidance','gradio-guidance'); syncSlider('custom-steps','gradio-steps');
1004
-
1005
- const randCheck = document.getElementById('custom-randomize');
1006
- if (randCheck) {
1007
- randCheck.addEventListener('change', () => {
1008
- const c = document.getElementById('gradio-randomize'); if (!c) return;
1009
- const cb = c.querySelector('input[type="checkbox"]'); if (cb && cb.checked !== randCheck.checked) cb.click();
1010
- });
1011
- }
1012
-
1013
- function showLoader() { const l=document.getElementById('output-loader'); if(l) l.classList.add('active'); const sb=document.querySelector('.sb-fixed'); if(sb) sb.textContent='Processing\u2026'; }
1014
- function hideLoader() { const l=document.getElementById('output-loader'); if(l) l.classList.remove('active'); const sb=document.querySelector('.sb-fixed'); if(sb) sb.textContent='Done'; }
1015
- window.__showLoader = showLoader; window.__hideLoader = hideLoader;
1016
-
1017
- function validateAndRun() {
1018
- const pv = promptInput ? promptInput.value.trim() : '';
1019
- if (!currentImage && !pv) { showToast('Please upload an image and enter a prompt','error'); flashPromptError(); return false; }
1020
- if (!currentImage) { showToast('Please upload an image','error'); return false; }
1021
- if (!pv) { showToast('Please enter an edit prompt','warning'); flashPromptError(); return false; }
1022
- return true;
1023
- }
1024
-
1025
- window.__clickRun = function() {
1026
- if (!validateAndRun()) return;
1027
- syncPrompt(); syncLora();
1028
- setGradioValue('hidden-image-b64', currentImage ? currentImage.b64 : '');
1029
- showLoader();
1030
- setTimeout(() => { const gb = document.getElementById('gradio-run-btn'); if (!gb) return; const b = gb.querySelector('button'); if (b) b.click(); else gb.click(); }, 200);
1031
- };
1032
-
1033
- if (runBtnEl) runBtnEl.addEventListener('click', () => window.__clickRun());
1034
- updateCount(false);
1035
- }
1036
- init();
1037
- }
1038
- """
1039
-
1040
- wire_outputs_js = r"""
1041
- () => {
1042
- function watchOutputs() {
1043
- const resultContainer = document.getElementById('gradio-result');
1044
- const outBody = document.getElementById('output-image-container');
1045
- const outPh = document.getElementById('output-placeholder');
1046
- const dlBtn = document.getElementById('dl-btn-output');
1047
- if (!resultContainer || !outBody) { setTimeout(watchOutputs, 500); return; }
1048
- if (dlBtn) {
1049
- dlBtn.addEventListener('click', (e) => {
1050
- e.stopPropagation();
1051
- const img = outBody.querySelector('img.modern-out-img');
1052
- if (img && img.src) { const a=document.createElement('a'); a.href=img.src; a.download='qwen_edit_output.png'; document.body.appendChild(a); a.click(); document.body.removeChild(a); }
1053
- });
1054
- }
1055
- function syncImage() {
1056
- const ri = resultContainer.querySelector('img');
1057
- if (ri && ri.src) {
1058
- if (outPh) outPh.style.display='none';
1059
- let ex = outBody.querySelector('img.modern-out-img');
1060
- if (!ex) { ex=document.createElement('img'); ex.className='modern-out-img'; outBody.appendChild(ex); }
1061
- if (ex.src !== ri.src) { ex.src=ri.src; if(dlBtn) dlBtn.classList.add('visible'); if(window.__hideLoader) window.__hideLoader(); }
1062
- }
1063
- }
1064
- const obs = new MutationObserver(syncImage);
1065
- obs.observe(resultContainer,{childList:true,subtree:true,attributes:true,attributeFilter:['src']});
1066
- setInterval(syncImage, 800);
1067
- }
1068
- watchOutputs();
1069
-
1070
- function watchSeed() {
1071
- const sc=document.getElementById('gradio-seed'); const sl=document.getElementById('custom-seed'); const sv=document.getElementById('custom-seed-val');
1072
- if (!sc||!sl) { setTimeout(watchSeed,500); return; }
1073
- function sync() { const el=sc.querySelector('input[type="range"],input[type="number"]'); if(el&&el.value){sl.value=el.value;if(sv)sv.textContent=el.value;} }
1074
- const obs=new MutationObserver(sync); obs.observe(sc,{childList:true,subtree:true,attributes:true,attributeFilter:['value']}); setInterval(sync,1000);
1075
- }
1076
- watchSeed();
1077
-
1078
- function watchExamples() {
1079
- const container = document.getElementById('example-result-data');
1080
- if (!container) { setTimeout(watchExamples,500); return; }
1081
- let last = '';
1082
- function check() {
1083
- const el = container.querySelector('textarea')||container.querySelector('input'); if (!el) return;
1084
- const val = el.value; if (!val||val===last||val.length<10) return;
1085
- try {
1086
- const data = JSON.parse(val);
1087
- if (data.status==='ok' && data.image) {
1088
- last = val;
1089
- if (window.__clearImage) window.__clearImage();
1090
- if (window.__setImage) window.__setImage(data.image, data.name||'example.jpg');
1091
- if (window.__setPrompt && data.prompt) window.__setPrompt(data.prompt);
1092
- if (window.__setLora && data.lora) window.__setLora(data.lora);
1093
- document.querySelectorAll('.example-card.loading').forEach(c=>c.classList.remove('loading'));
1094
- if (window.__showToast) window.__showToast('Example loaded','info');
1095
- } else if (data.status==='error') {
1096
- document.querySelectorAll('.example-card.loading').forEach(c=>c.classList.remove('loading'));
1097
- if (window.__showToast) window.__showToast('Could not load example','error');
1098
- }
1099
- } catch(e) { console.error('Example parse error:',e); }
1100
  }
1101
- const obs=new MutationObserver(check); obs.observe(container,{childList:true,subtree:true,characterData:true,attributes:true}); setInterval(check,500);
1102
- }
1103
- watchExamples();
1104
- }
1105
- """
1106
-
1107
- # ── SVG assets ─────────────────────────────────────────────────────────────────
1108
-
1109
- # Thunder logo SVG (replacing previous fire/wand logo)
1110
- THUNDER_LOGO_SVG = """<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
1111
- <path d="M13 2L5 13h5l-1 9 8-11h-5l1-9z" fill="white"/>
1112
- </svg>"""
1113
-
1114
- LOGO_SVG = THUNDER_LOGO_SVG
1115
-
1116
- 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>'
1117
- 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"/></svg>'
1118
- DOWNLOAD_SVG = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 16l-5-5h3V4h4v7h3l-5 5z" fill="currentColor"/><path d="M20 18H4v2h16v-2z" fill="currentColor"/></svg>'
1119
- 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>'
1120
-
1121
- # Thunder bolt icon for run button
1122
- RUN_SVG = '''<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="18" height="18">
1123
- <path d="M13 2L5 13h5l-1 9 8-11h-5l1-9z" fill="white"/>
1124
- </svg>'''
1125
 
1126
- LORA_OPTIONS_HTML = "\n".join(
1127
- f'<option value="{html_lib.escape(n)}">{html_lib.escape(n)}</option>'
1128
- for n in ADAPTER_NAMES
1129
- )
1130
 
1131
- # ── Gradio app ─────────────────────────────────────────────────────────────────
1132
- with gr.Blocks() as demo:
 
 
1133
 
1134
- # Hidden state
1135
- hidden_image_b64 = gr.Textbox(value="", elem_id="hidden-image-b64", elem_classes="hidden-input", container=False)
1136
- prompt = gr.Textbox(value="", elem_id="prompt-gradio-input", elem_classes="hidden-input", container=False)
1137
- lora_adapter = gr.Dropdown(choices=ADAPTER_NAMES, value="Photo-to-Anime", elem_id="gradio-lora", elem_classes="hidden-input", container=False)
1138
- seed = gr.Slider(minimum=0, maximum=MAX_SEED, step=1, value=0, elem_id="gradio-seed", elem_classes="hidden-input", container=False)
1139
- randomize_seed = gr.Checkbox(value=True, elem_id="gradio-randomize", elem_classes="hidden-input", container=False)
1140
- 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)
1141
- steps = gr.Slider(minimum=1, maximum=50, step=1, value=4, elem_id="gradio-steps", elem_classes="hidden-input", container=False)
1142
- result = gr.Image(elem_id="gradio-result", elem_classes="hidden-input", container=False, format="png")
1143
 
1144
- example_idx = gr.Textbox(value="", elem_id="example-idx-input", elem_classes="hidden-input", container=False)
1145
- example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False)
1146
- example_load_btn = gr.Button("Load Example", elem_id="example-load-btn")
 
 
1147
 
1148
- gr.HTML(f"""
1149
- <div class="app-shell">
1150
-
1151
- <!-- Header -->
1152
- <div class="app-header">
1153
- <div class="app-header-left">
1154
- <div class="app-logo">{LOGO_SVG}</div>
1155
- <span class="app-title">Qwen-Image-Edit</span>
1156
- <span class="app-badge">2509</span>
1157
- <span class="app-badge fast">4-Step Fast</span>
1158
- </div>
1159
- <a href="https://github.com/PRITHIVSAKTHIUR/Qwen-Image-Edit-2509-LoRAs-Fast-Lazy-Load"
1160
- target="_blank" class="gh-btn">
1161
- {GITHUB_SVG}
1162
- <span>GitHub</span>
1163
- </a>
1164
- </div>
1165
-
1166
- <!-- Toolbar -->
1167
- <div class="app-toolbar">
1168
- <button id="tb-upload" class="modern-tb-btn" title="Upload image">
1169
- {UPLOAD_SVG}<span class="tb-label">Upload</span>
1170
- </button>
1171
- <button id="tb-clear" class="modern-tb-btn" title="Clear image">
1172
- {CLEAR_SVG}<span class="tb-label">Clear</span>
1173
- </button>
1174
- <div class="tb-sep"></div>
1175
- <span id="tb-image-count" class="tb-info">No images</span>
1176
- </div>
1177
-
1178
- <!-- Main row -->
1179
- <div class="app-main-row">
1180
-
1181
- <!-- Left: image drop + examples -->
1182
- <div class="app-main-left">
1183
-
1184
- <!-- Drop zone -->
1185
- <div id="gallery-drop-zone">
1186
- <div id="upload-prompt" class="upload-prompt-modern">
1187
- <div id="upload-click-area" class="upload-click-area">
1188
- <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
1189
- <rect x="8" y="14" width="64" height="52" rx="6" fill="none"
1190
- stroke="#5D3FD3" stroke-width="2" stroke-dasharray="4 3"/>
1191
- <polygon points="12,62 30,40 42,50 54,34 68,62"
1192
- fill="rgba(93,63,211,0.12)" stroke="#5D3FD3" stroke-width="1.5"/>
1193
- <circle cx="28" cy="30" r="6"
1194
- fill="rgba(93,63,211,0.18)" stroke="#5D3FD3" stroke-width="1.5"/>
1195
- </svg>
1196
- <span class="upload-main-text">Click or drag image here</span>
1197
- <span class="upload-sub-text">Upload the image you want to edit</span>
1198
- </div>
1199
- </div>
1200
- <input id="custom-file-input" type="file" accept="image/*" style="display:none;" />
1201
- <!-- Single image preview -->
1202
- <div id="single-img-container" style="display:none;position:relative;width:100%;height:100%;min-height:440px;cursor:pointer;" title="Click to replace image">
1203
- <img id="single-img-preview" src="" alt="uploaded"
1204
- style="width:100%;height:100%;min-height:440px;object-fit:contain;display:block;background:#09090e;" />
1205
- <div style="position:absolute;bottom:12px;right:12px;background:rgba(93,63,211,.85);color:#fff;font-size:11px;font-weight:700;padding:4px 12px;border-radius:6px;font-family:'Inter',sans-serif;pointer-events:none;">
1206
- Click to replace
1207
- </div>
1208
- </div>
1209
- </div>
1210
-
1211
- <!-- Hint -->
1212
- <div class="hint-bar">
1213
- <b>Upload:</b> Click or drag an image &nbsp;&middot;&nbsp;
1214
- <b>Click preview</b> to replace &nbsp;&middot;&nbsp;
1215
- <kbd>Clear</kbd> removes the image
1216
- </div>
1217
-
1218
- <!-- Quick prompts -->
1219
- <div class="suggestions-section">
1220
- <div class="suggestions-title">Quick Prompts</div>
1221
- <div class="suggestions-wrap">
1222
- <button class="suggestion-chip" onclick="window.__setPrompt('Transform into anime.')">Anime</button>
1223
- <button class="suggestion-chip" onclick="window.__setPrompt('Remove shadows and relight using soft lighting.')">Relight</button>
1224
- <button class="suggestion-chip" onclick="window.__setPrompt('Rotate the camera 45 degrees to the right.')">Rotate 45°</button>
1225
- <button class="suggestion-chip" onclick="window.__setPrompt('Switch the camera to a top-down view.')">Top-down</button>
1226
- <button class="suggestion-chip" onclick="window.__setPrompt('Upscale this picture to 4K resolution.')">Upscale 4K</button>
1227
- <button class="suggestion-chip" onclick="window.__setPrompt('flatcolor Desaturate to flat log profile.')">Flat Log</button>
1228
- <button class="suggestion-chip" onclick="window.__setPrompt('Light source from the Right Rear.')">Right-Rear Light</button>
1229
- <button class="suggestion-chip" onclick="window.__setPrompt('Light source from the Below.')">Below Light</button>
1230
- <button class="suggestion-chip" onclick="window.__setPrompt('dotted illustration.')">Dotted Illus.</button>
1231
- <button class="suggestion-chip" onclick="window.__setPrompt('Make the skin details more prominent and natural.')">Edit Skin</button>
1232
- <button class="suggestion-chip" onclick="window.__setPrompt('Switch the camera to a bottom-up view.')">Bottom-up</button>
1233
- <button class="suggestion-chip" onclick="window.__setPrompt('Use a subtle golden-hour filter with smooth light diffusion.')">Golden Hour</button>
1234
- </div>
1235
- </div>
1236
-
1237
- <!-- Examples -->
1238
- <div class="examples-section">
1239
- <div class="examples-title">Quick Examples &mdash; click to load</div>
1240
- <div class="examples-scroll">
1241
- {EXAMPLE_CARDS_HTML}
1242
- </div>
1243
- </div>
1244
-
1245
- </div><!-- /left -->
1246
-
1247
- <!-- Right: controls + output -->
1248
- <div class="app-main-right">
1249
-
1250
- <!-- Prompt -->
1251
- <div class="panel-card">
1252
- <div class="panel-card-title">Edit Instruction</div>
1253
- <div class="panel-card-body">
1254
- <label class="modern-label" for="custom-prompt-input">Prompt</label>
1255
- <textarea id="custom-prompt-input" class="modern-textarea" rows="3"
1256
- placeholder="e.g., transform into anime, upscale, change lighting&hellip;"></textarea>
1257
- </div>
1258
- </div>
1259
-
1260
- <!-- LoRA -->
1261
- <div class="lora-selector-card">
1262
- <div class="lora-selector-body">
1263
- <div class="lora-select-label">Editing Style / LoRA</div>
1264
- <select id="custom-lora-select" class="lora-native-select">
1265
- {LORA_OPTIONS_HTML}
1266
- </select>
1267
- </div>
1268
- </div>
1269
-
1270
- <!-- Run -->
1271
- <div style="padding:14px 18px 6px;">
1272
- <button id="custom-run-btn" class="btn-run" type="button">
1273
- {RUN_SVG}
1274
- <span id="run-btn-label">Edit Image</span>
1275
- </button>
1276
- </div>
1277
-
1278
- <!-- Output -->
1279
- <div class="output-frame" style="flex:1">
1280
- <div class="out-title">
1281
- <span>Output</span>
1282
- <span id="dl-btn-output" class="out-download-btn" title="Download result">
1283
- {DOWNLOAD_SVG} Save
1284
- </span>
1285
- </div>
1286
- <div class="out-body" id="output-image-container">
1287
- <div class="modern-loader" id="output-loader">
1288
- <div class="loader-spinner"></div>
1289
- <div class="loader-text">Processing image&hellip;</div>
1290
- <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1291
- </div>
1292
- <div class="out-placeholder" id="output-placeholder">Result will appear here</div>
1293
- </div>
1294
- </div>
1295
-
1296
- <!-- Advanced settings -->
1297
- <div class="settings-group">
1298
- <div class="settings-group-title">Advanced Settings</div>
1299
- <div class="settings-group-body">
1300
- <div class="slider-row">
1301
- <label>Seed</label>
1302
- <input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0">
1303
- <span class="slider-val" id="custom-seed-val">0</span>
1304
- </div>
1305
- <div class="checkbox-row">
1306
- <input type="checkbox" id="custom-randomize" checked>
1307
- <label for="custom-randomize">Randomize seed</label>
1308
- </div>
1309
- <div class="slider-row">
1310
- <label>Guidance</label>
1311
- <input type="range" id="custom-guidance" min="1" max="10" step="0.1" value="1.0">
1312
- <span class="slider-val" id="custom-guidance-val">1.0</span>
1313
- </div>
1314
- <div class="slider-row">
1315
- <label>Steps</label>
1316
- <input type="range" id="custom-steps" min="1" max="50" step="1" value="4">
1317
- <span class="slider-val" id="custom-steps-val">4</span>
1318
- </div>
1319
- </div>
1320
- </div>
1321
-
1322
- </div><!-- /right -->
1323
- </div><!-- /main-row -->
1324
-
1325
- <!-- Footer -->
1326
- <div class="exp-note">
1327
- Experimental Space for
1328
- <a href="https://huggingface.co/Qwen/Qwen-Image-Edit-2509" target="_blank">Qwen-Image-Edit-2509</a>
1329
- &middot; LoRAs loaded lazily on first use
1330
- </div>
1331
-
1332
- <div class="app-statusbar">
1333
- <div class="sb-section" id="sb-image-count">No images uploaded</div>
1334
- <div class="sb-section sb-fixed">Ready</div>
1335
- </div>
1336
-
1337
- </div><!-- /app-shell -->
1338
- """)
1339
-
1340
- run_btn = gr.Button("Run", elem_id="gradio-run-btn")
1341
-
1342
- demo.load(fn=None, js=gallery_js)
1343
- demo.load(fn=None, js=wire_outputs_js)
1344
-
1345
- run_btn.click(
1346
- fn=infer,
1347
- inputs=[hidden_image_b64, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps],
1348
- outputs=[result, seed],
1349
- js=r"""(img, p, la, s, rs, gs, st) => {
1350
- const imgEl = document.getElementById('hidden-image-b64');
1351
- const promptEl = document.getElementById('custom-prompt-input');
1352
- const loraEl = document.getElementById('custom-lora-select');
1353
- const imgVal = imgEl ? (imgEl.querySelector('textarea,input') || {}).value || img : img;
1354
- const pVal = promptEl ? promptEl.value : p;
1355
- const lVal = loraEl ? loraEl.value : la;
1356
- return [imgVal, pVal, lVal, s, rs, gs, st];
1357
- }""",
1358
- )
1359
-
1360
- example_load_btn.click(
1361
- fn=load_example_data,
1362
- inputs=[example_idx],
1363
- outputs=[example_result],
1364
- queue=False,
1365
- )
1366
 
1367
  if __name__ == "__main__":
1368
- demo.queue(max_size=50).launch(
1369
- css=css,
1370
- mcp_server=True,
1371
- ssr_mode=False,
1372
- show_error=True,
1373
- allowed_paths=["examples"],
1374
- )
 
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
 
 
51
  except Exception as e:
52
  print(f"Warning: Could not set FA3 processor: {e}")
53
 
54
+ # ── LoRA adapter registry (2509) ───────────────────────────────────────────────
55
  ADAPTER_SPECS = {
56
  "Photo-to-Anime": {
57
  "repo": "autoweeb/Qwen-Image-Edit-2509-Photo-to-Anime",
 
132
  ]
133
 
134
 
 
135
  def make_thumb_b64(path, max_dim=220):
136
  if not os.path.exists(path):
137
  return ""
 
160
  return ""
161
 
162
 
163
+ def build_client_config():
164
+ """Static config consumed by the frontend: LoRA list + example cards."""
165
+ examples = []
166
  for i, ex in enumerate(EXAMPLES_CONFIG):
167
+ examples.append({
168
+ "idx": i,
169
+ "thumb": make_thumb_b64(ex["image"]),
170
+ "lora": ex["lora"],
171
+ "prompt": ex["prompt"],
172
+ })
173
+ return {
174
+ "loras": ADAPTER_NAMES,
175
+ "default_lora": "Photo-to-Anime",
176
+ "examples": examples,
177
+ }
178
+
 
 
179
 
180
+ print("Building client config (example thumbnails)…")
181
+ CLIENT_CONFIG = build_client_config()
182
+ print(f"Built config with {len(EXAMPLES_CONFIG)} examples and {len(ADAPTER_NAMES)} LoRAs.")
183
 
184
+
185
+ def b64_to_pil(b64_str):
186
+ if not b64_str or not isinstance(b64_str, str):
187
+ return None
188
  try:
189
+ if b64_str.startswith("data:image"):
190
+ _, data = b64_str.split(",", 1)
191
+ else:
192
+ data = b64_str
193
+ return Image.open(BytesIO(base64.b64decode(data))).convert("RGB")
194
+ except Exception as e:
195
+ print(f"Error decoding image: {e}")
196
+ return None
 
197
 
198
 
199
+ def pil_to_b64_png(image: Image.Image) -> str:
200
+ buf = BytesIO()
201
+ image.save(buf, format="PNG")
202
+ return f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"
203
 
204
 
205
  def update_dimensions_on_upload(image):
 
207
  return 1024, 1024
208
  w, h = image.size
209
  if w > h:
210
+ nw = 1024
211
+ nh = int(nw * h / w)
212
  else:
213
+ nh = 1024
214
+ nw = int(nh * w / h)
215
  return (nw // 8) * 8, (nh // 8) * 8
216
 
217
 
218
+ # ── Gradio Server (Server mode): FastAPI + Gradio queue/API engine ────────────
219
+ app = Server(title="Qwen-Image-Edit-2509-LoRAs-Fast")
 
 
 
 
 
 
 
 
 
 
220
 
221
 
222
+ @app.mcp.tool(name="edit_image")
223
+ @app.api(name="edit_image")
224
  @spaces.GPU(size="xlarge")
225
+ def infer(
226
+ image_b64: str,
227
+ prompt: str,
228
+ lora_adapter: str,
229
+ seed: int,
230
+ randomize_seed: bool,
231
+ guidance_scale: float,
232
+ steps: int,
233
+ ) -> dict:
234
+ """Edit a single image with Qwen-Image-Edit-2509 + a lazily-loaded LoRA.
235
+
236
+ Returns {"image": <base64 PNG data URL>, "seed": <seed used>}.
237
+ """
238
  gc.collect()
239
  torch.cuda.empty_cache()
240
 
 
282
  generator=generator,
283
  true_cfg_scale=guidance_scale,
284
  ).images[0]
285
+ return {"image": pil_to_b64_png(result_image), "seed": seed}
286
  except Exception as e:
287
  raise e
288
  finally:
 
290
  torch.cuda.empty_cache()
291
 
292
 
293
+ @app.api(name="load_example", queue=False)
294
+ def load_example(idx: float) -> dict:
295
+ """Return base64-encoded example image + prompt + LoRA for a given example index."""
296
+ try:
297
+ i = int(idx)
298
+ except (ValueError, TypeError):
299
+ i = -1
300
+ if i < 0 or i >= len(EXAMPLES_CONFIG):
301
+ return {"image": "", "prompt": "", "lora": "", "name": "", "status": "error"}
302
+ ex = EXAMPLES_CONFIG[i]
303
+ b64 = encode_full_image(ex["image"])
304
+ return {
305
+ "image": b64,
306
+ "prompt": ex["prompt"],
307
+ "lora": ex["lora"],
308
+ "name": os.path.basename(ex["image"]),
309
+ "status": "ok" if b64 else "error",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
 
 
 
 
312
 
313
+ @app.get("/api/config")
314
+ def client_config():
315
+ """Plain FastAPI route: LoRA choices + example card data for the frontend."""
316
+ return CLIENT_CONFIG
317
 
 
 
 
 
 
 
 
 
 
318
 
319
+ @app.get("/", response_class=HTMLResponse)
320
+ async def homepage():
321
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
322
+ with open(html_path, "r", encoding="utf-8") as f:
323
+ return f.read()
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
  if __name__ == "__main__":
327
+ app.launch(show_error=True, mcp_server=True)