someone-in-the-world Claude Sonnet 4.6 commited on
Commit
28ae1ac
·
1 Parent(s): a30e6f9

Refactor: extract inline CSS, JS, and HTML from app.py into static files

Browse files

Move all inline content out of app.py into dedicated files:
- static/app.css — all styles
- static/gallery.js — image gallery and UI wiring
- static/wire_outputs.js — output/seed/example result watchers
- static/run_preprocess.js — pre-flight JS for run button
- templates/app.html — HTML shell with named .format() placeholders

Named keyword arguments (e.g. {fire_logo_svg}, {example_cards_html}) are
used in the template so each substitution site is self-documenting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (6) hide show
  1. app.py +505 -1377
  2. static/app.css +343 -0
  3. static/gallery.js +289 -0
  4. static/run_preprocess.js +8 -0
  5. static/wire_outputs.js +99 -0
  6. templates/app.html +154 -0
app.py CHANGED
@@ -1,1377 +1,505 @@
1
- import os
2
- import gc
3
- import threading
4
- import gradio as gr
5
- import numpy as np
6
- import spaces
7
- import torch
8
- import random
9
- import base64
10
- import json
11
- import html as html_lib
12
- from io import BytesIO
13
- from datetime import datetime, timezone
14
- from PIL import Image
15
-
16
- MAX_SEED = np.iinfo(np.int32).max
17
- LANCZOS = getattr(Image, "Resampling", Image).LANCZOS
18
- MAX_OUTPUT_DIM = 2048
19
-
20
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
21
-
22
- print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
23
- print("torch.__version__ =", torch.__version__)
24
- print("Using device:", device)
25
-
26
- # TF32 matmul: ~10-15% free speedup on Ampere/Hopper (bfloat16 accumulation paths benefit too)
27
- torch.backends.cuda.matmul.allow_tf32 = True
28
- torch.backends.cudnn.allow_tf32 = True
29
-
30
- from diffusers import FlowMatchEulerDiscreteScheduler
31
- from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
32
- from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
33
- from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
34
-
35
- dtype = torch.bfloat16
36
-
37
- pipe = QwenImageEditPlusPipeline.from_pretrained(
38
- "FireRedTeam/FireRed-Image-Edit-1.1",
39
- transformer=QwenImageTransformer2DModel.from_pretrained(
40
- "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19",
41
- torch_dtype=dtype,
42
- device_map="cuda",
43
- ),
44
- torch_dtype=dtype,
45
- ).to(device)
46
-
47
- print("Using default attention processor (FA3 skipped for ZeroGPU GPU-arch compatibility).")
48
-
49
- print("torch.compile skipped: lazy Triton kernel compilation inside @spaces.GPU always exceeds ZeroGPU's task timeout.")
50
-
51
- HF_TOKEN = os.environ.get("HF_TOKEN")
52
- DATASET_REPO = os.environ.get("LOG_DATASET_REPO")
53
-
54
- EXAMPLES_CONFIG = [
55
- {
56
- "images": ["examples/1.jpg"],
57
- "prompt": "cinematic polaroid with soft grain subtle vignette gentle lighting white frame handwritten photographed 'Fire-Edit' preserving realistic texture and details.",
58
- },
59
- {
60
- "images": ["examples/2.jpg"],
61
- "prompt": "Transform the image into a dotted cartoon style.",
62
- },
63
- {
64
- "images": ["examples/3.jpeg"],
65
- "prompt": "Convert it to black and white.",
66
- },
67
- {
68
- "images": ["examples/4.jpg", "examples/5.jpg"],
69
- "prompt": "Replace her glasses with the new glasses from image 1.",
70
- },
71
- {
72
- "images": ["examples/8.jpg", "examples/9.png"],
73
- "prompt": "Replace the current clothing with the clothing from the reference image 2. Keep the person's face, hairstyle, body pose, background, lighting, and camera angle unchanged. Ensure the new outfit fits naturally with realistic fabric texture, proper shadows, folds, and accurate proportions. Match the lighting, color tone, and overall style for a seamless and high-quality result.",
74
- },
75
- {
76
- "images": ["examples/10.jpg", "examples/11.png"],
77
- "prompt": "Replace the current clothing with the clothing from the reference image 2. Keep the person's face, hairstyle, body pose, background, lighting, and camera angle unchanged. Ensure the new outfit fits naturally with realistic fabric texture, proper shadows, folds, and accurate proportions. Match the lighting, color tone, and overall style for a seamless and high-quality result.",
78
- },
79
- ]
80
-
81
-
82
- def make_thumb_b64(path, max_dim=220):
83
- if not os.path.exists(path):
84
- return ""
85
- try:
86
- img = Image.open(path).convert("RGB")
87
- img.thumbnail((max_dim, max_dim), LANCZOS)
88
- buf = BytesIO()
89
- img.save(buf, format="JPEG", quality=65)
90
- return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
91
- except Exception as e:
92
- print(f"Thumbnail error for {path}: {e}")
93
- return ""
94
-
95
-
96
- def encode_full_image(path):
97
- if not os.path.exists(path):
98
- return ""
99
- try:
100
- with open(path, "rb") as f:
101
- data = f.read()
102
- ext = path.rsplit(".", 1)[-1].lower()
103
- mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp"}.get(ext, "image/jpeg")
104
- return f"data:{mime};base64,{base64.b64encode(data).decode()}"
105
- except Exception as e:
106
- print(f"Encode error for {path}: {e}")
107
- return ""
108
-
109
-
110
- def build_example_cards_html():
111
- cards = ""
112
- for i, ex in enumerate(EXAMPLES_CONFIG):
113
- thumbs_html = ""
114
- for path in ex["images"]:
115
- thumb = make_thumb_b64(path)
116
- if thumb:
117
- thumbs_html += f'<img src="{thumb}" alt="">'
118
- else:
119
- thumbs_html += '<div class="example-thumb-placeholder">Preview</div>'
120
- n = len(ex["images"])
121
- badge = f'{n} image{"s" if n > 1 else ""}'
122
- prompt_short = html_lib.escape(ex["prompt"][:90])
123
- if len(ex["prompt"]) > 90:
124
- prompt_short += "..."
125
- cards += f'''<div class="example-card" data-idx="{i}">
126
- <div class="example-thumbs">{thumbs_html}</div>
127
- <div class="example-meta"><span class="example-badge">{badge}</span></div>
128
- <div class="example-prompt-text">{prompt_short}</div>
129
- </div>'''
130
- return cards
131
-
132
-
133
- def load_example_data(idx_str):
134
- try:
135
- idx = int(float(idx_str)) if idx_str and idx_str.strip() else -1
136
- except (ValueError, TypeError):
137
- idx = -1
138
- if idx < 0 or idx >= len(EXAMPLES_CONFIG):
139
- return json.dumps({"images": [], "prompt": "", "names": [], "status": "error"})
140
- ex = EXAMPLES_CONFIG[idx]
141
- b64_list, names = [], []
142
- for path in ex["images"]:
143
- b64 = encode_full_image(path)
144
- if b64:
145
- b64_list.append(b64)
146
- names.append(os.path.basename(path))
147
- return json.dumps({"images": b64_list, "prompt": ex["prompt"], "names": names, "status": "ok"})
148
-
149
-
150
- print("Building example thumbnails...")
151
- EXAMPLE_CARDS_HTML = build_example_cards_html()
152
- print(f"Built {len(EXAMPLES_CONFIG)} example cards.")
153
-
154
-
155
- def b64_to_pil_list(b64_json_str):
156
- if not b64_json_str or b64_json_str.strip() in ("", "[]"):
157
- return []
158
- try:
159
- b64_list = json.loads(b64_json_str)
160
- except Exception:
161
- return []
162
- pil_images = []
163
- for b64_str in b64_list:
164
- if not b64_str or not isinstance(b64_str, str):
165
- continue
166
- try:
167
- if b64_str.startswith("data:image"):
168
- _, data = b64_str.split(",", 1)
169
- else:
170
- data = b64_str
171
- image_data = base64.b64decode(data)
172
- pil_images.append(Image.open(BytesIO(image_data)).convert("RGB"))
173
- except Exception as e:
174
- print(f"Error decoding image: {e}")
175
- return pil_images
176
-
177
-
178
- def update_dimensions_on_upload(image):
179
- if image is None:
180
- return MAX_OUTPUT_DIM, MAX_OUTPUT_DIM
181
- w, h = image.size
182
- if w > h:
183
- nw = MAX_OUTPUT_DIM
184
- nh = int(nw * h / w)
185
- else:
186
- nh = MAX_OUTPUT_DIM
187
- nw = int(nh * w / h)
188
- return (nw // 8) * 8, (nh // 8) * 8
189
-
190
-
191
- def _readme_from_features(feats):
192
- lines = ["---", "configs:", "- config_name: default",
193
- " data_files:", " - split: train",
194
- " path: data/*.parquet", " features:"]
195
- for name, f in feats.items():
196
- lines.append(f" - name: {name}")
197
- if f.get("_type") == "Image":
198
- lines.append(" dtype: image")
199
- elif f.get("_type") == "Sequence" and f.get("feature", {}).get("_type") == "Image":
200
- lines.append(" sequence: image")
201
- else:
202
- lines.append(f" dtype: {f.get('dtype', 'string')}")
203
- lines.append("---")
204
- return "\n".join(lines) + "\n"
205
-
206
-
207
- def log_inference(pil_inputs, output_pil, prompt, seed, steps, guidance_scale,
208
- input_width, input_height, duration_seconds, success, error_message=""):
209
- if not HF_TOKEN or not DATASET_REPO:
210
- return
211
- try:
212
- import tempfile, json as _json
213
- import pyarrow as pa
214
- import pyarrow.parquet as pq
215
- from huggingface_hub import HfApi, hf_hub_download
216
-
217
- # Image columns need Arrow struct {bytes: binary, path: utf8} plus
218
- # a 'huggingface' schema metadata key for the HF viewer to render them.
219
- img_struct = pa.struct([("bytes", pa.binary()), ("path", pa.string())])
220
- hf_meta = _json.dumps({"info": {"features": {
221
- "timestamp": {"dtype": "string", "_type": "Value"},
222
- "prompt": {"dtype": "string", "_type": "Value"},
223
- "seed": {"dtype": "int32", "_type": "Value"},
224
- "steps": {"dtype": "int32", "_type": "Value"},
225
- "guidance_scale": {"dtype": "float32", "_type": "Value"},
226
- "input_images": {"feature": {"_type": "Image"}, "_type": "Sequence"},
227
- "output_image": {"_type": "Image"},
228
- "duration_seconds": {"dtype": "float32", "_type": "Value"},
229
- "input_width": {"dtype": "int32", "_type": "Value"},
230
- "input_height": {"dtype": "int32", "_type": "Value"},
231
- "success": {"dtype": "bool", "_type": "Value"},
232
- "error_message": {"dtype": "string", "_type": "Value"},
233
- }}}).encode()
234
- schema = pa.schema([
235
- ("timestamp", pa.string()),
236
- ("prompt", pa.string()),
237
- ("seed", pa.int32()),
238
- ("steps", pa.int32()),
239
- ("guidance_scale", pa.float32()),
240
- ("input_images", pa.list_(img_struct)),
241
- ("output_image", img_struct),
242
- ("duration_seconds", pa.float32()),
243
- ("input_width", pa.int32()),
244
- ("input_height", pa.int32()),
245
- ("success", pa.bool_()),
246
- ("error_message", pa.string()),
247
- ], metadata={b"huggingface": hf_meta})
248
-
249
- def _to_jpeg(img, quality=85):
250
- if img is None:
251
- return None
252
- buf = BytesIO()
253
- img.convert("RGB").save(buf, format="JPEG", quality=quality)
254
- return buf.getvalue()
255
-
256
- def _img(b):
257
- return {"bytes": b, "path": None}
258
-
259
- input_jpegs = [_to_jpeg(img) for img in pil_inputs]
260
- output_jpeg = _to_jpeg(output_pil)
261
-
262
- new_table = pa.table({
263
- "timestamp": pa.array([datetime.now(timezone.utc).isoformat()], type=pa.string()),
264
- "prompt": pa.array([prompt], type=pa.string()),
265
- "seed": pa.array([int(seed)], type=pa.int32()),
266
- "steps": pa.array([int(steps)], type=pa.int32()),
267
- "guidance_scale": pa.array([float(guidance_scale)], type=pa.float32()),
268
- "input_images": pa.array([[_img(b) for b in input_jpegs]], type=pa.list_(img_struct)),
269
- "output_image": pa.array([_img(output_jpeg) if output_jpeg else None], type=img_struct),
270
- "duration_seconds": pa.array([float(duration_seconds)], type=pa.float32()),
271
- "input_width": pa.array([int(input_width)], type=pa.int32()),
272
- "input_height": pa.array([int(input_height)], type=pa.int32()),
273
- "success": pa.array([bool(success)], type=pa.bool_()),
274
- "error_message": pa.array([str(error_message)], type=pa.string()),
275
- }, schema=schema)
276
- print(f"[log] built row — success={success}, inputs={len(input_jpegs)}")
277
-
278
- today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
279
- path_in_repo = f"data/{today}.parquet"
280
- api = HfApi(token=HF_TOKEN)
281
- api.create_repo(repo_id=DATASET_REPO, repo_type="dataset", private=True, exist_ok=True)
282
-
283
- # Upload README.md once so the HF viewer knows the column types.
284
- try:
285
- hf_hub_download(repo_id=DATASET_REPO, filename="README.md",
286
- repo_type="dataset", token=HF_TOKEN)
287
- except Exception:
288
- readme = _readme_from_features(_json.loads(hf_meta)["info"]["features"])
289
- api.upload_file(path_or_fileobj=readme.encode(), path_in_repo="README.md",
290
- repo_id=DATASET_REPO, repo_type="dataset")
291
- print("[log] uploaded README.md with feature schema")
292
-
293
- try:
294
- local_path = hf_hub_download(
295
- repo_id=DATASET_REPO, filename=path_in_repo,
296
- repo_type="dataset", token=HF_TOKEN,
297
- )
298
- existing = pq.read_table(local_path)
299
- combined = pa.concat_tables([existing, new_table])
300
- combined = combined.replace_schema_metadata(schema.metadata)
301
- print(f"[log] appending to existing {existing.num_rows} row(s)")
302
- except Exception as dl_err:
303
- print(f"[log] no existing file ({dl_err}), starting fresh")
304
- combined = new_table
305
-
306
- with tempfile.NamedTemporaryFile(suffix=".parquet", delete=False) as tmp:
307
- tmp_path = tmp.name
308
- pq.write_table(combined, tmp_path)
309
- print(f"[log] uploading {path_in_repo} ({combined.num_rows} row(s), {os.path.getsize(tmp_path)//1024}KB)")
310
- api.upload_file(
311
- path_or_fileobj=tmp_path, path_in_repo=path_in_repo,
312
- repo_id=DATASET_REPO, repo_type="dataset",
313
- )
314
- print(f"[log] upload done — {DATASET_REPO}/{path_in_repo}")
315
- except Exception as log_err:
316
- import traceback as _tb
317
- print(f"[log] WARNING: {log_err}\n{_tb.format_exc()}")
318
-
319
-
320
- @spaces.GPU
321
- def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps, progress=gr.Progress(track_tqdm=True)):
322
- import time, traceback
323
- t0 = time.time()
324
-
325
- def _t():
326
- return f"t={time.time()-t0:.1f}s"
327
-
328
- def _mem(sync=False):
329
- if not torch.cuda.is_available():
330
- return "CUDA not available"
331
- if sync:
332
- try:
333
- torch.cuda.synchronize()
334
- except Exception as se:
335
- return f"CUDA sync failed: {se}"
336
- alloc = torch.cuda.memory_allocated() / 1024**3
337
- reserved= torch.cuda.memory_reserved() / 1024**3
338
- peak = torch.cuda.max_memory_allocated()/ 1024**3
339
- return f"alloc={alloc:.2f}GB reserved={reserved:.2f}GB peak={peak:.2f}GB"
340
-
341
- print(f"[infer] ===== START =====")
342
- print(f"[infer] steps={steps}, guidance={guidance_scale}, seed={seed}, randomize={randomize_seed}")
343
- print(f"[infer] prompt={repr(prompt[:120])}")
344
-
345
- if torch.cuda.is_available():
346
- p = torch.cuda.get_device_properties(0)
347
- print(f"[infer] GPU: {p.name}, total={p.total_memory/1024**3:.1f}GB, cap={p.major}.{p.minor}")
348
- torch.cuda.reset_peak_memory_stats()
349
-
350
- print(f"[infer] {_mem()} — {_t()}")
351
-
352
- gc.collect()
353
- torch.cuda.empty_cache()
354
- print(f"[infer] cache cleared — {_mem()}")
355
-
356
- pil_images = b64_to_pil_list(images_b64_json)
357
- print(f"[infer] decoded {len(pil_images)} image(s)")
358
- if not pil_images:
359
- raise gr.Error("Please upload at least one image to edit.")
360
- if not prompt or prompt.strip() == "":
361
- raise gr.Error("Please enter an edit prompt.")
362
-
363
- if randomize_seed:
364
- seed = random.randint(0, MAX_SEED)
365
- generator = torch.Generator(device=device).manual_seed(seed)
366
- 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"
367
- width, height = update_dimensions_on_upload(pil_images[0])
368
- print(f"[infer] input={pil_images[0].size}, output={width}x{height}, seed={seed}")
369
-
370
- # Per-step callback: logs wall-clock time per denoising step after a GPU sync.
371
- # Step 1 time includes any torch.compile Triton kernel compilation — if it is
372
- # much longer than later steps, compilation overhead is the bottleneck.
373
- _step_t = []
374
- def _step_cb(pipeline, step_idx, timestep, cb_kwargs):
375
- torch.cuda.synchronize()
376
- now = time.time()
377
- _step_t.append(now)
378
- delta = now - (_step_t[-2] if len(_step_t) > 1 else t0)
379
- tag = " ← includes compile" if step_idx == 0 else ""
380
- print(f"[infer] step {step_idx+1}/{steps} done — {delta:.1f}s{tag} | {_mem()} | {_t()}")
381
- return cb_kwargs
382
-
383
- t_pipe_start = time.time()
384
- print(f"[infer] calling pipe... {_t()}")
385
- try:
386
- result_image = pipe(
387
- image=pil_images, prompt=prompt, negative_prompt=negative_prompt,
388
- height=height, width=width, num_inference_steps=steps,
389
- generator=generator, true_cfg_scale=guidance_scale,
390
- callback_on_step_end=_step_cb,
391
- callback_on_step_end_tensor_inputs=["latents"],
392
- ).images[0]
393
- print(f"[infer] VAE decode + postprocess done — {_mem(sync=True)} | {_t()}")
394
- duration = time.time() - t_pipe_start
395
- threading.Thread(
396
- target=log_inference,
397
- args=(pil_images, result_image, prompt, seed, steps, guidance_scale,
398
- width, height, duration, True, ""),
399
- daemon=True,
400
- ).start()
401
- return result_image, seed
402
- except Exception as e:
403
- print(f"[infer] ERROR: {type(e).__name__}: {e} | {_t()}")
404
- print(traceback.format_exc())
405
- try:
406
- torch.cuda.synchronize()
407
- except Exception as cuda_err:
408
- print(f"[infer] CUDA synchronize after error: {cuda_err}")
409
- duration = time.time() - t_pipe_start
410
- threading.Thread(
411
- target=log_inference,
412
- args=(pil_images, None, prompt, seed, steps, guidance_scale,
413
- width, height, duration, False, str(e)),
414
- daemon=True,
415
- ).start()
416
- raise e
417
- finally:
418
- gc.collect()
419
- torch.cuda.empty_cache()
420
- print(f"[infer] ===== END {_t()} =====")
421
-
422
-
423
- css = r"""
424
- @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');
425
- *{box-sizing:border-box;margin:0;padding:0}
426
- body,.gradio-container{
427
- background:#0f0f13!important;font-family:'Inter',system-ui,-apple-system,sans-serif!important;
428
- font-size:14px!important;color:#e4e4e7!important;min-height:100vh;
429
- }
430
- .dark body,.dark .gradio-container{background:#0f0f13!important;color:#e4e4e7!important}
431
- footer{display:none!important}
432
- .hidden-input{display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important}
433
-
434
- #example-load-btn{
435
- position:absolute!important;left:-9999px!important;top:-9999px!important;
436
- width:1px!important;height:1px!important;opacity:0.01!important;
437
- pointer-events:none!important;overflow:hidden!important;
438
- }
439
- #gradio-run-btn{
440
- position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;
441
- opacity:0.01;pointer-events:none;overflow:hidden;
442
- }
443
-
444
- .app-shell{
445
- background:#18181b;border:1px solid #27272a;border-radius:16px;
446
- margin:12px auto;max-width:1400px;overflow:hidden;
447
- box-shadow:0 25px 50px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.03);
448
- }
449
- .app-header{
450
- background:linear-gradient(135deg,#18181b,#1e1e24);border-bottom:1px solid #27272a;
451
- padding:14px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;
452
- }
453
- .app-header-left{display:flex;align-items:center;gap:12px}
454
- .app-logo{
455
- width:36px;height:36px;background:linear-gradient(135deg,#1E90FF,#47A3FF,#7CB8FF);
456
- border-radius:10px;display:flex;align-items:center;justify-content:center;
457
- box-shadow:0 4px 12px rgba(30,144,255,.35);
458
- }
459
- .app-logo svg{width:20px;height:20px;fill:#fff;flex-shrink:0}
460
- .app-title{
461
- font-size:18px;font-weight:700;background:linear-gradient(135deg,#e4e4e7,#a1a1aa);
462
- -webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.3px;
463
- }
464
- .app-badge{
465
- font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px;
466
- background:rgba(30,144,255,.15);color:#47A3FF;border:1px solid rgba(30,144,255,.25);letter-spacing:.3px;
467
- }
468
- .app-badge.fast{background:rgba(34,197,94,.12);color:#4ade80;border:1px solid rgba(34,197,94,.25)}
469
-
470
- .app-toolbar{
471
- background:#18181b;border-bottom:1px solid #27272a;padding:8px 16px;
472
- display:flex;gap:4px;align-items:center;flex-wrap:wrap;
473
- }
474
- .tb-sep{width:1px;height:28px;background:#27272a;margin:0 8px}
475
- .modern-tb-btn{
476
- display:inline-flex;align-items:center;justify-content:center;gap:6px;
477
- min-width:32px;height:34px;background:transparent;border:1px solid transparent;
478
- border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;padding:0 12px;
479
- font-family:'Inter',sans-serif;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
480
- transition:all .15s ease;
481
- }
482
- .modern-tb-btn:hover{background:rgba(30,144,255,.15);border-color:rgba(30,144,255,.3)}
483
- .modern-tb-btn:active,.modern-tb-btn.active{background:rgba(30,144,255,.25);border-color:rgba(30,144,255,.45)}
484
- .modern-tb-btn .tb-label{font-size:13px;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;font-weight:600}
485
- .modern-tb-btn .tb-svg{width:15px;height:15px;flex-shrink:0;color:#ffffff!important}
486
- .modern-tb-btn .tb-svg,
487
- .modern-tb-btn .tb-svg *{stroke:#ffffff!important;fill:none!important}
488
- .tb-info{font-family:'JetBrains Mono',monospace;font-size:12px;color:#71717a;padding:0 8px;display:flex;align-items:center}
489
-
490
- body:not(.dark) .modern-tb-btn,body:not(.dark) .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
491
- body:not(.dark) .modern-tb-btn .tb-svg,body:not(.dark) .modern-tb-btn .tb-svg *{stroke:#ffffff!important}
492
- .dark .modern-tb-btn,.dark .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
493
- .dark .modern-tb-btn .tb-svg,.dark .modern-tb-btn .tb-svg *{stroke:#ffffff!important}
494
- .gradio-container .modern-tb-btn,.gradio-container .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
495
- .gradio-container .modern-tb-btn .tb-svg,.gradio-container .modern-tb-btn .tb-svg *{stroke:#ffffff!important}
496
-
497
- .app-main-row{display:flex;gap:0;flex:1;overflow:hidden}
498
- .app-main-left{flex:1;display:flex;flex-direction:column;min-width:0;border-right:1px solid #27272a}
499
- .app-main-right{width:420px;display:flex;flex-direction:column;flex-shrink:0;background:#18181b}
500
-
501
- #gallery-drop-zone{position:relative;background:#09090b;min-height:440px;overflow:auto}
502
- #gallery-drop-zone.drag-over{outline:2px solid #1E90FF;outline-offset:-2px;background:rgba(30,144,255,.04)}
503
-
504
- .upload-prompt-modern{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:20}
505
- .upload-click-area{
506
- display:flex;flex-direction:column;align-items:center;justify-content:center;
507
- cursor:pointer;padding:36px 52px;border:2px dashed #3f3f46;border-radius:16px;
508
- background:rgba(30,144,255,.03);transition:all .2s ease;gap:8px;
509
- }
510
- .upload-click-area:hover{background:rgba(30,144,255,.08);border-color:#1E90FF;transform:scale(1.03)}
511
- .upload-click-area:active{background:rgba(30,144,255,.12);transform:scale(.98)}
512
- .upload-click-area svg{width:80px;height:80px}
513
- .upload-main-text{color:#71717a;font-size:14px;font-weight:500;margin-top:4px}
514
- .upload-sub-text{color:#52525b;font-size:12px}
515
-
516
- .image-gallery-grid{
517
- display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
518
- gap:12px;padding:16px;align-content:start;
519
- }
520
- .gallery-thumb{
521
- position:relative;aspect-ratio:1;border-radius:10px;overflow:hidden;
522
- cursor:pointer;border:2px solid #27272a;transition:all .2s ease;background:#18181b;
523
- }
524
- .gallery-thumb:hover{border-color:#3f3f46;transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.4)}
525
- .gallery-thumb.selected{border-color:#1E90FF!important;box-shadow:0 0 0 3px rgba(30,144,255,.2)}
526
- .gallery-thumb img{width:100%;height:100%;object-fit:cover}
527
- .thumb-badge{
528
- position:absolute;top:6px;left:6px;background:#1E90FF;color:#fff;
529
- padding:2px 8px;border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:600;
530
- }
531
- .thumb-remove{
532
- position:absolute;top:6px;right:6px;width:24px;height:24px;background:rgba(0,0,0,.75);
533
- color:#fff;border:1px solid rgba(255,255,255,.15);border-radius:50%;cursor:pointer;
534
- display:none;align-items:center;justify-content:center;font-size:12px;transition:all .15s;line-height:1;
535
- }
536
- .gallery-thumb:hover .thumb-remove{display:flex}
537
- .thumb-remove:hover{background:#1E90FF;border-color:#1E90FF}
538
- .gallery-add-card{
539
- aspect-ratio:1;border-radius:10px;border:2px dashed #3f3f46;
540
- display:flex;flex-direction:column;align-items:center;justify-content:center;
541
- cursor:pointer;transition:all .2s ease;background:rgba(30,144,255,.03);gap:4px;
542
- }
543
- .gallery-add-card:hover{border-color:#1E90FF;background:rgba(30,144,255,.08)}
544
- .gallery-add-card .add-icon{font-size:28px;color:#71717a;font-weight:300}
545
- .gallery-add-card .add-text{font-size:12px;color:#71717a;font-weight:500}
546
-
547
- .hint-bar{
548
- background:rgba(30,144,255,.06);border-top:1px solid #27272a;border-bottom:1px solid #27272a;
549
- padding:10px 20px;font-size:13px;color:#a1a1aa;line-height:1.7;
550
- }
551
- .hint-bar b{color:#7CB8FF;font-weight:600}
552
- .hint-bar kbd{
553
- display:inline-block;padding:1px 6px;background:#27272a;border:1px solid #3f3f46;
554
- border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;color:#a1a1aa;
555
- }
556
-
557
- .suggestions-section{border-top:1px solid #27272a;padding:12px 16px}
558
- .suggestions-title,.examples-title{
559
- font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;
560
- letter-spacing:.8px;margin-bottom:10px;
561
- }
562
- .suggestions-wrap{display:flex;flex-wrap:wrap;gap:6px}
563
- .suggestion-chip{
564
- display:inline-flex;align-items:center;gap:4px;padding:5px 12px;
565
- background:rgba(30,144,255,.08);border:1px solid rgba(30,144,255,.2);border-radius:20px;
566
- color:#7CB8FF;font-size:12px;font-weight:500;font-family:'Inter',sans-serif;
567
- cursor:pointer;transition:all .15s;white-space:nowrap;
568
- }
569
- .suggestion-chip:hover{background:rgba(30,144,255,.15);border-color:rgba(30,144,255,.35);color:#47A3FF;transform:translateY(-1px)}
570
-
571
- .examples-section{border-top:1px solid #27272a;padding:12px 16px}
572
- .examples-scroll{display:flex;gap:10px;overflow-x:auto;padding-bottom:8px}
573
- .examples-scroll::-webkit-scrollbar{height:6px}
574
- .examples-scroll::-webkit-scrollbar-track{background:#09090b;border-radius:3px}
575
- .examples-scroll::-webkit-scrollbar-thumb{background:#27272a;border-radius:3px}
576
- .examples-scroll::-webkit-scrollbar-thumb:hover{background:#3f3f46}
577
- .example-card{
578
- flex-shrink:0;width:210px;background:#09090b;border:1px solid #27272a;
579
- border-radius:10px;overflow:hidden;cursor:pointer;transition:all .2s ease;
580
- }
581
- .example-card:hover{border-color:#1E90FF;transform:translateY(-2px);box-shadow:0 4px 12px rgba(30,144,255,.15)}
582
- .example-card.loading{opacity:.5;pointer-events:none}
583
- .example-thumbs{display:flex;height:110px;overflow:hidden;background:#18181b}
584
- .example-thumbs img{flex:1;object-fit:cover;min-width:0;border-bottom:1px solid #27272a}
585
- .example-thumb-placeholder{
586
- flex:1;display:flex;align-items:center;justify-content:center;
587
- background:#18181b;color:#3f3f46;font-size:11px;min-width:0;
588
- }
589
- .example-meta{padding:6px 10px;display:flex;align-items:center;gap:6px}
590
- .example-badge{
591
- display:inline-flex;padding:2px 7px;background:rgba(30,144,255,.1);border-radius:4px;
592
- font-size:10px;font-weight:600;color:#47A3FF;font-family:'JetBrains Mono',monospace;white-space:nowrap;
593
- }
594
- .example-prompt-text{
595
- padding:0 10px 8px;font-size:11px;color:#a1a1aa;line-height:1.4;
596
- display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
597
- }
598
-
599
- .panel-card{border-bottom:1px solid #27272a}
600
- .panel-card-title{
601
- padding:12px 20px;font-size:12px;font-weight:600;color:#71717a;
602
- text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6);
603
- }
604
- .panel-card-body{padding:16px 20px;display:flex;flex-direction:column;gap:8px}
605
- .modern-label{font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block}
606
- .modern-textarea{
607
- width:100%;background:#09090b;border:1px solid #27272a;border-radius:8px;
608
- padding:10px 14px;font-family:'Inter',sans-serif;font-size:14px;color:#e4e4e7;
609
- resize:vertical;outline:none;min-height:42px;transition:border-color .2s;
610
- }
611
- .modern-textarea:focus{border-color:#1E90FF;box-shadow:0 0 0 3px rgba(30,144,255,.15)}
612
- .modern-textarea::placeholder{color:#3f3f46}
613
- .modern-textarea.error-flash{
614
- border-color:#ef4444!important;box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;animation:shake .4s ease;
615
- }
616
- @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}}
617
-
618
- .toast-notification{
619
- position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-120%);
620
- z-index:9999;padding:10px 24px;border-radius:10px;font-family:'Inter',sans-serif;
621
- font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px;
622
- box-shadow:0 8px 24px rgba(0,0,0,.5);
623
- transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none;
624
- }
625
- .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}
626
- .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)}
627
- .toast-notification.warning{background:linear-gradient(135deg,#d97706,#b45309);color:#fff;border:1px solid rgba(255,255,255,.15)}
628
- .toast-notification.info{background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;border:1px solid rgba(255,255,255,.15)}
629
- .toast-notification .toast-icon{font-size:16px;line-height:1}
630
- .toast-notification .toast-text{line-height:1.3}
631
-
632
- .btn-run{
633
- display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
634
- background:linear-gradient(135deg,#1E90FF,#1873CC);border:none;border-radius:10px;
635
- padding:12px 24px;cursor:pointer;font-size:15px;font-weight:600;font-family:'Inter',sans-serif;
636
- color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;transition:all .2s ease;letter-spacing:-.2px;
637
- box-shadow:0 4px 16px rgba(30,144,255,.3),inset 0 1px 0 rgba(255,255,255,.1);
638
- }
639
- .btn-run:hover{
640
- background:linear-gradient(135deg,#47A3FF,#1E90FF);transform:translateY(-1px);
641
- box-shadow:0 6px 24px rgba(30,144,255,.45),inset 0 1px 0 rgba(255,255,255,.15);
642
- }
643
- .btn-run:active{transform:translateY(0);box-shadow:0 2px 8px rgba(30,144,255,.3)}
644
- .btn-run svg{width:18px;height:18px;fill:#ffffff!important}
645
- .btn-run svg path{fill:#ffffff!important}
646
- #custom-run-btn,#custom-run-btn *,#custom-run-btn span,#custom-run-btn svg,
647
- #custom-run-btn svg path,#run-btn-label,.btn-run,.btn-run *{
648
- color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
649
- }
650
- body:not(.dark) .btn-run,body:not(.dark) .btn-run *,body:not(.dark) #custom-run-btn,
651
- body:not(.dark) #custom-run-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important}
652
- .dark .btn-run,.dark .btn-run *,.dark #custom-run-btn,.dark #custom-run-btn *{
653
- color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
654
- }
655
- .gradio-container .btn-run,.gradio-container .btn-run *,.gradio-container #custom-run-btn,
656
- .gradio-container #custom-run-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important}
657
-
658
- .output-frame{border-bottom:1px solid #27272a;display:flex;flex-direction:column;position:relative}
659
- .output-frame .out-title{
660
- padding:10px 20px;font-size:13px;font-weight:700;color:#ffffff!important;
661
- -webkit-text-fill-color:#ffffff!important;text-transform:uppercase;letter-spacing:.8px;
662
- border-bottom:1px solid rgba(39,39,42,.6);display:flex;align-items:center;justify-content:space-between;
663
- }
664
- .output-frame .out-title span{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
665
- .output-frame .out-body{
666
- flex:1;background:#09090b;display:flex;align-items:center;justify-content:center;
667
- overflow:hidden;min-height:240px;position:relative;
668
- }
669
- .output-frame .out-body img{max-width:100%;max-height:460px;image-rendering:auto}
670
- .output-frame .out-placeholder{color:#3f3f46;font-size:13px;text-align:center;padding:20px}
671
- .out-download-btn{
672
- display:none;align-items:center;justify-content:center;background:rgba(30,144,255,.1);
673
- border:1px solid rgba(30,144,255,.2);border-radius:6px;cursor:pointer;padding:3px 10px;
674
- font-size:11px;font-weight:500;color:#7CB8FF!important;gap:4px;height:24px;transition:all .15s;
675
- }
676
- .out-download-btn:hover{background:rgba(30,144,255,.2);border-color:rgba(30,144,255,.35);color:#ffffff!important}
677
- .out-download-btn.visible{display:inline-flex}
678
- .out-download-btn svg{width:12px;height:12px;fill:#7CB8FF}
679
-
680
- .modern-loader{
681
- display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(9,9,11,.92);
682
- z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(4px);
683
- }
684
- .modern-loader.active{display:flex}
685
- .modern-loader .loader-spinner{
686
- width:36px;height:36px;border:3px solid #27272a;border-top-color:#1E90FF;
687
- border-radius:50%;animation:spin .8s linear infinite;
688
- }
689
- @keyframes spin{to{transform:rotate(360deg)}}
690
- .modern-loader .loader-text{font-size:13px;color:#a1a1aa;font-weight:500}
691
- .loader-bar-track{width:200px;height:4px;background:#27272a;border-radius:2px;overflow:hidden}
692
- .loader-bar-fill{
693
- height:100%;background:linear-gradient(90deg,#1E90FF,#47A3FF,#1E90FF);
694
- background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px;
695
- }
696
- @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
697
-
698
- .settings-group{border:1px solid #27272a;border-radius:10px;margin:12px 16px;padding:0;overflow:hidden}
699
- .settings-group-title{
700
- font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;
701
- padding:10px 16px;border-bottom:1px solid #27272a;background:rgba(24,24,27,.5);
702
- }
703
- .settings-group-body{padding:14px 16px;display:flex;flex-direction:column;gap:12px}
704
- .slider-row{display:flex;align-items:center;gap:10px;min-height:28px}
705
- .slider-row label{font-size:13px;font-weight:500;color:#a1a1aa;min-width:72px;flex-shrink:0}
706
- .slider-row input[type="range"]{
707
- flex:1;-webkit-appearance:none;appearance:none;height:6px;background:#27272a;
708
- border-radius:3px;outline:none;min-width:0;
709
- }
710
- .slider-row input[type="range"]::-webkit-slider-thumb{
711
- -webkit-appearance:none;width:16px;height:16px;background:linear-gradient(135deg,#1E90FF,#1873CC);
712
- border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(30,144,255,.4);transition:transform .15s;
713
- }
714
- .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)}
715
- .slider-row input[type="range"]::-moz-range-thumb{
716
- width:16px;height:16px;background:linear-gradient(135deg,#1E90FF,#1873CC);
717
- border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(30,144,255,.4);
718
- }
719
- .slider-row .slider-val{
720
- min-width:52px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:12px;
721
- font-weight:500;padding:3px 8px;background:#09090b;border:1px solid #27272a;
722
- border-radius:6px;color:#a1a1aa;flex-shrink:0;
723
- }
724
- .checkbox-row{display:flex;align-items:center;gap:8px;font-size:13px;color:#a1a1aa}
725
- .checkbox-row input[type="checkbox"]{accent-color:#1E90FF;width:16px;height:16px;cursor:pointer}
726
- .checkbox-row label{color:#a1a1aa;font-size:13px;cursor:pointer}
727
-
728
- .app-statusbar{
729
- background:#18181b;border-top:1px solid #27272a;padding:6px 20px;
730
- display:flex;gap:12px;height:34px;align-items:center;font-size:12px;
731
- }
732
- .app-statusbar .sb-section{
733
- padding:0 12px;flex:1;display:flex;align-items:center;font-family:'JetBrains Mono',monospace;
734
- font-size:12px;color:#52525b;overflow:hidden;white-space:nowrap;
735
- }
736
- .app-statusbar .sb-section.sb-fixed{
737
- flex:0 0 auto;min-width:90px;text-align:center;justify-content:center;
738
- padding:3px 12px;background:rgba(30,144,255,.08);border-radius:6px;color:#47A3FF;font-weight:500;
739
- }
740
-
741
- .app-notice{padding:10px 24px;font-size:11px;color:#d4d4d8!important;border-bottom:1px solid #27272a;background:rgba(255,255,255,.015)}
742
- .app-notice a{color:#47A3FF;text-decoration:none}
743
- .app-notice a:hover{text-decoration:underline}
744
- .notice-list{list-style:none;display:flex;flex-direction:column;gap:3px;margin:0;padding:0}
745
- .notice-list li{padding-left:14px;position:relative;line-height:1.6;color:#d4d4d8!important}
746
- .notice-list li::before{content:"·";position:absolute;left:0;color:#d4d4d8!important;font-weight:700}
747
-
748
- .dark .app-shell{background:#18181b}
749
- .dark .upload-prompt-modern{background:transparent}
750
- .dark .panel-card{background:#18181b}
751
- .dark .settings-group{background:#18181b}
752
- .dark .output-frame .out-title{color:#ffffff!important}
753
- .dark .output-frame .out-title span{color:#ffffff!important}
754
- .dark .out-download-btn{color:#7CB8FF!important}
755
- .dark .out-download-btn:hover{color:#ffffff!important}
756
-
757
- ::-webkit-scrollbar{width:8px;height:8px}
758
- ::-webkit-scrollbar-track{background:#09090b}
759
- ::-webkit-scrollbar-thumb{background:#27272a;border-radius:4px}
760
- ::-webkit-scrollbar-thumb:hover{background:#3f3f46}
761
-
762
- @media(max-width:840px){
763
- .app-main-row{flex-direction:column}
764
- .app-main-right{width:100%}
765
- .app-main-left{border-right:none;border-bottom:1px solid #27272a}
766
- }
767
- """
768
-
769
- gallery_js = r"""
770
- () => {
771
- function init() {
772
- if (window.__fireRedInitDone) return;
773
-
774
- const galleryGrid = document.getElementById('image-gallery-grid');
775
- const dropZone = document.getElementById('gallery-drop-zone');
776
- const uploadPrompt = document.getElementById('upload-prompt');
777
- const uploadClick = document.getElementById('upload-click-area');
778
- const fileInput = document.getElementById('custom-file-input');
779
- const btnUpload = document.getElementById('tb-upload');
780
- const btnRemove = document.getElementById('tb-remove');
781
- const btnClear = document.getElementById('tb-clear');
782
- const promptInput = document.getElementById('custom-prompt-input');
783
- const runBtnEl = document.getElementById('custom-run-btn');
784
- const imgCountTb = document.getElementById('tb-image-count');
785
- const imgCountSb = document.getElementById('sb-image-count');
786
-
787
- if (!galleryGrid || !fileInput || !dropZone) {
788
- setTimeout(init, 250);
789
- return;
790
- }
791
-
792
- window.__fireRedInitDone = true;
793
-
794
- let images = [];
795
- window.__uploadedImages = images;
796
- let selectedIdx = -1;
797
- let toastTimer = null;
798
-
799
- function showToast(message, type) {
800
- let toast = document.getElementById('app-toast');
801
- if (!toast) {
802
- toast = document.createElement('div');
803
- toast.id = 'app-toast';
804
- toast.className = 'toast-notification';
805
- toast.innerHTML = '<span class="toast-icon"></span><span class="toast-text"></span>';
806
- document.body.appendChild(toast);
807
- }
808
- const icon = toast.querySelector('.toast-icon');
809
- const text = toast.querySelector('.toast-text');
810
- toast.className = 'toast-notification ' + (type || 'error');
811
- if (type === 'warning') icon.textContent = '\u26A0';
812
- else if (type === 'info') icon.textContent = '\u2139';
813
- else icon.textContent = '\u2717';
814
- text.textContent = message;
815
- if (toastTimer) clearTimeout(toastTimer);
816
- void toast.offsetWidth;
817
- toast.classList.add('visible');
818
- toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500);
819
- }
820
- window.__showToast = showToast;
821
-
822
- function flashPromptError() {
823
- if (!promptInput) return;
824
- promptInput.classList.add('error-flash');
825
- promptInput.focus();
826
- setTimeout(() => promptInput.classList.remove('error-flash'), 800);
827
- }
828
-
829
- function setGradioValue(containerId, value) {
830
- const container = document.getElementById(containerId);
831
- if (!container) return;
832
- container.querySelectorAll('input, textarea').forEach(el => {
833
- if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return;
834
- const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
835
- const ns = Object.getOwnPropertyDescriptor(proto, 'value');
836
- if (ns && ns.set) {
837
- ns.set.call(el, value);
838
- el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
839
- el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
840
- }
841
- });
842
- }
843
- window.__setGradioValue = setGradioValue;
844
-
845
- function syncImagesToGradio() {
846
- window.__uploadedImages = images;
847
- const b64Array = images.map(img => img.b64);
848
- setGradioValue('hidden-images-b64', JSON.stringify(b64Array));
849
- updateCounts();
850
- }
851
-
852
- function syncPromptToGradio() {
853
- if (promptInput) setGradioValue('prompt-gradio-input', promptInput.value);
854
- }
855
-
856
- function updateCounts() {
857
- const n = images.length;
858
- const txt = n > 0 ? n + ' image' + (n > 1 ? 's' : '') : 'No images';
859
- if (imgCountTb) imgCountTb.textContent = txt;
860
- if (imgCountSb) imgCountSb.textContent = n > 0 ? txt + ' uploaded' : 'No images uploaded';
861
- }
862
-
863
- function addImage(b64, name) {
864
- images.push({id: Date.now() + Math.random(), b64: b64, name: name});
865
- renderGallery();
866
- syncImagesToGradio();
867
- }
868
- window.__addImage = addImage;
869
-
870
- function removeImage(idx) {
871
- images.splice(idx, 1);
872
- if (selectedIdx === idx) selectedIdx = -1;
873
- else if (selectedIdx > idx) selectedIdx--;
874
- renderGallery();
875
- syncImagesToGradio();
876
- }
877
-
878
- function clearAll() {
879
- images = [];
880
- window.__uploadedImages = images;
881
- selectedIdx = -1;
882
- renderGallery();
883
- syncImagesToGradio();
884
- }
885
- window.__clearAll = clearAll;
886
-
887
- function selectImage(idx) {
888
- selectedIdx = (selectedIdx === idx) ? -1 : idx;
889
- renderGallery();
890
- }
891
-
892
- function renderGallery() {
893
- if (images.length === 0) {
894
- galleryGrid.innerHTML = '';
895
- galleryGrid.style.display = 'none';
896
- if (uploadPrompt) uploadPrompt.style.display = '';
897
- return;
898
- }
899
- if (uploadPrompt) uploadPrompt.style.display = 'none';
900
- galleryGrid.style.display = 'grid';
901
-
902
- let html = '';
903
- images.forEach((img, i) => {
904
- const sel = i === selectedIdx ? ' selected' : '';
905
- html += '<div class="gallery-thumb' + sel + '" data-idx="' + i + '">'
906
- + '<img src="' + img.b64 + '" alt="' + (img.name||'image') + '">'
907
- + '<span class="thumb-badge">#' + (i+1) + '</span>'
908
- + '<button class="thumb-remove" data-remove="' + i + '">\u2715</button>'
909
- + '</div>';
910
- });
911
- html += '<div class="gallery-add-card" id="gallery-add-card">'
912
- + '<span class="add-icon">+</span>'
913
- + '<span class="add-text">Add</span>'
914
- + '</div>';
915
- galleryGrid.innerHTML = html;
916
-
917
- galleryGrid.querySelectorAll('.gallery-thumb').forEach(thumb => {
918
- thumb.addEventListener('click', (e) => {
919
- if (e.target.closest('.thumb-remove')) return;
920
- selectImage(parseInt(thumb.dataset.idx));
921
- });
922
- });
923
- galleryGrid.querySelectorAll('.thumb-remove').forEach(btn => {
924
- btn.addEventListener('click', (e) => {
925
- e.stopPropagation();
926
- removeImage(parseInt(btn.dataset.remove));
927
- });
928
- });
929
- const addCard = document.getElementById('gallery-add-card');
930
- if (addCard) addCard.addEventListener('click', () => fileInput.click());
931
- }
932
-
933
- function processFiles(files) {
934
- Array.from(files).forEach(file => {
935
- if (!file.type.startsWith('image/')) return;
936
- const reader = new FileReader();
937
- reader.onload = (e) => addImage(e.target.result, file.name);
938
- reader.readAsDataURL(file);
939
- });
940
- }
941
-
942
- fileInput.addEventListener('change', (e) => { processFiles(e.target.files); e.target.value = ''; });
943
- if (uploadClick) uploadClick.addEventListener('click', () => fileInput.click());
944
- if (btnUpload) btnUpload.addEventListener('click', () => fileInput.click());
945
- if (btnRemove) btnRemove.addEventListener('click', () => {
946
- if (selectedIdx >= 0 && selectedIdx < images.length) removeImage(selectedIdx);
947
- });
948
- if (btnClear) btnClear.addEventListener('click', clearAll);
949
-
950
- dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
951
- dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); });
952
- dropZone.addEventListener('drop', (e) => {
953
- e.preventDefault(); dropZone.classList.remove('drag-over');
954
- if (e.dataTransfer.files.length) processFiles(e.dataTransfer.files);
955
- });
956
-
957
- if (promptInput) promptInput.addEventListener('input', syncPromptToGradio);
958
-
959
- window.__setPrompt = function(text) {
960
- if (promptInput) { promptInput.value = text; syncPromptToGradio(); }
961
- };
962
-
963
- document.querySelectorAll('.example-card[data-idx]').forEach(card => {
964
- card.addEventListener('click', () => {
965
- const idx = card.getAttribute('data-idx');
966
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
967
- card.classList.add('loading');
968
- showToast('Loading example...', 'info');
969
-
970
- setGradioValue('example-result-data', '');
971
- setGradioValue('example-idx-input', idx);
972
-
973
- setTimeout(() => {
974
- const btn = document.getElementById('example-load-btn');
975
- if (btn) {
976
- const b = btn.querySelector('button');
977
- if (b) b.click(); else btn.click();
978
- }
979
- }, 150);
980
-
981
- setTimeout(() => card.classList.remove('loading'), 12000);
982
- });
983
- });
984
-
985
- function syncSlider(customId, gradioId) {
986
- const slider = document.getElementById(customId);
987
- const valSpan = document.getElementById(customId + '-val');
988
- if (!slider) return;
989
- slider.addEventListener('input', () => {
990
- if (valSpan) valSpan.textContent = slider.value;
991
- const container = document.getElementById(gradioId);
992
- if (!container) return;
993
- container.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => {
994
- const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
995
- if (ns && ns.set) {
996
- ns.set.call(el, slider.value);
997
- el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
998
- el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
999
- }
1000
- });
1001
- });
1002
- }
1003
- syncSlider('custom-seed', 'gradio-seed');
1004
- syncSlider('custom-guidance', 'gradio-guidance');
1005
- syncSlider('custom-steps', 'gradio-steps');
1006
-
1007
- const randCheck = document.getElementById('custom-randomize');
1008
- if (randCheck) {
1009
- randCheck.addEventListener('change', () => {
1010
- const container = document.getElementById('gradio-randomize');
1011
- if (!container) return;
1012
- const cb = container.querySelector('input[type="checkbox"]');
1013
- if (cb && cb.checked !== randCheck.checked) cb.click();
1014
- });
1015
- }
1016
-
1017
- function showLoader() {
1018
- const l = document.getElementById('output-loader');
1019
- if (l) l.classList.add('active');
1020
- const sb = document.querySelector('.sb-fixed');
1021
- if (sb) sb.textContent = 'Processing...';
1022
- }
1023
- function hideLoader() {
1024
- const l = document.getElementById('output-loader');
1025
- if (l) l.classList.remove('active');
1026
- const sb = document.querySelector('.sb-fixed');
1027
- if (sb) sb.textContent = 'Done';
1028
- }
1029
- window.__showLoader = showLoader;
1030
- window.__hideLoader = hideLoader;
1031
-
1032
- function validateBeforeRun() {
1033
- const promptVal = promptInput ? promptInput.value.trim() : '';
1034
- const hasImages = images.length > 0;
1035
- if (!hasImages && !promptVal) { showToast('Please upload an image and enter a prompt', 'error'); flashPromptError(); return false; }
1036
- if (!hasImages) { showToast('Please upload at least one image', 'error'); return false; }
1037
- if (!promptVal) { showToast('Please enter an edit prompt', 'warning'); flashPromptError(); return false; }
1038
- return true;
1039
- }
1040
-
1041
- window.__clickGradioRunBtn = function() {
1042
- if (!validateBeforeRun()) return;
1043
- syncPromptToGradio(); syncImagesToGradio(); showLoader();
1044
- setTimeout(() => {
1045
- const gradioBtn = document.getElementById('gradio-run-btn');
1046
- if (!gradioBtn) return;
1047
- const btn = gradioBtn.querySelector('button');
1048
- if (btn) btn.click(); else gradioBtn.click();
1049
- }, 200);
1050
- };
1051
-
1052
- if (runBtnEl) runBtnEl.addEventListener('click', () => window.__clickGradioRunBtn());
1053
-
1054
- renderGallery();
1055
- updateCounts();
1056
- }
1057
- init();
1058
- }
1059
- """
1060
-
1061
- wire_outputs_js = r"""
1062
- () => {
1063
- function watchOutputs() {
1064
- const resultContainer = document.getElementById('gradio-result');
1065
- const outBody = document.getElementById('output-image-container');
1066
- const outPh = document.getElementById('output-placeholder');
1067
- const dlBtn = document.getElementById('dl-btn-output');
1068
-
1069
- if (!resultContainer || !outBody) { setTimeout(watchOutputs, 500); return; }
1070
-
1071
- if (dlBtn) {
1072
- dlBtn.addEventListener('click', (e) => {
1073
- e.stopPropagation();
1074
- const img = outBody.querySelector('img.modern-out-img');
1075
- if (img && img.src) {
1076
- const a = document.createElement('a');
1077
- a.href = img.src; a.download = 'firered_output.png';
1078
- document.body.appendChild(a); a.click(); document.body.removeChild(a);
1079
- }
1080
- });
1081
- }
1082
-
1083
- function syncImage() {
1084
- const resultImg = resultContainer.querySelector('img');
1085
- if (resultImg && resultImg.src) {
1086
- if (outPh) outPh.style.display = 'none';
1087
- let existing = outBody.querySelector('img.modern-out-img');
1088
- if (!existing) { existing = document.createElement('img'); existing.className = 'modern-out-img'; outBody.appendChild(existing); }
1089
- if (existing.src !== resultImg.src) {
1090
- existing.src = resultImg.src;
1091
- if (dlBtn) dlBtn.classList.add('visible');
1092
- if (window.__hideLoader) window.__hideLoader();
1093
- }
1094
- }
1095
- }
1096
- const observer = new MutationObserver(syncImage);
1097
- observer.observe(resultContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']});
1098
- setInterval(syncImage, 800);
1099
- }
1100
- watchOutputs();
1101
-
1102
- function watchSeed() {
1103
- const seedContainer = document.getElementById('gradio-seed');
1104
- const seedSlider = document.getElementById('custom-seed');
1105
- const seedVal = document.getElementById('custom-seed-val');
1106
- if (!seedContainer || !seedSlider) { setTimeout(watchSeed, 500); return; }
1107
- function sync() {
1108
- const el = seedContainer.querySelector('input[type="range"],input[type="number"]');
1109
- if (el && el.value) { seedSlider.value = el.value; if (seedVal) seedVal.textContent = el.value; }
1110
- }
1111
- const obs = new MutationObserver(sync);
1112
- obs.observe(seedContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']});
1113
- setInterval(sync, 1000);
1114
- }
1115
- watchSeed();
1116
-
1117
- function watchExampleResults() {
1118
- const container = document.getElementById('example-result-data');
1119
- if (!container) { setTimeout(watchExampleResults, 500); return; }
1120
-
1121
- let lastProcessed = '';
1122
-
1123
- function checkResult() {
1124
- const el = container.querySelector('textarea') || container.querySelector('input');
1125
- if (!el) return;
1126
- const val = el.value;
1127
- if (!val || val === lastProcessed || val.length < 20) return;
1128
-
1129
- try {
1130
- const data = JSON.parse(val);
1131
- if (data.status === 'ok' && data.images && data.images.length > 0) {
1132
- lastProcessed = val;
1133
-
1134
- if (window.__clearAll) window.__clearAll();
1135
- if (window.__setPrompt && data.prompt) window.__setPrompt(data.prompt);
1136
-
1137
- data.images.forEach((b64, i) => {
1138
- if (b64 && window.__addImage) {
1139
- const name = (data.names && data.names[i]) ? data.names[i] : ('example_' + (i+1) + '.jpg');
1140
- window.__addImage(b64, name);
1141
- }
1142
- });
1143
-
1144
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1145
- if (window.__showToast) window.__showToast('Example loaded — ' + data.images.length + ' image(s)', 'info');
1146
- } else if (data.status === 'error') {
1147
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1148
- if (window.__showToast) window.__showToast('Could not load example images', 'error');
1149
- }
1150
- } catch(e) {
1151
- console.error('Example parse error:', e);
1152
- }
1153
- }
1154
-
1155
- const obs = new MutationObserver(checkResult);
1156
- obs.observe(container, {childList:true, subtree:true, characterData:true, attributes:true});
1157
- setInterval(checkResult, 500);
1158
- }
1159
- watchExampleResults();
1160
- }
1161
- """
1162
-
1163
- 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>'
1164
-
1165
- 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>'
1166
-
1167
- 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>'
1168
-
1169
- 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>'
1170
-
1171
- 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>'
1172
-
1173
- with gr.Blocks() as demo:
1174
-
1175
- hidden_images_b64 = gr.Textbox(value="[]", elem_id="hidden-images-b64", elem_classes="hidden-input", container=False)
1176
- prompt = gr.Textbox(value="", elem_id="prompt-gradio-input", elem_classes="hidden-input", container=False)
1177
- seed = gr.Slider(minimum=0, maximum=MAX_SEED, step=1, value=0, elem_id="gradio-seed", elem_classes="hidden-input", container=False)
1178
- randomize_seed = gr.Checkbox(value=True, elem_id="gradio-randomize", elem_classes="hidden-input", container=False)
1179
- 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)
1180
- steps = gr.Slider(minimum=1, maximum=50, step=1, value=4, elem_id="gradio-steps", elem_classes="hidden-input", container=False)
1181
- result = gr.Image(elem_id="gradio-result", elem_classes="hidden-input", container=False, format="png")
1182
-
1183
- example_idx = gr.Textbox(value="", elem_id="example-idx-input", elem_classes="hidden-input", container=False)
1184
- example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False)
1185
- example_load_btn = gr.Button("Load Example", elem_id="example-load-btn")
1186
-
1187
- gr.HTML(f"""
1188
- <div class="app-shell">
1189
-
1190
- <div class="app-header">
1191
- <div class="app-header-left">
1192
- <div class="app-logo">{FIRE_LOGO_SVG}</div>
1193
- <span class="app-title">FireRed-Image-Edit</span>
1194
- <span class="app-badge">v1.1</span>
1195
- <span class="app-badge fast">4-Step Fast</span>
1196
- </div>
1197
- </div>
1198
-
1199
- <div class="app-notice">
1200
- <ul class="notice-list">
1201
- <li>Aims to produce high-resolution 2K output images while keeping the generating speed, based on <a href="https://github.com/PRITHIVSAKTHIUR/FireRed-Image-Edit-1.0-Fast" target="_blank">FireRed-Image-Edit-1.0-Fast</a>.</li>
1202
- <li>Service availability and response times may vary as this space is under development and runs on shared GPU infrastructure.</li>
1203
- <li>When using this Space, please comply with the <a href="https://huggingface.co/content-policy" target="_blank">Hugging Face Content Policy</a>.</li>
1204
- <li>To monitor application performance and improve quality, input data (images/text) and generated outputs are temporarily logged and saved to external storage. Please do not input any personal, sensitive, or confidential information.</li>
1205
- </ul>
1206
- </div>
1207
-
1208
- <div class="app-toolbar">
1209
- <button id="tb-upload" class="modern-tb-btn" title="Upload images">
1210
- {UPLOAD_SVG}<span class="tb-label">Upload</span>
1211
- </button>
1212
- <button id="tb-remove" class="modern-tb-btn" title="Remove selected image">
1213
- {REMOVE_SVG}<span class="tb-label">Remove</span>
1214
- </button>
1215
- <button id="tb-clear" class="modern-tb-btn" title="Clear all images">
1216
- {CLEAR_SVG}<span class="tb-label">Clear All</span>
1217
- </button>
1218
- <div class="tb-sep"></div>
1219
- <span id="tb-image-count" class="tb-info">No images</span>
1220
- </div>
1221
-
1222
- <div class="app-main-row">
1223
- <div class="app-main-left">
1224
- <div id="gallery-drop-zone">
1225
- <div id="upload-prompt" class="upload-prompt-modern">
1226
- <div id="upload-click-area" class="upload-click-area">
1227
- <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
1228
- <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#1E90FF" stroke-width="2" stroke-dasharray="4 3"/>
1229
- <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(30,144,255,0.15)" stroke="#1E90FF" stroke-width="1.5"/>
1230
- <circle cx="28" cy="30" r="6" fill="rgba(30,144,255,0.2)" stroke="#1E90FF" stroke-width="1.5"/>
1231
- </svg>
1232
- <span class="upload-main-text">Click or drag images here</span>
1233
- <span class="upload-sub-text">Supports multiple images for reference-based editing and guided manipulation</span>
1234
- </div>
1235
- </div>
1236
- <input id="custom-file-input" type="file" accept="image/*" multiple style="display:none;" />
1237
- <div id="image-gallery-grid" class="image-gallery-grid" style="display:none;"></div>
1238
- </div>
1239
-
1240
- <div class="hint-bar">
1241
- <b>Upload:</b> Click or drag to add images &nbsp;&middot;&nbsp;
1242
- <b>Multi-image:</b> Upload multiple images for reference-based editing &nbsp;&middot;&nbsp;
1243
- <kbd>Remove</kbd> deletes selected &nbsp;&middot;&nbsp;
1244
- <kbd>Clear All</kbd> removes everything
1245
- </div>
1246
-
1247
- <div class="suggestions-section">
1248
- <div class="suggestions-title">Quick Prompts</div>
1249
- <div class="suggestions-wrap">
1250
- <button class="suggestion-chip" onclick="window.__setPrompt('Transform the image into a dotted cartoon style.')">Cartoon Style</button>
1251
- <button class="suggestion-chip" onclick="window.__setPrompt('Convert it to black and white.')">Black and White</button>
1252
- <button class="suggestion-chip" onclick="window.__setPrompt('Add cinematic lighting with warm orange tones and film grain.')">Cinematic</button>
1253
- <button class="suggestion-chip" onclick="window.__setPrompt('Transform into anime style illustration.')">Anime Style</button>
1254
- <button class="suggestion-chip" onclick="window.__setPrompt('Apply oil painting effect with visible brush strokes.')">Oil Painting</button>
1255
- <button class="suggestion-chip" onclick="window.__setPrompt('Enhance and upscale with more detail and clarity.')">Enhance</button>
1256
- <button class="suggestion-chip" onclick="window.__setPrompt('Make it look like a watercolor painting with soft edges.')">Watercolor</button>
1257
- <button class="suggestion-chip" onclick="window.__setPrompt('Add dramatic sunset sky and warm lighting.')">Sunset Glow</button>
1258
- <button class="suggestion-chip" onclick="window.__setPrompt('Convert to detailed pencil sketch with cross-hatching and shading.')">Pencil Sketch</button>
1259
- <button class="suggestion-chip" onclick="window.__setPrompt('Apply pop art style with bold colors and halftone patterns.')">Pop Art</button>
1260
- <button class="suggestion-chip" onclick="window.__setPrompt('Apply a vintage retro film look with faded colors and light leaks.')">Vintage Retro</button>
1261
- <button class="suggestion-chip" onclick="window.__setPrompt('Add neon glow effects with vibrant colors against a dark background.')">Neon Glow</button>
1262
- <button class="suggestion-chip" onclick="window.__setPrompt('Convert to pixel art style with a retro 16-bit aesthetic.')">Pixel Art</button>
1263
- <button class="suggestion-chip" onclick="window.__setPrompt('Simplify into a clean minimalist illustration with flat colors.')">Minimalist</button>
1264
- <button class="suggestion-chip" onclick="window.__setPrompt('Convert to low poly 3D geometric art style.')">Low Poly 3D</button>
1265
- <button class="suggestion-chip" onclick="window.__setPrompt('Transform into comic book style with bold outlines and cel shading.')">Comic Book</button>
1266
- </div>
1267
- </div>
1268
-
1269
- <div class="examples-section">
1270
- <div class="examples-title">Quick Examples</div>
1271
- <div class="examples-scroll">
1272
- {EXAMPLE_CARDS_HTML}
1273
- </div>
1274
- </div>
1275
- </div>
1276
-
1277
- <div class="app-main-right">
1278
- <div class="panel-card">
1279
- <div class="panel-card-title">Edit Instruction</div>
1280
- <div class="panel-card-body">
1281
- <label class="modern-label" for="custom-prompt-input">Prompt</label>
1282
- <textarea id="custom-prompt-input" class="modern-textarea" rows="3" placeholder="e.g., transform into anime, upscale, change lighting..."></textarea>
1283
- </div>
1284
- </div>
1285
-
1286
- <div style="padding:12px 20px;">
1287
- <button id="custom-run-btn" class="btn-run">
1288
- <svg viewBox="0 0 24 24" 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>
1289
- <span id="run-btn-label">Edit Image</span>
1290
- </button>
1291
- </div>
1292
-
1293
- <div class="output-frame" style="flex:1">
1294
- <div class="out-title">
1295
- <span>Output</span>
1296
- <span id="dl-btn-output" class="out-download-btn" title="Download">
1297
- {DOWNLOAD_SVG} Save
1298
- </span>
1299
- </div>
1300
- <div class="out-body" id="output-image-container">
1301
- <div class="modern-loader" id="output-loader">
1302
- <div class="loader-spinner"></div>
1303
- <div class="loader-text">Processing image...</div>
1304
- <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1305
- </div>
1306
- <div class="out-placeholder" id="output-placeholder">Result will appear here</div>
1307
- </div>
1308
- </div>
1309
-
1310
- <div class="settings-group">
1311
- <div class="settings-group-title">Advanced Settings</div>
1312
- <div class="settings-group-body">
1313
- <div class="slider-row">
1314
- <label>Seed</label>
1315
- <input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0">
1316
- <span class="slider-val" id="custom-seed-val">0</span>
1317
- </div>
1318
- <div class="checkbox-row">
1319
- <input type="checkbox" id="custom-randomize" checked>
1320
- <label for="custom-randomize">Randomize seed</label>
1321
- </div>
1322
- <div class="slider-row">
1323
- <label>Guidance</label>
1324
- <input type="range" id="custom-guidance" min="1" max="10" step="0.1" value="1.0">
1325
- <span class="slider-val" id="custom-guidance-val">1.0</span>
1326
- </div>
1327
- <div class="slider-row">
1328
- <label>Steps</label>
1329
- <input type="range" id="custom-steps" min="1" max="50" step="1" value="4">
1330
- <span class="slider-val" id="custom-steps-val">4</span>
1331
- </div>
1332
- </div>
1333
- </div>
1334
- </div>
1335
- </div>
1336
-
1337
- <div class="app-statusbar">
1338
- <div class="sb-section" id="sb-image-count">No images uploaded</div>
1339
- <div class="sb-section sb-fixed">Ready</div>
1340
- </div>
1341
- </div>
1342
- """)
1343
-
1344
- run_btn = gr.Button("Run", elem_id="gradio-run-btn")
1345
-
1346
- demo.load(fn=None, js=gallery_js)
1347
- demo.load(fn=None, js=wire_outputs_js)
1348
-
1349
- run_btn.click(
1350
- fn=infer,
1351
- inputs=[hidden_images_b64, prompt, seed, randomize_seed, guidance_scale, steps],
1352
- outputs=[result, seed],
1353
- js=r"""(imgs, p, s, rs, gs, st) => {
1354
- const images = window.__uploadedImages || [];
1355
- const b64Array = images.map(img => img.b64);
1356
- const imgsJson = JSON.stringify(b64Array);
1357
- const promptEl = document.getElementById('custom-prompt-input');
1358
- const promptVal = promptEl ? promptEl.value : p;
1359
- return [imgsJson, promptVal, s, rs, gs, st];
1360
- }""",
1361
- )
1362
-
1363
- example_load_btn.click(
1364
- fn=load_example_data,
1365
- inputs=[example_idx],
1366
- outputs=[example_result],
1367
- queue=False,
1368
- )
1369
-
1370
- if __name__ == "__main__":
1371
- demo.queue(max_size=30).launch(
1372
- css=css,
1373
- mcp_server=True,
1374
- ssr_mode=False,
1375
- show_error=True,
1376
- allowed_paths=["examples"],
1377
- )
 
1
+ import os
2
+ import gc
3
+ import threading
4
+ import gradio as gr
5
+ import numpy as np
6
+ import spaces
7
+ import torch
8
+ import random
9
+ import base64
10
+ import json
11
+ import html as html_lib
12
+ from io import BytesIO
13
+ from datetime import datetime, timezone
14
+ from PIL import Image
15
+
16
+ MAX_SEED = np.iinfo(np.int32).max
17
+ LANCZOS = getattr(Image, "Resampling", Image).LANCZOS
18
+ MAX_OUTPUT_DIM = 2048
19
+
20
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
21
+
22
+ print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
23
+ print("torch.__version__ =", torch.__version__)
24
+ print("Using device:", device)
25
+
26
+ # TF32 matmul: ~10-15% free speedup on Ampere/Hopper (bfloat16 accumulation paths benefit too)
27
+ torch.backends.cuda.matmul.allow_tf32 = True
28
+ torch.backends.cudnn.allow_tf32 = True
29
+
30
+ from diffusers import FlowMatchEulerDiscreteScheduler
31
+ from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
32
+ from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
33
+ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
34
+
35
+ dtype = torch.bfloat16
36
+
37
+ pipe = QwenImageEditPlusPipeline.from_pretrained(
38
+ "FireRedTeam/FireRed-Image-Edit-1.1",
39
+ transformer=QwenImageTransformer2DModel.from_pretrained(
40
+ "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19",
41
+ torch_dtype=dtype,
42
+ device_map="cuda",
43
+ ),
44
+ torch_dtype=dtype,
45
+ ).to(device)
46
+
47
+ print("Using default attention processor (FA3 skipped for ZeroGPU GPU-arch compatibility).")
48
+
49
+ print("torch.compile skipped: lazy Triton kernel compilation inside @spaces.GPU always exceeds ZeroGPU's task timeout.")
50
+
51
+ HF_TOKEN = os.environ.get("HF_TOKEN")
52
+ DATASET_REPO = os.environ.get("LOG_DATASET_REPO")
53
+
54
+ EXAMPLES_CONFIG = [
55
+ {
56
+ "images": ["examples/1.jpg"],
57
+ "prompt": "cinematic polaroid with soft grain subtle vignette gentle lighting white frame handwritten photographed 'Fire-Edit' preserving realistic texture and details.",
58
+ },
59
+ {
60
+ "images": ["examples/2.jpg"],
61
+ "prompt": "Transform the image into a dotted cartoon style.",
62
+ },
63
+ {
64
+ "images": ["examples/3.jpeg"],
65
+ "prompt": "Convert it to black and white.",
66
+ },
67
+ {
68
+ "images": ["examples/4.jpg", "examples/5.jpg"],
69
+ "prompt": "Replace her glasses with the new glasses from image 1.",
70
+ },
71
+ {
72
+ "images": ["examples/8.jpg", "examples/9.png"],
73
+ "prompt": "Replace the current clothing with the clothing from the reference image 2. Keep the person's face, hairstyle, body pose, background, lighting, and camera angle unchanged. Ensure the new outfit fits naturally with realistic fabric texture, proper shadows, folds, and accurate proportions. Match the lighting, color tone, and overall style for a seamless and high-quality result.",
74
+ },
75
+ {
76
+ "images": ["examples/10.jpg", "examples/11.png"],
77
+ "prompt": "Replace the current clothing with the clothing from the reference image 2. Keep the person's face, hairstyle, body pose, background, lighting, and camera angle unchanged. Ensure the new outfit fits naturally with realistic fabric texture, proper shadows, folds, and accurate proportions. Match the lighting, color tone, and overall style for a seamless and high-quality result.",
78
+ },
79
+ ]
80
+
81
+
82
+ def make_thumb_b64(path, max_dim=220):
83
+ if not os.path.exists(path):
84
+ return ""
85
+ try:
86
+ img = Image.open(path).convert("RGB")
87
+ img.thumbnail((max_dim, max_dim), LANCZOS)
88
+ buf = BytesIO()
89
+ img.save(buf, format="JPEG", quality=65)
90
+ return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
91
+ except Exception as e:
92
+ print(f"Thumbnail error for {path}: {e}")
93
+ return ""
94
+
95
+
96
+ def encode_full_image(path):
97
+ if not os.path.exists(path):
98
+ return ""
99
+ try:
100
+ with open(path, "rb") as f:
101
+ data = f.read()
102
+ ext = path.rsplit(".", 1)[-1].lower()
103
+ mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp"}.get(ext, "image/jpeg")
104
+ return f"data:{mime};base64,{base64.b64encode(data).decode()}"
105
+ except Exception as e:
106
+ print(f"Encode error for {path}: {e}")
107
+ return ""
108
+
109
+
110
+ def build_example_cards_html():
111
+ cards = ""
112
+ for i, ex in enumerate(EXAMPLES_CONFIG):
113
+ thumbs_html = ""
114
+ for path in ex["images"]:
115
+ thumb = make_thumb_b64(path)
116
+ if thumb:
117
+ thumbs_html += f'<img src="{thumb}" alt="">'
118
+ else:
119
+ thumbs_html += '<div class="example-thumb-placeholder">Preview</div>'
120
+ n = len(ex["images"])
121
+ badge = f'{n} image{"s" if n > 1 else ""}'
122
+ prompt_short = html_lib.escape(ex["prompt"][:90])
123
+ if len(ex["prompt"]) > 90:
124
+ prompt_short += "..."
125
+ cards += f'''<div class="example-card" data-idx="{i}">
126
+ <div class="example-thumbs">{thumbs_html}</div>
127
+ <div class="example-meta"><span class="example-badge">{badge}</span></div>
128
+ <div class="example-prompt-text">{prompt_short}</div>
129
+ </div>'''
130
+ return cards
131
+
132
+
133
+ def load_example_data(idx_str):
134
+ try:
135
+ idx = int(float(idx_str)) if idx_str and idx_str.strip() else -1
136
+ except (ValueError, TypeError):
137
+ idx = -1
138
+ if idx < 0 or idx >= len(EXAMPLES_CONFIG):
139
+ return json.dumps({"images": [], "prompt": "", "names": [], "status": "error"})
140
+ ex = EXAMPLES_CONFIG[idx]
141
+ b64_list, names = [], []
142
+ for path in ex["images"]:
143
+ b64 = encode_full_image(path)
144
+ if b64:
145
+ b64_list.append(b64)
146
+ names.append(os.path.basename(path))
147
+ return json.dumps({"images": b64_list, "prompt": ex["prompt"], "names": names, "status": "ok"})
148
+
149
+
150
+ print("Building example thumbnails...")
151
+ EXAMPLE_CARDS_HTML = build_example_cards_html()
152
+ print(f"Built {len(EXAMPLES_CONFIG)} example cards.")
153
+
154
+
155
+ def b64_to_pil_list(b64_json_str):
156
+ if not b64_json_str or b64_json_str.strip() in ("", "[]"):
157
+ return []
158
+ try:
159
+ b64_list = json.loads(b64_json_str)
160
+ except Exception:
161
+ return []
162
+ pil_images = []
163
+ for b64_str in b64_list:
164
+ if not b64_str or not isinstance(b64_str, str):
165
+ continue
166
+ try:
167
+ if b64_str.startswith("data:image"):
168
+ _, data = b64_str.split(",", 1)
169
+ else:
170
+ data = b64_str
171
+ image_data = base64.b64decode(data)
172
+ pil_images.append(Image.open(BytesIO(image_data)).convert("RGB"))
173
+ except Exception as e:
174
+ print(f"Error decoding image: {e}")
175
+ return pil_images
176
+
177
+
178
+ def update_dimensions_on_upload(image):
179
+ if image is None:
180
+ return MAX_OUTPUT_DIM, MAX_OUTPUT_DIM
181
+ w, h = image.size
182
+ if w > h:
183
+ nw = MAX_OUTPUT_DIM
184
+ nh = int(nw * h / w)
185
+ else:
186
+ nh = MAX_OUTPUT_DIM
187
+ nw = int(nh * w / h)
188
+ return (nw // 8) * 8, (nh // 8) * 8
189
+
190
+
191
+ def _readme_from_features(feats):
192
+ lines = ["---", "configs:", "- config_name: default",
193
+ " data_files:", " - split: train",
194
+ " path: data/*.parquet", " features:"]
195
+ for name, f in feats.items():
196
+ lines.append(f" - name: {name}")
197
+ if f.get("_type") == "Image":
198
+ lines.append(" dtype: image")
199
+ elif f.get("_type") == "Sequence" and f.get("feature", {}).get("_type") == "Image":
200
+ lines.append(" sequence: image")
201
+ else:
202
+ lines.append(f" dtype: {f.get('dtype', 'string')}")
203
+ lines.append("---")
204
+ return "\n".join(lines) + "\n"
205
+
206
+
207
+ def log_inference(pil_inputs, output_pil, prompt, seed, steps, guidance_scale,
208
+ input_width, input_height, duration_seconds, success, error_message=""):
209
+ if not HF_TOKEN or not DATASET_REPO:
210
+ return
211
+ try:
212
+ import tempfile, json as _json
213
+ import pyarrow as pa
214
+ import pyarrow.parquet as pq
215
+ from huggingface_hub import HfApi, hf_hub_download
216
+
217
+ # Image columns need Arrow struct {bytes: binary, path: utf8} plus
218
+ # a 'huggingface' schema metadata key for the HF viewer to render them.
219
+ img_struct = pa.struct([("bytes", pa.binary()), ("path", pa.string())])
220
+ hf_meta = _json.dumps({"info": {"features": {
221
+ "timestamp": {"dtype": "string", "_type": "Value"},
222
+ "prompt": {"dtype": "string", "_type": "Value"},
223
+ "seed": {"dtype": "int32", "_type": "Value"},
224
+ "steps": {"dtype": "int32", "_type": "Value"},
225
+ "guidance_scale": {"dtype": "float32", "_type": "Value"},
226
+ "input_images": {"feature": {"_type": "Image"}, "_type": "Sequence"},
227
+ "output_image": {"_type": "Image"},
228
+ "duration_seconds": {"dtype": "float32", "_type": "Value"},
229
+ "input_width": {"dtype": "int32", "_type": "Value"},
230
+ "input_height": {"dtype": "int32", "_type": "Value"},
231
+ "success": {"dtype": "bool", "_type": "Value"},
232
+ "error_message": {"dtype": "string", "_type": "Value"},
233
+ }}}).encode()
234
+ schema = pa.schema([
235
+ ("timestamp", pa.string()),
236
+ ("prompt", pa.string()),
237
+ ("seed", pa.int32()),
238
+ ("steps", pa.int32()),
239
+ ("guidance_scale", pa.float32()),
240
+ ("input_images", pa.list_(img_struct)),
241
+ ("output_image", img_struct),
242
+ ("duration_seconds", pa.float32()),
243
+ ("input_width", pa.int32()),
244
+ ("input_height", pa.int32()),
245
+ ("success", pa.bool_()),
246
+ ("error_message", pa.string()),
247
+ ], metadata={b"huggingface": hf_meta})
248
+
249
+ def _to_jpeg(img, quality=85):
250
+ if img is None:
251
+ return None
252
+ buf = BytesIO()
253
+ img.convert("RGB").save(buf, format="JPEG", quality=quality)
254
+ return buf.getvalue()
255
+
256
+ def _img(b):
257
+ return {"bytes": b, "path": None}
258
+
259
+ input_jpegs = [_to_jpeg(img) for img in pil_inputs]
260
+ output_jpeg = _to_jpeg(output_pil)
261
+
262
+ new_table = pa.table({
263
+ "timestamp": pa.array([datetime.now(timezone.utc).isoformat()], type=pa.string()),
264
+ "prompt": pa.array([prompt], type=pa.string()),
265
+ "seed": pa.array([int(seed)], type=pa.int32()),
266
+ "steps": pa.array([int(steps)], type=pa.int32()),
267
+ "guidance_scale": pa.array([float(guidance_scale)], type=pa.float32()),
268
+ "input_images": pa.array([[_img(b) for b in input_jpegs]], type=pa.list_(img_struct)),
269
+ "output_image": pa.array([_img(output_jpeg) if output_jpeg else None], type=img_struct),
270
+ "duration_seconds": pa.array([float(duration_seconds)], type=pa.float32()),
271
+ "input_width": pa.array([int(input_width)], type=pa.int32()),
272
+ "input_height": pa.array([int(input_height)], type=pa.int32()),
273
+ "success": pa.array([bool(success)], type=pa.bool_()),
274
+ "error_message": pa.array([str(error_message)], type=pa.string()),
275
+ }, schema=schema)
276
+ print(f"[log] built row — success={success}, inputs={len(input_jpegs)}")
277
+
278
+ today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
279
+ path_in_repo = f"data/{today}.parquet"
280
+ api = HfApi(token=HF_TOKEN)
281
+ api.create_repo(repo_id=DATASET_REPO, repo_type="dataset", private=True, exist_ok=True)
282
+
283
+ # Upload README.md once so the HF viewer knows the column types.
284
+ try:
285
+ hf_hub_download(repo_id=DATASET_REPO, filename="README.md",
286
+ repo_type="dataset", token=HF_TOKEN)
287
+ except Exception:
288
+ readme = _readme_from_features(_json.loads(hf_meta)["info"]["features"])
289
+ api.upload_file(path_or_fileobj=readme.encode(), path_in_repo="README.md",
290
+ repo_id=DATASET_REPO, repo_type="dataset")
291
+ print("[log] uploaded README.md with feature schema")
292
+
293
+ try:
294
+ local_path = hf_hub_download(
295
+ repo_id=DATASET_REPO, filename=path_in_repo,
296
+ repo_type="dataset", token=HF_TOKEN,
297
+ )
298
+ existing = pq.read_table(local_path)
299
+ combined = pa.concat_tables([existing, new_table])
300
+ combined = combined.replace_schema_metadata(schema.metadata)
301
+ print(f"[log] appending to existing {existing.num_rows} row(s)")
302
+ except Exception as dl_err:
303
+ print(f"[log] no existing file ({dl_err}), starting fresh")
304
+ combined = new_table
305
+
306
+ with tempfile.NamedTemporaryFile(suffix=".parquet", delete=False) as tmp:
307
+ tmp_path = tmp.name
308
+ pq.write_table(combined, tmp_path)
309
+ print(f"[log] uploading {path_in_repo} ({combined.num_rows} row(s), {os.path.getsize(tmp_path)//1024}KB)")
310
+ api.upload_file(
311
+ path_or_fileobj=tmp_path, path_in_repo=path_in_repo,
312
+ repo_id=DATASET_REPO, repo_type="dataset",
313
+ )
314
+ print(f"[log] upload done — {DATASET_REPO}/{path_in_repo}")
315
+ except Exception as log_err:
316
+ import traceback as _tb
317
+ print(f"[log] WARNING: {log_err}\n{_tb.format_exc()}")
318
+
319
+
320
+ @spaces.GPU
321
+ def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps, progress=gr.Progress(track_tqdm=True)):
322
+ import time, traceback
323
+ t0 = time.time()
324
+
325
+ def _t():
326
+ return f"t={time.time()-t0:.1f}s"
327
+
328
+ def _mem(sync=False):
329
+ if not torch.cuda.is_available():
330
+ return "CUDA not available"
331
+ if sync:
332
+ try:
333
+ torch.cuda.synchronize()
334
+ except Exception as se:
335
+ return f"CUDA sync failed: {se}"
336
+ alloc = torch.cuda.memory_allocated() / 1024**3
337
+ reserved= torch.cuda.memory_reserved() / 1024**3
338
+ peak = torch.cuda.max_memory_allocated()/ 1024**3
339
+ return f"alloc={alloc:.2f}GB reserved={reserved:.2f}GB peak={peak:.2f}GB"
340
+
341
+ print(f"[infer] ===== START =====")
342
+ print(f"[infer] steps={steps}, guidance={guidance_scale}, seed={seed}, randomize={randomize_seed}")
343
+ print(f"[infer] prompt={repr(prompt[:120])}")
344
+
345
+ if torch.cuda.is_available():
346
+ p = torch.cuda.get_device_properties(0)
347
+ print(f"[infer] GPU: {p.name}, total={p.total_memory/1024**3:.1f}GB, cap={p.major}.{p.minor}")
348
+ torch.cuda.reset_peak_memory_stats()
349
+
350
+ print(f"[infer] {_mem()} — {_t()}")
351
+
352
+ gc.collect()
353
+ torch.cuda.empty_cache()
354
+ print(f"[infer] cache cleared — {_mem()}")
355
+
356
+ pil_images = b64_to_pil_list(images_b64_json)
357
+ print(f"[infer] decoded {len(pil_images)} image(s)")
358
+ if not pil_images:
359
+ raise gr.Error("Please upload at least one image to edit.")
360
+ if not prompt or prompt.strip() == "":
361
+ raise gr.Error("Please enter an edit prompt.")
362
+
363
+ if randomize_seed:
364
+ seed = random.randint(0, MAX_SEED)
365
+ generator = torch.Generator(device=device).manual_seed(seed)
366
+ 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"
367
+ width, height = update_dimensions_on_upload(pil_images[0])
368
+ print(f"[infer] input={pil_images[0].size}, output={width}x{height}, seed={seed}")
369
+
370
+ # Per-step callback: logs wall-clock time per denoising step after a GPU sync.
371
+ # Step 1 time includes any torch.compile Triton kernel compilation — if it is
372
+ # much longer than later steps, compilation overhead is the bottleneck.
373
+ _step_t = []
374
+ def _step_cb(pipeline, step_idx, timestep, cb_kwargs):
375
+ torch.cuda.synchronize()
376
+ now = time.time()
377
+ _step_t.append(now)
378
+ delta = now - (_step_t[-2] if len(_step_t) > 1 else t0)
379
+ tag = " ← includes compile" if step_idx == 0 else ""
380
+ print(f"[infer] step {step_idx+1}/{steps} done — {delta:.1f}s{tag} | {_mem()} | {_t()}")
381
+ return cb_kwargs
382
+
383
+ t_pipe_start = time.time()
384
+ print(f"[infer] calling pipe... {_t()}")
385
+ try:
386
+ result_image = pipe(
387
+ image=pil_images, prompt=prompt, negative_prompt=negative_prompt,
388
+ height=height, width=width, num_inference_steps=steps,
389
+ generator=generator, true_cfg_scale=guidance_scale,
390
+ callback_on_step_end=_step_cb,
391
+ callback_on_step_end_tensor_inputs=["latents"],
392
+ ).images[0]
393
+ print(f"[infer] VAE decode + postprocess done — {_mem(sync=True)} | {_t()}")
394
+ duration = time.time() - t_pipe_start
395
+ threading.Thread(
396
+ target=log_inference,
397
+ args=(pil_images, result_image, prompt, seed, steps, guidance_scale,
398
+ width, height, duration, True, ""),
399
+ daemon=True,
400
+ ).start()
401
+ return result_image, seed
402
+ except Exception as e:
403
+ print(f"[infer] ERROR: {type(e).__name__}: {e} | {_t()}")
404
+ print(traceback.format_exc())
405
+ try:
406
+ torch.cuda.synchronize()
407
+ except Exception as cuda_err:
408
+ print(f"[infer] CUDA synchronize after error: {cuda_err}")
409
+ duration = time.time() - t_pipe_start
410
+ threading.Thread(
411
+ target=log_inference,
412
+ args=(pil_images, None, prompt, seed, steps, guidance_scale,
413
+ width, height, duration, False, str(e)),
414
+ daemon=True,
415
+ ).start()
416
+ raise e
417
+ finally:
418
+ gc.collect()
419
+ torch.cuda.empty_cache()
420
+ print(f"[infer] ===== END {_t()} =====")
421
+
422
+
423
+ # ── static assets ─────────────────────────────────────────────────────────────
424
+
425
+ with open("static/app.css") as _f:
426
+ css = _f.read()
427
+
428
+ with open("static/gallery.js") as _f:
429
+ gallery_js = _f.read()
430
+
431
+ with open("static/wire_outputs.js") as _f:
432
+ wire_outputs_js = _f.read()
433
+
434
+ with open("static/run_preprocess.js") as _f:
435
+ run_preprocess_js = _f.read()
436
+
437
+ # ── SVG icon constants (inlined into the HTML template) ───────────────────────
438
+
439
+ 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>'
440
+
441
+ 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>'
442
+
443
+ 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>'
444
+
445
+ 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>'
446
+
447
+ 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>'
448
+
449
+ # ── HTML template ──────────────────────────────────────────────────────────────
450
+
451
+ with open("templates/app.html") as _f:
452
+ app_html = _f.read().format(
453
+ fire_logo_svg=FIRE_LOGO_SVG,
454
+ upload_svg=UPLOAD_SVG,
455
+ remove_svg=REMOVE_SVG,
456
+ clear_svg=CLEAR_SVG,
457
+ download_svg=DOWNLOAD_SVG,
458
+ example_cards_html=EXAMPLE_CARDS_HTML,
459
+ )
460
+
461
+ # ── Gradio blocks ──────────────────────────────────────────────────────────────
462
+
463
+ with gr.Blocks() as demo:
464
+
465
+ hidden_images_b64 = gr.Textbox(value="[]", elem_id="hidden-images-b64", elem_classes="hidden-input", container=False)
466
+ prompt = gr.Textbox(value="", elem_id="prompt-gradio-input", elem_classes="hidden-input", container=False)
467
+ seed = gr.Slider(minimum=0, maximum=MAX_SEED, step=1, value=0, elem_id="gradio-seed", elem_classes="hidden-input", container=False)
468
+ randomize_seed = gr.Checkbox(value=True, elem_id="gradio-randomize", elem_classes="hidden-input", container=False)
469
+ 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)
470
+ steps = gr.Slider(minimum=1, maximum=50, step=1, value=4, elem_id="gradio-steps", elem_classes="hidden-input", container=False)
471
+ result = gr.Image(elem_id="gradio-result", elem_classes="hidden-input", container=False, format="png")
472
+
473
+ example_idx = gr.Textbox(value="", elem_id="example-idx-input", elem_classes="hidden-input", container=False)
474
+ example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False)
475
+ example_load_btn = gr.Button("Load Example", elem_id="example-load-btn")
476
+
477
+ gr.HTML(app_html)
478
+
479
+ run_btn = gr.Button("Run", elem_id="gradio-run-btn")
480
+
481
+ demo.load(fn=None, js=gallery_js)
482
+ demo.load(fn=None, js=wire_outputs_js)
483
+
484
+ run_btn.click(
485
+ fn=infer,
486
+ inputs=[hidden_images_b64, prompt, seed, randomize_seed, guidance_scale, steps],
487
+ outputs=[result, seed],
488
+ js=run_preprocess_js,
489
+ )
490
+
491
+ example_load_btn.click(
492
+ fn=load_example_data,
493
+ inputs=[example_idx],
494
+ outputs=[example_result],
495
+ queue=False,
496
+ )
497
+
498
+ if __name__ == "__main__":
499
+ demo.queue(max_size=30).launch(
500
+ css=css,
501
+ mcp_server=True,
502
+ ssr_mode=False,
503
+ show_error=True,
504
+ allowed_paths=["examples"],
505
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/app.css ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @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');
2
+ *{box-sizing:border-box;margin:0;padding:0}
3
+ body,.gradio-container{
4
+ background:#0f0f13!important;font-family:'Inter',system-ui,-apple-system,sans-serif!important;
5
+ font-size:14px!important;color:#e4e4e7!important;min-height:100vh;
6
+ }
7
+ .dark body,.dark .gradio-container{background:#0f0f13!important;color:#e4e4e7!important}
8
+ footer{display:none!important}
9
+ .hidden-input{display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important}
10
+
11
+ #example-load-btn{
12
+ position:absolute!important;left:-9999px!important;top:-9999px!important;
13
+ width:1px!important;height:1px!important;opacity:0.01!important;
14
+ pointer-events:none!important;overflow:hidden!important;
15
+ }
16
+ #gradio-run-btn{
17
+ position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;
18
+ opacity:0.01;pointer-events:none;overflow:hidden;
19
+ }
20
+
21
+ .app-shell{
22
+ background:#18181b;border:1px solid #27272a;border-radius:16px;
23
+ margin:12px auto;max-width:1400px;overflow:hidden;
24
+ box-shadow:0 25px 50px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.03);
25
+ }
26
+ .app-header{
27
+ background:linear-gradient(135deg,#18181b,#1e1e24);border-bottom:1px solid #27272a;
28
+ padding:14px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;
29
+ }
30
+ .app-header-left{display:flex;align-items:center;gap:12px}
31
+ .app-logo{
32
+ width:36px;height:36px;background:linear-gradient(135deg,#1E90FF,#47A3FF,#7CB8FF);
33
+ border-radius:10px;display:flex;align-items:center;justify-content:center;
34
+ box-shadow:0 4px 12px rgba(30,144,255,.35);
35
+ }
36
+ .app-logo svg{width:20px;height:20px;fill:#fff;flex-shrink:0}
37
+ .app-title{
38
+ font-size:18px;font-weight:700;background:linear-gradient(135deg,#e4e4e7,#a1a1aa);
39
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.3px;
40
+ }
41
+ .app-badge{
42
+ font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px;
43
+ background:rgba(30,144,255,.15);color:#47A3FF;border:1px solid rgba(30,144,255,.25);letter-spacing:.3px;
44
+ }
45
+ .app-badge.fast{background:rgba(34,197,94,.12);color:#4ade80;border:1px solid rgba(34,197,94,.25)}
46
+
47
+ .app-toolbar{
48
+ background:#18181b;border-bottom:1px solid #27272a;padding:8px 16px;
49
+ display:flex;gap:4px;align-items:center;flex-wrap:wrap;
50
+ }
51
+ .tb-sep{width:1px;height:28px;background:#27272a;margin:0 8px}
52
+ .modern-tb-btn{
53
+ display:inline-flex;align-items:center;justify-content:center;gap:6px;
54
+ min-width:32px;height:34px;background:transparent;border:1px solid transparent;
55
+ border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;padding:0 12px;
56
+ font-family:'Inter',sans-serif;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
57
+ transition:all .15s ease;
58
+ }
59
+ .modern-tb-btn:hover{background:rgba(30,144,255,.15);border-color:rgba(30,144,255,.3)}
60
+ .modern-tb-btn:active,.modern-tb-btn.active{background:rgba(30,144,255,.25);border-color:rgba(30,144,255,.45)}
61
+ .modern-tb-btn .tb-label{font-size:13px;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;font-weight:600}
62
+ .modern-tb-btn .tb-svg{width:15px;height:15px;flex-shrink:0;color:#ffffff!important}
63
+ .modern-tb-btn .tb-svg,
64
+ .modern-tb-btn .tb-svg *{stroke:#ffffff!important;fill:none!important}
65
+ .tb-info{font-family:'JetBrains Mono',monospace;font-size:12px;color:#71717a;padding:0 8px;display:flex;align-items:center}
66
+
67
+ body:not(.dark) .modern-tb-btn,body:not(.dark) .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
68
+ body:not(.dark) .modern-tb-btn .tb-svg,body:not(.dark) .modern-tb-btn .tb-svg *{stroke:#ffffff!important}
69
+ .dark .modern-tb-btn,.dark .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
70
+ .dark .modern-tb-btn .tb-svg,.dark .modern-tb-btn .tb-svg *{stroke:#ffffff!important}
71
+ .gradio-container .modern-tb-btn,.gradio-container .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
72
+ .gradio-container .modern-tb-btn .tb-svg,.gradio-container .modern-tb-btn .tb-svg *{stroke:#ffffff!important}
73
+
74
+ .app-main-row{display:flex;gap:0;flex:1;overflow:hidden}
75
+ .app-main-left{flex:1;display:flex;flex-direction:column;min-width:0;border-right:1px solid #27272a}
76
+ .app-main-right{width:420px;display:flex;flex-direction:column;flex-shrink:0;background:#18181b}
77
+
78
+ #gallery-drop-zone{position:relative;background:#09090b;min-height:440px;overflow:auto}
79
+ #gallery-drop-zone.drag-over{outline:2px solid #1E90FF;outline-offset:-2px;background:rgba(30,144,255,.04)}
80
+
81
+ .upload-prompt-modern{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:20}
82
+ .upload-click-area{
83
+ display:flex;flex-direction:column;align-items:center;justify-content:center;
84
+ cursor:pointer;padding:36px 52px;border:2px dashed #3f3f46;border-radius:16px;
85
+ background:rgba(30,144,255,.03);transition:all .2s ease;gap:8px;
86
+ }
87
+ .upload-click-area:hover{background:rgba(30,144,255,.08);border-color:#1E90FF;transform:scale(1.03)}
88
+ .upload-click-area:active{background:rgba(30,144,255,.12);transform:scale(.98)}
89
+ .upload-click-area svg{width:80px;height:80px}
90
+ .upload-main-text{color:#71717a;font-size:14px;font-weight:500;margin-top:4px}
91
+ .upload-sub-text{color:#52525b;font-size:12px}
92
+
93
+ .image-gallery-grid{
94
+ display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
95
+ gap:12px;padding:16px;align-content:start;
96
+ }
97
+ .gallery-thumb{
98
+ position:relative;aspect-ratio:1;border-radius:10px;overflow:hidden;
99
+ cursor:pointer;border:2px solid #27272a;transition:all .2s ease;background:#18181b;
100
+ }
101
+ .gallery-thumb:hover{border-color:#3f3f46;transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.4)}
102
+ .gallery-thumb.selected{border-color:#1E90FF!important;box-shadow:0 0 0 3px rgba(30,144,255,.2)}
103
+ .gallery-thumb img{width:100%;height:100%;object-fit:cover}
104
+ .thumb-badge{
105
+ position:absolute;top:6px;left:6px;background:#1E90FF;color:#fff;
106
+ padding:2px 8px;border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:600;
107
+ }
108
+ .thumb-remove{
109
+ position:absolute;top:6px;right:6px;width:24px;height:24px;background:rgba(0,0,0,.75);
110
+ color:#fff;border:1px solid rgba(255,255,255,.15);border-radius:50%;cursor:pointer;
111
+ display:none;align-items:center;justify-content:center;font-size:12px;transition:all .15s;line-height:1;
112
+ }
113
+ .gallery-thumb:hover .thumb-remove{display:flex}
114
+ .thumb-remove:hover{background:#1E90FF;border-color:#1E90FF}
115
+ .gallery-add-card{
116
+ aspect-ratio:1;border-radius:10px;border:2px dashed #3f3f46;
117
+ display:flex;flex-direction:column;align-items:center;justify-content:center;
118
+ cursor:pointer;transition:all .2s ease;background:rgba(30,144,255,.03);gap:4px;
119
+ }
120
+ .gallery-add-card:hover{border-color:#1E90FF;background:rgba(30,144,255,.08)}
121
+ .gallery-add-card .add-icon{font-size:28px;color:#71717a;font-weight:300}
122
+ .gallery-add-card .add-text{font-size:12px;color:#71717a;font-weight:500}
123
+
124
+ .hint-bar{
125
+ background:rgba(30,144,255,.06);border-top:1px solid #27272a;border-bottom:1px solid #27272a;
126
+ padding:10px 20px;font-size:13px;color:#a1a1aa;line-height:1.7;
127
+ }
128
+ .hint-bar b{color:#7CB8FF;font-weight:600}
129
+ .hint-bar kbd{
130
+ display:inline-block;padding:1px 6px;background:#27272a;border:1px solid #3f3f46;
131
+ border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;color:#a1a1aa;
132
+ }
133
+
134
+ .suggestions-section{border-top:1px solid #27272a;padding:12px 16px}
135
+ .suggestions-title,.examples-title{
136
+ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;
137
+ letter-spacing:.8px;margin-bottom:10px;
138
+ }
139
+ .suggestions-wrap{display:flex;flex-wrap:wrap;gap:6px}
140
+ .suggestion-chip{
141
+ display:inline-flex;align-items:center;gap:4px;padding:5px 12px;
142
+ background:rgba(30,144,255,.08);border:1px solid rgba(30,144,255,.2);border-radius:20px;
143
+ color:#7CB8FF;font-size:12px;font-weight:500;font-family:'Inter',sans-serif;
144
+ cursor:pointer;transition:all .15s;white-space:nowrap;
145
+ }
146
+ .suggestion-chip:hover{background:rgba(30,144,255,.15);border-color:rgba(30,144,255,.35);color:#47A3FF;transform:translateY(-1px)}
147
+
148
+ .examples-section{border-top:1px solid #27272a;padding:12px 16px}
149
+ .examples-scroll{display:flex;gap:10px;overflow-x:auto;padding-bottom:8px}
150
+ .examples-scroll::-webkit-scrollbar{height:6px}
151
+ .examples-scroll::-webkit-scrollbar-track{background:#09090b;border-radius:3px}
152
+ .examples-scroll::-webkit-scrollbar-thumb{background:#27272a;border-radius:3px}
153
+ .examples-scroll::-webkit-scrollbar-thumb:hover{background:#3f3f46}
154
+ .example-card{
155
+ flex-shrink:0;width:210px;background:#09090b;border:1px solid #27272a;
156
+ border-radius:10px;overflow:hidden;cursor:pointer;transition:all .2s ease;
157
+ }
158
+ .example-card:hover{border-color:#1E90FF;transform:translateY(-2px);box-shadow:0 4px 12px rgba(30,144,255,.15)}
159
+ .example-card.loading{opacity:.5;pointer-events:none}
160
+ .example-thumbs{display:flex;height:110px;overflow:hidden;background:#18181b}
161
+ .example-thumbs img{flex:1;object-fit:cover;min-width:0;border-bottom:1px solid #27272a}
162
+ .example-thumb-placeholder{
163
+ flex:1;display:flex;align-items:center;justify-content:center;
164
+ background:#18181b;color:#3f3f46;font-size:11px;min-width:0;
165
+ }
166
+ .example-meta{padding:6px 10px;display:flex;align-items:center;gap:6px}
167
+ .example-badge{
168
+ display:inline-flex;padding:2px 7px;background:rgba(30,144,255,.1);border-radius:4px;
169
+ font-size:10px;font-weight:600;color:#47A3FF;font-family:'JetBrains Mono',monospace;white-space:nowrap;
170
+ }
171
+ .example-prompt-text{
172
+ padding:0 10px 8px;font-size:11px;color:#a1a1aa;line-height:1.4;
173
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
174
+ }
175
+
176
+ .panel-card{border-bottom:1px solid #27272a}
177
+ .panel-card-title{
178
+ padding:12px 20px;font-size:12px;font-weight:600;color:#71717a;
179
+ text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6);
180
+ }
181
+ .panel-card-body{padding:16px 20px;display:flex;flex-direction:column;gap:8px}
182
+ .modern-label{font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block}
183
+ .modern-textarea{
184
+ width:100%;background:#09090b;border:1px solid #27272a;border-radius:8px;
185
+ padding:10px 14px;font-family:'Inter',sans-serif;font-size:14px;color:#e4e4e7;
186
+ resize:vertical;outline:none;min-height:42px;transition:border-color .2s;
187
+ }
188
+ .modern-textarea:focus{border-color:#1E90FF;box-shadow:0 0 0 3px rgba(30,144,255,.15)}
189
+ .modern-textarea::placeholder{color:#3f3f46}
190
+ .modern-textarea.error-flash{
191
+ border-color:#ef4444!important;box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;animation:shake .4s ease;
192
+ }
193
+ @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}}
194
+
195
+ .toast-notification{
196
+ position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-120%);
197
+ z-index:9999;padding:10px 24px;border-radius:10px;font-family:'Inter',sans-serif;
198
+ font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px;
199
+ box-shadow:0 8px 24px rgba(0,0,0,.5);
200
+ transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none;
201
+ }
202
+ .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}
203
+ .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)}
204
+ .toast-notification.warning{background:linear-gradient(135deg,#d97706,#b45309);color:#fff;border:1px solid rgba(255,255,255,.15)}
205
+ .toast-notification.info{background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;border:1px solid rgba(255,255,255,.15)}
206
+ .toast-notification .toast-icon{font-size:16px;line-height:1}
207
+ .toast-notification .toast-text{line-height:1.3}
208
+
209
+ .btn-run{
210
+ display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
211
+ background:linear-gradient(135deg,#1E90FF,#1873CC);border:none;border-radius:10px;
212
+ padding:12px 24px;cursor:pointer;font-size:15px;font-weight:600;font-family:'Inter',sans-serif;
213
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;transition:all .2s ease;letter-spacing:-.2px;
214
+ box-shadow:0 4px 16px rgba(30,144,255,.3),inset 0 1px 0 rgba(255,255,255,.1);
215
+ }
216
+ .btn-run:hover{
217
+ background:linear-gradient(135deg,#47A3FF,#1E90FF);transform:translateY(-1px);
218
+ box-shadow:0 6px 24px rgba(30,144,255,.45),inset 0 1px 0 rgba(255,255,255,.15);
219
+ }
220
+ .btn-run:active{transform:translateY(0);box-shadow:0 2px 8px rgba(30,144,255,.3)}
221
+ .btn-run svg{width:18px;height:18px;fill:#ffffff!important}
222
+ .btn-run svg path{fill:#ffffff!important}
223
+ #custom-run-btn,#custom-run-btn *,#custom-run-btn span,#custom-run-btn svg,
224
+ #custom-run-btn svg path,#run-btn-label,.btn-run,.btn-run *{
225
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
226
+ }
227
+ body:not(.dark) .btn-run,body:not(.dark) .btn-run *,body:not(.dark) #custom-run-btn,
228
+ body:not(.dark) #custom-run-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important}
229
+ .dark .btn-run,.dark .btn-run *,.dark #custom-run-btn,.dark #custom-run-btn *{
230
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
231
+ }
232
+ .gradio-container .btn-run,.gradio-container .btn-run *,.gradio-container #custom-run-btn,
233
+ .gradio-container #custom-run-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important}
234
+
235
+ .output-frame{border-bottom:1px solid #27272a;display:flex;flex-direction:column;position:relative}
236
+ .output-frame .out-title{
237
+ padding:10px 20px;font-size:13px;font-weight:700;color:#ffffff!important;
238
+ -webkit-text-fill-color:#ffffff!important;text-transform:uppercase;letter-spacing:.8px;
239
+ border-bottom:1px solid rgba(39,39,42,.6);display:flex;align-items:center;justify-content:space-between;
240
+ }
241
+ .output-frame .out-title span{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
242
+ .output-frame .out-body{
243
+ flex:1;background:#09090b;display:flex;align-items:center;justify-content:center;
244
+ overflow:hidden;min-height:240px;position:relative;
245
+ }
246
+ .output-frame .out-body img{max-width:100%;max-height:460px;image-rendering:auto}
247
+ .output-frame .out-placeholder{color:#3f3f46;font-size:13px;text-align:center;padding:20px}
248
+ .out-download-btn{
249
+ display:none;align-items:center;justify-content:center;background:rgba(30,144,255,.1);
250
+ border:1px solid rgba(30,144,255,.2);border-radius:6px;cursor:pointer;padding:3px 10px;
251
+ font-size:11px;font-weight:500;color:#7CB8FF!important;gap:4px;height:24px;transition:all .15s;
252
+ }
253
+ .out-download-btn:hover{background:rgba(30,144,255,.2);border-color:rgba(30,144,255,.35);color:#ffffff!important}
254
+ .out-download-btn.visible{display:inline-flex}
255
+ .out-download-btn svg{width:12px;height:12px;fill:#7CB8FF}
256
+
257
+ .modern-loader{
258
+ display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(9,9,11,.92);
259
+ z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(4px);
260
+ }
261
+ .modern-loader.active{display:flex}
262
+ .modern-loader .loader-spinner{
263
+ width:36px;height:36px;border:3px solid #27272a;border-top-color:#1E90FF;
264
+ border-radius:50%;animation:spin .8s linear infinite;
265
+ }
266
+ @keyframes spin{to{transform:rotate(360deg)}}
267
+ .modern-loader .loader-text{font-size:13px;color:#a1a1aa;font-weight:500}
268
+ .loader-bar-track{width:200px;height:4px;background:#27272a;border-radius:2px;overflow:hidden}
269
+ .loader-bar-fill{
270
+ height:100%;background:linear-gradient(90deg,#1E90FF,#47A3FF,#1E90FF);
271
+ background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px;
272
+ }
273
+ @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
274
+
275
+ .settings-group{border:1px solid #27272a;border-radius:10px;margin:12px 16px;padding:0;overflow:hidden}
276
+ .settings-group-title{
277
+ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;
278
+ padding:10px 16px;border-bottom:1px solid #27272a;background:rgba(24,24,27,.5);
279
+ }
280
+ .settings-group-body{padding:14px 16px;display:flex;flex-direction:column;gap:12px}
281
+ .slider-row{display:flex;align-items:center;gap:10px;min-height:28px}
282
+ .slider-row label{font-size:13px;font-weight:500;color:#a1a1aa;min-width:72px;flex-shrink:0}
283
+ .slider-row input[type="range"]{
284
+ flex:1;-webkit-appearance:none;appearance:none;height:6px;background:#27272a;
285
+ border-radius:3px;outline:none;min-width:0;
286
+ }
287
+ .slider-row input[type="range"]::-webkit-slider-thumb{
288
+ -webkit-appearance:none;width:16px;height:16px;background:linear-gradient(135deg,#1E90FF,#1873CC);
289
+ border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(30,144,255,.4);transition:transform .15s;
290
+ }
291
+ .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)}
292
+ .slider-row input[type="range"]::-moz-range-thumb{
293
+ width:16px;height:16px;background:linear-gradient(135deg,#1E90FF,#1873CC);
294
+ border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(30,144,255,.4);
295
+ }
296
+ .slider-row .slider-val{
297
+ min-width:52px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:12px;
298
+ font-weight:500;padding:3px 8px;background:#09090b;border:1px solid #27272a;
299
+ border-radius:6px;color:#a1a1aa;flex-shrink:0;
300
+ }
301
+ .checkbox-row{display:flex;align-items:center;gap:8px;font-size:13px;color:#a1a1aa}
302
+ .checkbox-row input[type="checkbox"]{accent-color:#1E90FF;width:16px;height:16px;cursor:pointer}
303
+ .checkbox-row label{color:#a1a1aa;font-size:13px;cursor:pointer}
304
+
305
+ .app-statusbar{
306
+ background:#18181b;border-top:1px solid #27272a;padding:6px 20px;
307
+ display:flex;gap:12px;height:34px;align-items:center;font-size:12px;
308
+ }
309
+ .app-statusbar .sb-section{
310
+ padding:0 12px;flex:1;display:flex;align-items:center;font-family:'JetBrains Mono',monospace;
311
+ font-size:12px;color:#52525b;overflow:hidden;white-space:nowrap;
312
+ }
313
+ .app-statusbar .sb-section.sb-fixed{
314
+ flex:0 0 auto;min-width:90px;text-align:center;justify-content:center;
315
+ padding:3px 12px;background:rgba(30,144,255,.08);border-radius:6px;color:#47A3FF;font-weight:500;
316
+ }
317
+
318
+ .app-notice{padding:10px 24px;font-size:11px;color:#d4d4d8!important;border-bottom:1px solid #27272a;background:rgba(255,255,255,.015)}
319
+ .app-notice a{color:#47A3FF;text-decoration:none}
320
+ .app-notice a:hover{text-decoration:underline}
321
+ .notice-list{list-style:none;display:flex;flex-direction:column;gap:3px;margin:0;padding:0}
322
+ .notice-list li{padding-left:14px;position:relative;line-height:1.6;color:#d4d4d8!important}
323
+ .notice-list li::before{content:"·";position:absolute;left:0;color:#d4d4d8!important;font-weight:700}
324
+
325
+ .dark .app-shell{background:#18181b}
326
+ .dark .upload-prompt-modern{background:transparent}
327
+ .dark .panel-card{background:#18181b}
328
+ .dark .settings-group{background:#18181b}
329
+ .dark .output-frame .out-title{color:#ffffff!important}
330
+ .dark .output-frame .out-title span{color:#ffffff!important}
331
+ .dark .out-download-btn{color:#7CB8FF!important}
332
+ .dark .out-download-btn:hover{color:#ffffff!important}
333
+
334
+ ::-webkit-scrollbar{width:8px;height:8px}
335
+ ::-webkit-scrollbar-track{background:#09090b}
336
+ ::-webkit-scrollbar-thumb{background:#27272a;border-radius:4px}
337
+ ::-webkit-scrollbar-thumb:hover{background:#3f3f46}
338
+
339
+ @media(max-width:840px){
340
+ .app-main-row{flex-direction:column}
341
+ .app-main-right{width:100%}
342
+ .app-main-left{border-right:none;border-bottom:1px solid #27272a}
343
+ }
static/gallery.js ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ () => {
2
+ function init() {
3
+ if (window.__fireRedInitDone) return;
4
+
5
+ const galleryGrid = document.getElementById('image-gallery-grid');
6
+ const dropZone = document.getElementById('gallery-drop-zone');
7
+ const uploadPrompt = document.getElementById('upload-prompt');
8
+ const uploadClick = document.getElementById('upload-click-area');
9
+ const fileInput = document.getElementById('custom-file-input');
10
+ const btnUpload = document.getElementById('tb-upload');
11
+ const btnRemove = document.getElementById('tb-remove');
12
+ const btnClear = document.getElementById('tb-clear');
13
+ const promptInput = document.getElementById('custom-prompt-input');
14
+ const runBtnEl = document.getElementById('custom-run-btn');
15
+ const imgCountTb = document.getElementById('tb-image-count');
16
+ const imgCountSb = document.getElementById('sb-image-count');
17
+
18
+ if (!galleryGrid || !fileInput || !dropZone) {
19
+ setTimeout(init, 250);
20
+ return;
21
+ }
22
+
23
+ window.__fireRedInitDone = true;
24
+
25
+ let images = [];
26
+ window.__uploadedImages = images;
27
+ let selectedIdx = -1;
28
+ let toastTimer = null;
29
+
30
+ function showToast(message, type) {
31
+ let toast = document.getElementById('app-toast');
32
+ if (!toast) {
33
+ toast = document.createElement('div');
34
+ toast.id = 'app-toast';
35
+ toast.className = 'toast-notification';
36
+ toast.innerHTML = '<span class="toast-icon"></span><span class="toast-text"></span>';
37
+ document.body.appendChild(toast);
38
+ }
39
+ const icon = toast.querySelector('.toast-icon');
40
+ const text = toast.querySelector('.toast-text');
41
+ toast.className = 'toast-notification ' + (type || 'error');
42
+ if (type === 'warning') icon.textContent = '⚠';
43
+ else if (type === 'info') icon.textContent = 'ℹ';
44
+ else icon.textContent = '✗';
45
+ text.textContent = message;
46
+ if (toastTimer) clearTimeout(toastTimer);
47
+ void toast.offsetWidth;
48
+ toast.classList.add('visible');
49
+ toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500);
50
+ }
51
+ window.__showToast = showToast;
52
+
53
+ function flashPromptError() {
54
+ if (!promptInput) return;
55
+ promptInput.classList.add('error-flash');
56
+ promptInput.focus();
57
+ setTimeout(() => promptInput.classList.remove('error-flash'), 800);
58
+ }
59
+
60
+ function setGradioValue(containerId, value) {
61
+ const container = document.getElementById(containerId);
62
+ if (!container) return;
63
+ container.querySelectorAll('input, textarea').forEach(el => {
64
+ if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return;
65
+ const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
66
+ const ns = Object.getOwnPropertyDescriptor(proto, 'value');
67
+ if (ns && ns.set) {
68
+ ns.set.call(el, value);
69
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
70
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
71
+ }
72
+ });
73
+ }
74
+ window.__setGradioValue = setGradioValue;
75
+
76
+ function syncImagesToGradio() {
77
+ window.__uploadedImages = images;
78
+ const b64Array = images.map(img => img.b64);
79
+ setGradioValue('hidden-images-b64', JSON.stringify(b64Array));
80
+ updateCounts();
81
+ }
82
+
83
+ function syncPromptToGradio() {
84
+ if (promptInput) setGradioValue('prompt-gradio-input', promptInput.value);
85
+ }
86
+
87
+ function updateCounts() {
88
+ const n = images.length;
89
+ const txt = n > 0 ? n + ' image' + (n > 1 ? 's' : '') : 'No images';
90
+ if (imgCountTb) imgCountTb.textContent = txt;
91
+ if (imgCountSb) imgCountSb.textContent = n > 0 ? txt + ' uploaded' : 'No images uploaded';
92
+ }
93
+
94
+ function addImage(b64, name) {
95
+ images.push({id: Date.now() + Math.random(), b64: b64, name: name});
96
+ renderGallery();
97
+ syncImagesToGradio();
98
+ }
99
+ window.__addImage = addImage;
100
+
101
+ function removeImage(idx) {
102
+ images.splice(idx, 1);
103
+ if (selectedIdx === idx) selectedIdx = -1;
104
+ else if (selectedIdx > idx) selectedIdx--;
105
+ renderGallery();
106
+ syncImagesToGradio();
107
+ }
108
+
109
+ function clearAll() {
110
+ images = [];
111
+ window.__uploadedImages = images;
112
+ selectedIdx = -1;
113
+ renderGallery();
114
+ syncImagesToGradio();
115
+ }
116
+ window.__clearAll = clearAll;
117
+
118
+ function selectImage(idx) {
119
+ selectedIdx = (selectedIdx === idx) ? -1 : idx;
120
+ renderGallery();
121
+ }
122
+
123
+ function renderGallery() {
124
+ if (images.length === 0) {
125
+ galleryGrid.innerHTML = '';
126
+ galleryGrid.style.display = 'none';
127
+ if (uploadPrompt) uploadPrompt.style.display = '';
128
+ return;
129
+ }
130
+ if (uploadPrompt) uploadPrompt.style.display = 'none';
131
+ galleryGrid.style.display = 'grid';
132
+
133
+ let html = '';
134
+ images.forEach((img, i) => {
135
+ const sel = i === selectedIdx ? ' selected' : '';
136
+ html += '<div class="gallery-thumb' + sel + '" data-idx="' + i + '">'
137
+ + '<img src="' + img.b64 + '" alt="' + (img.name||'image') + '">'
138
+ + '<span class="thumb-badge">#' + (i+1) + '</span>'
139
+ + '<button class="thumb-remove" data-remove="' + i + '">✕</button>'
140
+ + '</div>';
141
+ });
142
+ html += '<div class="gallery-add-card" id="gallery-add-card">'
143
+ + '<span class="add-icon">+</span>'
144
+ + '<span class="add-text">Add</span>'
145
+ + '</div>';
146
+ galleryGrid.innerHTML = html;
147
+
148
+ galleryGrid.querySelectorAll('.gallery-thumb').forEach(thumb => {
149
+ thumb.addEventListener('click', (e) => {
150
+ if (e.target.closest('.thumb-remove')) return;
151
+ selectImage(parseInt(thumb.dataset.idx));
152
+ });
153
+ });
154
+ galleryGrid.querySelectorAll('.thumb-remove').forEach(btn => {
155
+ btn.addEventListener('click', (e) => {
156
+ e.stopPropagation();
157
+ removeImage(parseInt(btn.dataset.remove));
158
+ });
159
+ });
160
+ const addCard = document.getElementById('gallery-add-card');
161
+ if (addCard) addCard.addEventListener('click', () => fileInput.click());
162
+ }
163
+
164
+ function processFiles(files) {
165
+ Array.from(files).forEach(file => {
166
+ if (!file.type.startsWith('image/')) return;
167
+ const reader = new FileReader();
168
+ reader.onload = (e) => addImage(e.target.result, file.name);
169
+ reader.readAsDataURL(file);
170
+ });
171
+ }
172
+
173
+ fileInput.addEventListener('change', (e) => { processFiles(e.target.files); e.target.value = ''; });
174
+ if (uploadClick) uploadClick.addEventListener('click', () => fileInput.click());
175
+ if (btnUpload) btnUpload.addEventListener('click', () => fileInput.click());
176
+ if (btnRemove) btnRemove.addEventListener('click', () => {
177
+ if (selectedIdx >= 0 && selectedIdx < images.length) removeImage(selectedIdx);
178
+ });
179
+ if (btnClear) btnClear.addEventListener('click', clearAll);
180
+
181
+ dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
182
+ dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); });
183
+ dropZone.addEventListener('drop', (e) => {
184
+ e.preventDefault(); dropZone.classList.remove('drag-over');
185
+ if (e.dataTransfer.files.length) processFiles(e.dataTransfer.files);
186
+ });
187
+
188
+ if (promptInput) promptInput.addEventListener('input', syncPromptToGradio);
189
+
190
+ window.__setPrompt = function(text) {
191
+ if (promptInput) { promptInput.value = text; syncPromptToGradio(); }
192
+ };
193
+
194
+ document.querySelectorAll('.example-card[data-idx]').forEach(card => {
195
+ card.addEventListener('click', () => {
196
+ const idx = card.getAttribute('data-idx');
197
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
198
+ card.classList.add('loading');
199
+ showToast('Loading example...', 'info');
200
+
201
+ setGradioValue('example-result-data', '');
202
+ setGradioValue('example-idx-input', idx);
203
+
204
+ setTimeout(() => {
205
+ const btn = document.getElementById('example-load-btn');
206
+ if (btn) {
207
+ const b = btn.querySelector('button');
208
+ if (b) b.click(); else btn.click();
209
+ }
210
+ }, 150);
211
+
212
+ setTimeout(() => card.classList.remove('loading'), 12000);
213
+ });
214
+ });
215
+
216
+ function syncSlider(customId, gradioId) {
217
+ const slider = document.getElementById(customId);
218
+ const valSpan = document.getElementById(customId + '-val');
219
+ if (!slider) return;
220
+ slider.addEventListener('input', () => {
221
+ if (valSpan) valSpan.textContent = slider.value;
222
+ const container = document.getElementById(gradioId);
223
+ if (!container) return;
224
+ container.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => {
225
+ const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
226
+ if (ns && ns.set) {
227
+ ns.set.call(el, slider.value);
228
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
229
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
230
+ }
231
+ });
232
+ });
233
+ }
234
+ syncSlider('custom-seed', 'gradio-seed');
235
+ syncSlider('custom-guidance', 'gradio-guidance');
236
+ syncSlider('custom-steps', 'gradio-steps');
237
+
238
+ const randCheck = document.getElementById('custom-randomize');
239
+ if (randCheck) {
240
+ randCheck.addEventListener('change', () => {
241
+ const container = document.getElementById('gradio-randomize');
242
+ if (!container) return;
243
+ const cb = container.querySelector('input[type="checkbox"]');
244
+ if (cb && cb.checked !== randCheck.checked) cb.click();
245
+ });
246
+ }
247
+
248
+ function showLoader() {
249
+ const l = document.getElementById('output-loader');
250
+ if (l) l.classList.add('active');
251
+ const sb = document.querySelector('.sb-fixed');
252
+ if (sb) sb.textContent = 'Processing...';
253
+ }
254
+ function hideLoader() {
255
+ const l = document.getElementById('output-loader');
256
+ if (l) l.classList.remove('active');
257
+ const sb = document.querySelector('.sb-fixed');
258
+ if (sb) sb.textContent = 'Done';
259
+ }
260
+ window.__showLoader = showLoader;
261
+ window.__hideLoader = hideLoader;
262
+
263
+ function validateBeforeRun() {
264
+ const promptVal = promptInput ? promptInput.value.trim() : '';
265
+ const hasImages = images.length > 0;
266
+ if (!hasImages && !promptVal) { showToast('Please upload an image and enter a prompt', 'error'); flashPromptError(); return false; }
267
+ if (!hasImages) { showToast('Please upload at least one image', 'error'); return false; }
268
+ if (!promptVal) { showToast('Please enter an edit prompt', 'warning'); flashPromptError(); return false; }
269
+ return true;
270
+ }
271
+
272
+ window.__clickGradioRunBtn = function() {
273
+ if (!validateBeforeRun()) return;
274
+ syncPromptToGradio(); syncImagesToGradio(); showLoader();
275
+ setTimeout(() => {
276
+ const gradioBtn = document.getElementById('gradio-run-btn');
277
+ if (!gradioBtn) return;
278
+ const btn = gradioBtn.querySelector('button');
279
+ if (btn) btn.click(); else gradioBtn.click();
280
+ }, 200);
281
+ };
282
+
283
+ if (runBtnEl) runBtnEl.addEventListener('click', () => window.__clickGradioRunBtn());
284
+
285
+ renderGallery();
286
+ updateCounts();
287
+ }
288
+ init();
289
+ }
static/run_preprocess.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ (imgs, p, s, rs, gs, st) => {
2
+ const images = window.__uploadedImages || [];
3
+ const b64Array = images.map(img => img.b64);
4
+ const imgsJson = JSON.stringify(b64Array);
5
+ const promptEl = document.getElementById('custom-prompt-input');
6
+ const promptVal = promptEl ? promptEl.value : p;
7
+ return [imgsJson, promptVal, s, rs, gs, st];
8
+ }
static/wire_outputs.js ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ () => {
2
+ function watchOutputs() {
3
+ const resultContainer = document.getElementById('gradio-result');
4
+ const outBody = document.getElementById('output-image-container');
5
+ const outPh = document.getElementById('output-placeholder');
6
+ const dlBtn = document.getElementById('dl-btn-output');
7
+
8
+ if (!resultContainer || !outBody) { setTimeout(watchOutputs, 500); return; }
9
+
10
+ if (dlBtn) {
11
+ dlBtn.addEventListener('click', (e) => {
12
+ e.stopPropagation();
13
+ const img = outBody.querySelector('img.modern-out-img');
14
+ if (img && img.src) {
15
+ const a = document.createElement('a');
16
+ a.href = img.src; a.download = 'firered_output.png';
17
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
18
+ }
19
+ });
20
+ }
21
+
22
+ function syncImage() {
23
+ const resultImg = resultContainer.querySelector('img');
24
+ if (resultImg && resultImg.src) {
25
+ if (outPh) outPh.style.display = 'none';
26
+ let existing = outBody.querySelector('img.modern-out-img');
27
+ if (!existing) { existing = document.createElement('img'); existing.className = 'modern-out-img'; outBody.appendChild(existing); }
28
+ if (existing.src !== resultImg.src) {
29
+ existing.src = resultImg.src;
30
+ if (dlBtn) dlBtn.classList.add('visible');
31
+ if (window.__hideLoader) window.__hideLoader();
32
+ }
33
+ }
34
+ }
35
+ const observer = new MutationObserver(syncImage);
36
+ observer.observe(resultContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']});
37
+ setInterval(syncImage, 800);
38
+ }
39
+ watchOutputs();
40
+
41
+ function watchSeed() {
42
+ const seedContainer = document.getElementById('gradio-seed');
43
+ const seedSlider = document.getElementById('custom-seed');
44
+ const seedVal = document.getElementById('custom-seed-val');
45
+ if (!seedContainer || !seedSlider) { setTimeout(watchSeed, 500); return; }
46
+ function sync() {
47
+ const el = seedContainer.querySelector('input[type="range"],input[type="number"]');
48
+ if (el && el.value) { seedSlider.value = el.value; if (seedVal) seedVal.textContent = el.value; }
49
+ }
50
+ const obs = new MutationObserver(sync);
51
+ obs.observe(seedContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']});
52
+ setInterval(sync, 1000);
53
+ }
54
+ watchSeed();
55
+
56
+ function watchExampleResults() {
57
+ const container = document.getElementById('example-result-data');
58
+ if (!container) { setTimeout(watchExampleResults, 500); return; }
59
+
60
+ let lastProcessed = '';
61
+
62
+ function checkResult() {
63
+ const el = container.querySelector('textarea') || container.querySelector('input');
64
+ if (!el) return;
65
+ const val = el.value;
66
+ if (!val || val === lastProcessed || val.length < 20) return;
67
+
68
+ try {
69
+ const data = JSON.parse(val);
70
+ if (data.status === 'ok' && data.images && data.images.length > 0) {
71
+ lastProcessed = val;
72
+
73
+ if (window.__clearAll) window.__clearAll();
74
+ if (window.__setPrompt && data.prompt) window.__setPrompt(data.prompt);
75
+
76
+ data.images.forEach((b64, i) => {
77
+ if (b64 && window.__addImage) {
78
+ const name = (data.names && data.names[i]) ? data.names[i] : ('example_' + (i+1) + '.jpg');
79
+ window.__addImage(b64, name);
80
+ }
81
+ });
82
+
83
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
84
+ if (window.__showToast) window.__showToast('Example loaded — ' + data.images.length + ' image(s)', 'info');
85
+ } else if (data.status === 'error') {
86
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
87
+ if (window.__showToast) window.__showToast('Could not load example images', 'error');
88
+ }
89
+ } catch(e) {
90
+ console.error('Example parse error:', e);
91
+ }
92
+ }
93
+
94
+ const obs = new MutationObserver(checkResult);
95
+ obs.observe(container, {childList:true, subtree:true, characterData:true, attributes:true});
96
+ setInterval(checkResult, 500);
97
+ }
98
+ watchExampleResults();
99
+ }
templates/app.html ADDED
@@ -0,0 +1,154 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="app-shell">
2
+
3
+ <div class="app-header">
4
+ <div class="app-header-left">
5
+ <div class="app-logo">{fire_logo_svg}</div>
6
+ <span class="app-title">FireRed-Image-Edit</span>
7
+ <span class="app-badge">v1.1</span>
8
+ <span class="app-badge fast">4-Step Fast</span>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="app-notice">
13
+ <ul class="notice-list">
14
+ <li>Aims to produce high-resolution 2K output images while keeping the generating speed, based on <a href="https://github.com/PRITHIVSAKTHIUR/FireRed-Image-Edit-1.0-Fast" target="_blank">FireRed-Image-Edit-1.0-Fast</a>.</li>
15
+ <li>Service availability and response times may vary as this space is under development and runs on shared GPU infrastructure.</li>
16
+ <li>When using this Space, please comply with the <a href="https://huggingface.co/content-policy" target="_blank">Hugging Face Content Policy</a>.</li>
17
+ <li>To monitor application performance and improve quality, input data (images/text) and generated outputs are temporarily logged and saved to external storage. Please do not input any personal, sensitive, or confidential information.</li>
18
+ </ul>
19
+ </div>
20
+
21
+ <div class="app-toolbar">
22
+ <button id="tb-upload" class="modern-tb-btn" title="Upload images">
23
+ {upload_svg}<span class="tb-label">Upload</span>
24
+ </button>
25
+ <button id="tb-remove" class="modern-tb-btn" title="Remove selected image">
26
+ {remove_svg}<span class="tb-label">Remove</span>
27
+ </button>
28
+ <button id="tb-clear" class="modern-tb-btn" title="Clear all images">
29
+ {clear_svg}<span class="tb-label">Clear All</span>
30
+ </button>
31
+ <div class="tb-sep"></div>
32
+ <span id="tb-image-count" class="tb-info">No images</span>
33
+ </div>
34
+
35
+ <div class="app-main-row">
36
+ <div class="app-main-left">
37
+ <div id="gallery-drop-zone">
38
+ <div id="upload-prompt" class="upload-prompt-modern">
39
+ <div id="upload-click-area" class="upload-click-area">
40
+ <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
41
+ <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#1E90FF" stroke-width="2" stroke-dasharray="4 3"/>
42
+ <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(30,144,255,0.15)" stroke="#1E90FF" stroke-width="1.5"/>
43
+ <circle cx="28" cy="30" r="6" fill="rgba(30,144,255,0.2)" stroke="#1E90FF" stroke-width="1.5"/>
44
+ </svg>
45
+ <span class="upload-main-text">Click or drag images here</span>
46
+ <span class="upload-sub-text">Supports multiple images for reference-based editing and guided manipulation</span>
47
+ </div>
48
+ </div>
49
+ <input id="custom-file-input" type="file" accept="image/*" multiple style="display:none;" />
50
+ <div id="image-gallery-grid" class="image-gallery-grid" style="display:none;"></div>
51
+ </div>
52
+
53
+ <div class="hint-bar">
54
+ <b>Upload:</b> Click or drag to add images &nbsp;&middot;&nbsp;
55
+ <b>Multi-image:</b> Upload multiple images for reference-based editing &nbsp;&middot;&nbsp;
56
+ <kbd>Remove</kbd> deletes selected &nbsp;&middot;&nbsp;
57
+ <kbd>Clear All</kbd> removes everything
58
+ </div>
59
+
60
+ <div class="suggestions-section">
61
+ <div class="suggestions-title">Quick Prompts</div>
62
+ <div class="suggestions-wrap">
63
+ <button class="suggestion-chip" onclick="window.__setPrompt('Transform the image into a dotted cartoon style.')">Cartoon Style</button>
64
+ <button class="suggestion-chip" onclick="window.__setPrompt('Convert it to black and white.')">Black and White</button>
65
+ <button class="suggestion-chip" onclick="window.__setPrompt('Add cinematic lighting with warm orange tones and film grain.')">Cinematic</button>
66
+ <button class="suggestion-chip" onclick="window.__setPrompt('Transform into anime style illustration.')">Anime Style</button>
67
+ <button class="suggestion-chip" onclick="window.__setPrompt('Apply oil painting effect with visible brush strokes.')">Oil Painting</button>
68
+ <button class="suggestion-chip" onclick="window.__setPrompt('Enhance and upscale with more detail and clarity.')">Enhance</button>
69
+ <button class="suggestion-chip" onclick="window.__setPrompt('Make it look like a watercolor painting with soft edges.')">Watercolor</button>
70
+ <button class="suggestion-chip" onclick="window.__setPrompt('Add dramatic sunset sky and warm lighting.')">Sunset Glow</button>
71
+ <button class="suggestion-chip" onclick="window.__setPrompt('Convert to detailed pencil sketch with cross-hatching and shading.')">Pencil Sketch</button>
72
+ <button class="suggestion-chip" onclick="window.__setPrompt('Apply pop art style with bold colors and halftone patterns.')">Pop Art</button>
73
+ <button class="suggestion-chip" onclick="window.__setPrompt('Apply a vintage retro film look with faded colors and light leaks.')">Vintage Retro</button>
74
+ <button class="suggestion-chip" onclick="window.__setPrompt('Add neon glow effects with vibrant colors against a dark background.')">Neon Glow</button>
75
+ <button class="suggestion-chip" onclick="window.__setPrompt('Convert to pixel art style with a retro 16-bit aesthetic.')">Pixel Art</button>
76
+ <button class="suggestion-chip" onclick="window.__setPrompt('Simplify into a clean minimalist illustration with flat colors.')">Minimalist</button>
77
+ <button class="suggestion-chip" onclick="window.__setPrompt('Convert to low poly 3D geometric art style.')">Low Poly 3D</button>
78
+ <button class="suggestion-chip" onclick="window.__setPrompt('Transform into comic book style with bold outlines and cel shading.')">Comic Book</button>
79
+ </div>
80
+ </div>
81
+
82
+ <div class="examples-section">
83
+ <div class="examples-title">Quick Examples</div>
84
+ <div class="examples-scroll">
85
+ {example_cards_html}
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <div class="app-main-right">
91
+ <div class="panel-card">
92
+ <div class="panel-card-title">Edit Instruction</div>
93
+ <div class="panel-card-body">
94
+ <label class="modern-label" for="custom-prompt-input">Prompt</label>
95
+ <textarea id="custom-prompt-input" class="modern-textarea" rows="3" placeholder="e.g., transform into anime, upscale, change lighting..."></textarea>
96
+ </div>
97
+ </div>
98
+
99
+ <div style="padding:12px 20px;">
100
+ <button id="custom-run-btn" class="btn-run">
101
+ <svg viewBox="0 0 24 24" 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>
102
+ <span id="run-btn-label">Edit Image</span>
103
+ </button>
104
+ </div>
105
+
106
+ <div class="output-frame" style="flex:1">
107
+ <div class="out-title">
108
+ <span>Output</span>
109
+ <span id="dl-btn-output" class="out-download-btn" title="Download">
110
+ {download_svg} Save
111
+ </span>
112
+ </div>
113
+ <div class="out-body" id="output-image-container">
114
+ <div class="modern-loader" id="output-loader">
115
+ <div class="loader-spinner"></div>
116
+ <div class="loader-text">Processing image...</div>
117
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
118
+ </div>
119
+ <div class="out-placeholder" id="output-placeholder">Result will appear here</div>
120
+ </div>
121
+ </div>
122
+
123
+ <div class="settings-group">
124
+ <div class="settings-group-title">Advanced Settings</div>
125
+ <div class="settings-group-body">
126
+ <div class="slider-row">
127
+ <label>Seed</label>
128
+ <input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0">
129
+ <span class="slider-val" id="custom-seed-val">0</span>
130
+ </div>
131
+ <div class="checkbox-row">
132
+ <input type="checkbox" id="custom-randomize" checked>
133
+ <label for="custom-randomize">Randomize seed</label>
134
+ </div>
135
+ <div class="slider-row">
136
+ <label>Guidance</label>
137
+ <input type="range" id="custom-guidance" min="1" max="10" step="0.1" value="1.0">
138
+ <span class="slider-val" id="custom-guidance-val">1.0</span>
139
+ </div>
140
+ <div class="slider-row">
141
+ <label>Steps</label>
142
+ <input type="range" id="custom-steps" min="1" max="50" step="1" value="4">
143
+ <span class="slider-val" id="custom-steps-val">4</span>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <div class="app-statusbar">
151
+ <div class="sb-section" id="sb-image-count">No images uploaded</div>
152
+ <div class="sb-section sb-fixed">Ready</div>
153
+ </div>
154
+ </div>