prithivMLmods commited on
Commit
f78f9c4
Β·
verified Β·
1 Parent(s): 40c856a

update app - [cleaned]

Browse files
Files changed (1) hide show
  1. app.py +940 -187
app.py CHANGED
@@ -5,78 +5,12 @@ import numpy as np
5
  import spaces
6
  import torch
7
  import random
 
 
 
8
  from PIL import Image
9
- from typing import Iterable
10
- from gradio.themes import Soft
11
- from gradio.themes.utils import colors, fonts, sizes
12
-
13
- colors.orange_red = colors.Color(
14
- name="orange_red",
15
- c50="#FFF0E5",
16
- c100="#FFE0CC",
17
- c200="#FFC299",
18
- c300="#FFA366",
19
- c400="#FF8533",
20
- c500="#FF4500",
21
- c600="#E63E00",
22
- c700="#CC3700",
23
- c800="#B33000",
24
- c900="#992900",
25
- c950="#802200",
26
- )
27
-
28
- class OrangeRedTheme(Soft):
29
- def __init__(
30
- self,
31
- *,
32
- primary_hue: colors.Color | str = colors.gray,
33
- secondary_hue: colors.Color | str = colors.orange_red,
34
- neutral_hue: colors.Color | str = colors.slate,
35
- text_size: sizes.Size | str = sizes.text_lg,
36
- font: fonts.Font | str | Iterable[fonts.Font | str] = (
37
- fonts.GoogleFont("Outfit"), "Arial", "sans-serif",
38
- ),
39
- font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
40
- fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace",
41
- ),
42
- ):
43
- super().__init__(
44
- primary_hue=primary_hue,
45
- secondary_hue=secondary_hue,
46
- neutral_hue=neutral_hue,
47
- text_size=text_size,
48
- font=font,
49
- font_mono=font_mono,
50
- )
51
- super().set(
52
- background_fill_primary="*primary_50",
53
- background_fill_primary_dark="*primary_900",
54
- body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
55
- body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
56
- button_primary_text_color="white",
57
- button_primary_text_color_hover="white",
58
- button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
59
- button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
60
- button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)",
61
- button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)",
62
- button_secondary_text_color="black",
63
- button_secondary_text_color_hover="white",
64
- button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)",
65
- button_secondary_background_fill_hover="linear-gradient(90deg, *primary_400, *primary_400)",
66
- button_secondary_background_fill_dark="linear-gradient(90deg, *primary_500, *primary_600)",
67
- button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_500, *primary_500)",
68
- slider_color="*secondary_500",
69
- slider_color_dark="*secondary_600",
70
- block_title_text_weight="600",
71
- block_border_width="3px",
72
- block_shadow="*shadow_drop_lg",
73
- button_primary_shadow="*shadow_drop_lg",
74
- button_large_padding="11px",
75
- color_accent_soft="*primary_100",
76
- block_label_background_fill="*primary_200",
77
- )
78
-
79
- orange_red_theme = OrangeRedTheme()
80
 
81
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
82
 
@@ -92,13 +26,13 @@ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
92
  dtype = torch.bfloat16
93
 
94
  pipe = QwenImageEditPlusPipeline.from_pretrained(
95
- "FireRedTeam/FireRed-Image-Edit-1.1", # ---> Prev: FireRedTeam/FireRed-Image-Edit-1.0
96
  transformer=QwenImageTransformer2DModel.from_pretrained(
97
  "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19",
98
  torch_dtype=dtype,
99
- device_map='cuda'
100
  ),
101
- torch_dtype=dtype
102
  ).to(device)
103
 
104
  try:
@@ -107,14 +41,34 @@ try:
107
  except Exception as e:
108
  print(f"Warning: Could not set FA3 processor: {e}")
109
 
110
- MAX_SEED = np.iinfo(np.int32).max
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  def update_dimensions_on_upload(image):
113
  if image is None:
114
  return 1024, 1024
115
-
116
  original_width, original_height = image.size
117
-
118
  if original_width > original_height:
119
  new_width = 1024
120
  aspect_ratio = original_height / original_width
@@ -123,56 +77,39 @@ def update_dimensions_on_upload(image):
123
  new_height = 1024
124
  aspect_ratio = original_width / original_height
125
  new_width = int(new_height * aspect_ratio)
126
-
127
  new_width = (new_width // 8) * 8
128
  new_height = (new_height // 8) * 8
129
-
130
  return new_width, new_height
131
 
 
132
  @spaces.GPU
133
  def infer(
134
- images,
135
  prompt,
136
  seed,
137
  randomize_seed,
138
  guidance_scale,
139
  steps,
140
- progress=gr.Progress(track_tqdm=True)
141
  ):
142
  gc.collect()
143
  torch.cuda.empty_cache()
144
 
145
- if not images:
146
- raise gr.Error("Please upload at least one image to edit.")
147
-
148
- pil_images = []
149
- if images is not None:
150
- for item in images:
151
- try:
152
- if isinstance(item, tuple) or isinstance(item, list):
153
- path_or_img = item[0]
154
- else:
155
- path_or_img = item
156
-
157
- if isinstance(path_or_img, str):
158
- pil_images.append(Image.open(path_or_img).convert("RGB"))
159
- elif isinstance(path_or_img, Image.Image):
160
- pil_images.append(path_or_img.convert("RGB"))
161
- else:
162
- pil_images.append(Image.open(path_or_img.name).convert("RGB"))
163
- except Exception as e:
164
- print(f"Skipping invalid image item: {e}")
165
- continue
166
-
167
  if not pil_images:
168
- raise gr.Error("Could not process uploaded images.")
 
 
169
 
170
  if randomize_seed:
171
  seed = random.randint(0, MAX_SEED)
172
 
173
  generator = torch.Generator(device=device).manual_seed(seed)
174
- 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"
175
-
 
 
 
176
  width, height = update_dimensions_on_upload(pil_images[0])
177
 
178
  try:
@@ -186,101 +123,917 @@ def infer(
186
  generator=generator,
187
  true_cfg_scale=guidance_scale,
188
  ).images[0]
189
-
190
  return result_image, seed
191
-
192
  except Exception as e:
193
  raise e
194
  finally:
195
  gc.collect()
196
  torch.cuda.empty_cache()
197
 
198
- @spaces.GPU
199
- def infer_example(images, prompt):
200
- if not images:
201
- return None, 0
202
 
203
- if isinstance(images, str):
204
- images_list = [images]
205
- else:
206
- images_list = images
207
-
208
- result, seed = infer(
209
- images=images_list,
210
- prompt=prompt,
211
- seed=0,
212
- randomize_seed=True,
213
- guidance_scale=1.0,
214
- steps=4
215
- )
216
- return result, seed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
- css = """
219
- #col-container {
220
- margin: 0 auto;
221
- max-width: 1000px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  }
223
- #main-title h1 {font-size: 2.4em !important;}
224
  """
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  with gr.Blocks() as demo:
227
- with gr.Column(elem_id="col-container"):
228
- gr.Markdown("# **FireRed-Image-Edit-1.0-Fast - [v@1.1](https://huggingface.co/FireRedTeam/FireRed-Image-Edit-1.1)**", elem_id="main-title")
229
- gr.Markdown("Perform image edits using [FireRed-Image-Edit-1.0](https://huggingface.co/FireRedTeam/FireRed-Image-Edit-1.0) with 4-step fast inference. Open on [GitHub](https://github.com/PRITHIVSAKTHIUR/FireRed-Image-Edit-1.0-Fast)")
230
-
231
- with gr.Row(equal_height=True):
232
- with gr.Column():
233
- images = gr.Gallery(
234
- label="Upload Images",
235
- #sources=["upload", "clipboard"],
236
- type="filepath",
237
- columns=2,
238
- rows=1,
239
- height=300,
240
- allow_preview=True
241
- )
242
-
243
- prompt = gr.Text(
244
- label="Edit Prompt",
245
- show_label=True,
246
- max_lines=2,
247
- placeholder="e.g., transform into anime, upscale, change lighting...",
248
- )
249
-
250
- run_button = gr.Button("Edit Image", variant="primary")
251
-
252
- with gr.Column():
253
- output_image = gr.Image(label="Output Image", interactive=False, format="png", height=395)
254
-
255
- with gr.Accordion("Advanced Settings", open=False, visible=False):
256
- seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
257
- randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
258
- guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
259
- steps = gr.Slider(label="Inference Steps", minimum=1, maximum=50, step=1, value=4)
260
-
261
- gr.Examples(
262
- examples=[
263
- [["examples/1.jpg"], "cinematic polaroid with soft grain subtle vignette gentle lighting white frame handwritten photographed 'Fire-Edit' preserving realistic texture and details."],
264
- [["examples/2.jpg"], "Transform the image into a dotted cartoon style."],
265
- [["examples/3.jpeg"], "Convert it to black and white."],
266
- [["examples/4.jpg", "examples/5.jpg"], "Replace her glasses with the new glasses from image 1."],
267
- [["examples/8.jpg", "examples/9.png"], "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."],
268
- [["examples/10.jpg", "examples/11.png"], "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."],
269
- ],
270
- inputs=[images, prompt],
271
- outputs=[output_image, seed],
272
- fn=infer_example,
273
- cache_examples=False,
274
- label="Examples"
275
- )
276
-
277
- gr.Markdown("[*](https://huggingface.co/FireRedTeam/FireRed-Image-Edit-1.0)This is still an experimental Space for FireRed-Image-Edit-1.0.")
278
-
279
- run_button.click(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  fn=infer,
281
- inputs=[images, prompt, seed, randomize_seed, guidance_scale, steps],
282
- outputs=[output_image, seed]
 
 
 
 
 
 
 
 
283
  )
284
 
285
  if __name__ == "__main__":
286
- demo.queue(max_size=30).launch(css=css, theme=orange_red_theme, mcp_server=True, ssr_mode=False, show_error=True)
 
 
 
5
  import spaces
6
  import torch
7
  import random
8
+ import base64
9
+ import json
10
+ from io import BytesIO
11
  from PIL import Image
12
+
13
+ MAX_SEED = np.iinfo(np.int32).max
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
16
 
 
26
  dtype = torch.bfloat16
27
 
28
  pipe = QwenImageEditPlusPipeline.from_pretrained(
29
+ "FireRedTeam/FireRed-Image-Edit-1.1",
30
  transformer=QwenImageTransformer2DModel.from_pretrained(
31
  "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19",
32
  torch_dtype=dtype,
33
+ device_map="cuda",
34
  ),
35
+ torch_dtype=dtype,
36
  ).to(device)
37
 
38
  try:
 
41
  except Exception as e:
42
  print(f"Warning: Could not set FA3 processor: {e}")
43
 
44
+
45
+ def b64_to_pil_list(b64_json_str):
46
+ if not b64_json_str or b64_json_str.strip() in ("", "[]"):
47
+ return []
48
+ try:
49
+ b64_list = json.loads(b64_json_str)
50
+ except Exception:
51
+ return []
52
+ pil_images = []
53
+ for b64_str in b64_list:
54
+ if not b64_str or not isinstance(b64_str, str):
55
+ continue
56
+ try:
57
+ if b64_str.startswith("data:image"):
58
+ _, data = b64_str.split(",", 1)
59
+ else:
60
+ data = b64_str
61
+ image_data = base64.b64decode(data)
62
+ pil_images.append(Image.open(BytesIO(image_data)).convert("RGB"))
63
+ except Exception as e:
64
+ print(f"Error decoding image: {e}")
65
+ return pil_images
66
+
67
 
68
  def update_dimensions_on_upload(image):
69
  if image is None:
70
  return 1024, 1024
 
71
  original_width, original_height = image.size
 
72
  if original_width > original_height:
73
  new_width = 1024
74
  aspect_ratio = original_height / original_width
 
77
  new_height = 1024
78
  aspect_ratio = original_width / original_height
79
  new_width = int(new_height * aspect_ratio)
 
80
  new_width = (new_width // 8) * 8
81
  new_height = (new_height // 8) * 8
 
82
  return new_width, new_height
83
 
84
+
85
  @spaces.GPU
86
  def infer(
87
+ images_b64_json,
88
  prompt,
89
  seed,
90
  randomize_seed,
91
  guidance_scale,
92
  steps,
93
+ progress=gr.Progress(track_tqdm=True),
94
  ):
95
  gc.collect()
96
  torch.cuda.empty_cache()
97
 
98
+ pil_images = b64_to_pil_list(images_b64_json)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  if not pil_images:
100
+ raise gr.Error("Please upload at least one image to edit.")
101
+ if not prompt or prompt.strip() == "":
102
+ raise gr.Error("Please enter an edit prompt.")
103
 
104
  if randomize_seed:
105
  seed = random.randint(0, MAX_SEED)
106
 
107
  generator = torch.Generator(device=device).manual_seed(seed)
108
+ negative_prompt = (
109
+ "worst quality, low quality, bad anatomy, bad hands, text, error, "
110
+ "missing fingers, extra digit, fewer digits, cropped, jpeg artifacts, "
111
+ "signature, watermark, username, blurry"
112
+ )
113
  width, height = update_dimensions_on_upload(pil_images[0])
114
 
115
  try:
 
123
  generator=generator,
124
  true_cfg_scale=guidance_scale,
125
  ).images[0]
 
126
  return result_image, seed
 
127
  except Exception as e:
128
  raise e
129
  finally:
130
  gc.collect()
131
  torch.cuda.empty_cache()
132
 
 
 
 
 
133
 
134
+ css = r"""
135
+ @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');
136
+
137
+ *{box-sizing:border-box;margin:0;padding:0}
138
+
139
+ body,.gradio-container{
140
+ background:#0f0f13!important;
141
+ font-family:'Inter',system-ui,-apple-system,sans-serif!important;
142
+ font-size:14px!important;
143
+ color:#e4e4e7!important;
144
+ min-height:100vh;
145
+ }
146
+ .dark body,.dark .gradio-container{background:#0f0f13!important;color:#e4e4e7!important}
147
+ footer{display:none!important}
148
+
149
+ .hidden-input{
150
+ display:none!important;height:0!important;overflow:hidden!important;
151
+ margin:0!important;padding:0!important;
152
+ }
153
+
154
+ /* ── App Shell ── */
155
+ .app-shell{
156
+ background:#18181b;border:1px solid #27272a;border-radius:16px;
157
+ margin:12px auto;max-width:1400px;overflow:hidden;
158
+ box-shadow:0 25px 50px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.03);
159
+ }
160
+
161
+ /* ── Header ── */
162
+ .app-header{
163
+ background:linear-gradient(135deg,#18181b,#1e1e24);
164
+ border-bottom:1px solid #27272a;padding:14px 24px;
165
+ display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;
166
+ }
167
+ .app-header-left{display:flex;align-items:center;gap:12px}
168
+ .app-logo{
169
+ width:36px;height:36px;
170
+ background:linear-gradient(135deg,#FF4500,#FF6633,#FF8C66);
171
+ border-radius:10px;display:flex;align-items:center;justify-content:center;
172
+ font-size:18px;color:#fff;box-shadow:0 4px 12px rgba(255,69,0,.35);
173
+ }
174
+ .app-title{
175
+ font-size:18px;font-weight:700;
176
+ background:linear-gradient(135deg,#e4e4e7,#a1a1aa);
177
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.3px;
178
+ }
179
+ .app-badge{
180
+ font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px;
181
+ background:rgba(255,69,0,.15);color:#FF6633;
182
+ border:1px solid rgba(255,69,0,.25);letter-spacing:.3px;
183
+ }
184
+ .app-badge.fast{
185
+ background:rgba(34,197,94,.12);color:#4ade80;
186
+ border:1px solid rgba(34,197,94,.25);
187
+ }
188
+
189
+ /* ── Toolbar ── */
190
+ .app-toolbar{
191
+ background:#18181b;border-bottom:1px solid #27272a;
192
+ padding:8px 16px;display:flex;gap:4px;align-items:center;flex-wrap:wrap;
193
+ }
194
+ .tb-sep{width:1px;height:28px;background:#27272a;margin:0 8px}
195
+ .modern-tb-btn{
196
+ display:inline-flex;align-items:center;justify-content:center;gap:6px;
197
+ min-width:32px;height:34px;background:transparent;
198
+ border:1px solid transparent;border-radius:8px;cursor:pointer;
199
+ font-size:13px;font-weight:600;padding:0 12px;
200
+ font-family:'Inter',sans-serif;color:#ffffff!important;
201
+ -webkit-text-fill-color:#ffffff!important;transition:all .15s ease;
202
+ }
203
+ .modern-tb-btn:hover{
204
+ background:rgba(255,69,0,.15);color:#ffffff!important;
205
+ -webkit-text-fill-color:#ffffff!important;border-color:rgba(255,69,0,.3);
206
+ }
207
+ .modern-tb-btn:active,.modern-tb-btn.active{
208
+ background:rgba(255,69,0,.25);color:#ffffff!important;
209
+ -webkit-text-fill-color:#ffffff!important;border-color:rgba(255,69,0,.45);
210
+ }
211
+ .modern-tb-btn .tb-icon{font-size:15px;line-height:1;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
212
+ .modern-tb-btn .tb-label{font-size:13px;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;font-weight:600}
213
+ .tb-info{
214
+ font-family:'JetBrains Mono',monospace;font-size:12px;
215
+ color:#71717a;padding:0 8px;display:flex;align-items:center;
216
+ }
217
+
218
+ /* ── Main Layout ── */
219
+ .app-main-row{display:flex;gap:0;flex:1;overflow:hidden}
220
+ .app-main-left{
221
+ flex:1;display:flex;flex-direction:column;min-width:0;
222
+ border-right:1px solid #27272a;
223
+ }
224
+ .app-main-right{
225
+ width:420px;display:flex;flex-direction:column;flex-shrink:0;background:#18181b;
226
+ }
227
+
228
+ /* ── Gallery Drop Zone ── */
229
+ #gallery-drop-zone{
230
+ position:relative;background:#09090b;min-height:440px;overflow:auto;
231
+ }
232
+ #gallery-drop-zone.drag-over{
233
+ outline:2px solid #FF4500;outline-offset:-2px;
234
+ background:rgba(255,69,0,.04);
235
+ }
236
+
237
+ /* ── Upload Prompt ── */
238
+ .upload-prompt-modern{
239
+ position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:20;
240
+ }
241
+ .upload-click-area{
242
+ display:flex;flex-direction:column;align-items:center;justify-content:center;
243
+ cursor:pointer;padding:36px 52px;border:2px dashed #3f3f46;
244
+ border-radius:16px;background:rgba(255,69,0,.03);transition:all .2s ease;gap:8px;
245
+ }
246
+ .upload-click-area:hover{
247
+ background:rgba(255,69,0,.08);border-color:#FF4500;transform:scale(1.03);
248
+ }
249
+ .upload-click-area:active{background:rgba(255,69,0,.12);transform:scale(.98)}
250
+ .upload-click-area svg{width:80px;height:80px}
251
+ .upload-main-text{color:#71717a;font-size:14px;font-weight:500;margin-top:4px}
252
+ .upload-sub-text{color:#52525b;font-size:12px}
253
+
254
+ /* ── Gallery Grid ── */
255
+ .image-gallery-grid{
256
+ display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
257
+ gap:12px;padding:16px;align-content:start;
258
+ }
259
+ .gallery-thumb{
260
+ position:relative;aspect-ratio:1;border-radius:10px;overflow:hidden;
261
+ cursor:pointer;border:2px solid #27272a;transition:all .2s ease;background:#18181b;
262
+ }
263
+ .gallery-thumb:hover{
264
+ border-color:#3f3f46;transform:translateY(-2px);
265
+ box-shadow:0 4px 12px rgba(0,0,0,.4);
266
+ }
267
+ .gallery-thumb.selected{
268
+ border-color:#FF4500!important;box-shadow:0 0 0 3px rgba(255,69,0,.2);
269
+ }
270
+ .gallery-thumb img{width:100%;height:100%;object-fit:cover}
271
+ .thumb-badge{
272
+ position:absolute;top:6px;left:6px;background:#FF4500;color:#fff;
273
+ padding:2px 8px;border-radius:4px;font-family:'JetBrains Mono',monospace;
274
+ font-size:11px;font-weight:600;
275
+ }
276
+ .thumb-remove{
277
+ position:absolute;top:6px;right:6px;width:24px;height:24px;
278
+ background:rgba(0,0,0,.75);color:#fff;border:1px solid rgba(255,255,255,.15);
279
+ border-radius:50%;cursor:pointer;display:none;align-items:center;
280
+ justify-content:center;font-size:12px;transition:all .15s;line-height:1;
281
+ }
282
+ .gallery-thumb:hover .thumb-remove{display:flex}
283
+ .thumb-remove:hover{background:#FF4500;border-color:#FF4500}
284
+ .gallery-add-card{
285
+ aspect-ratio:1;border-radius:10px;border:2px dashed #3f3f46;
286
+ display:flex;flex-direction:column;align-items:center;justify-content:center;
287
+ cursor:pointer;transition:all .2s ease;background:rgba(255,69,0,.03);gap:4px;
288
+ }
289
+ .gallery-add-card:hover{border-color:#FF4500;background:rgba(255,69,0,.08)}
290
+ .gallery-add-card .add-icon{font-size:28px;color:#71717a;font-weight:300}
291
+ .gallery-add-card .add-text{font-size:12px;color:#71717a;font-weight:500}
292
+
293
+ /* ── Hint Bar ── */
294
+ .hint-bar{
295
+ background:rgba(255,69,0,.06);border-top:1px solid #27272a;
296
+ border-bottom:1px solid #27272a;padding:10px 20px;
297
+ font-size:13px;color:#a1a1aa;line-height:1.7;
298
+ }
299
+ .hint-bar b{color:#FF8C66;font-weight:600}
300
+ .hint-bar kbd{
301
+ display:inline-block;padding:1px 6px;background:#27272a;
302
+ border:1px solid #3f3f46;border-radius:4px;
303
+ font-family:'JetBrains Mono',monospace;font-size:11px;color:#a1a1aa;
304
+ }
305
+
306
+ /* ── Suggestions ── */
307
+ .suggestions-section{border-top:1px solid #27272a;padding:12px 16px}
308
+ .suggestions-title{
309
+ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;
310
+ letter-spacing:.8px;margin-bottom:10px;
311
+ }
312
+ .suggestions-wrap{display:flex;flex-wrap:wrap;gap:6px}
313
+ .suggestion-chip{
314
+ display:inline-flex;align-items:center;gap:4px;padding:5px 12px;
315
+ background:rgba(255,69,0,.08);border:1px solid rgba(255,69,0,.2);
316
+ border-radius:20px;color:#FF8C66;font-size:12px;font-weight:500;
317
+ font-family:'Inter',sans-serif;cursor:pointer;transition:all .15s;white-space:nowrap;
318
+ }
319
+ .suggestion-chip:hover{
320
+ background:rgba(255,69,0,.15);border-color:rgba(255,69,0,.35);
321
+ color:#FF6633;transform:translateY(-1px);
322
+ }
323
+
324
+ /* ── Right Panel Cards ── */
325
+ .panel-card{border-bottom:1px solid #27272a}
326
+ .panel-card-title{
327
+ padding:12px 20px;font-size:12px;font-weight:600;color:#71717a;
328
+ text-transform:uppercase;letter-spacing:.8px;
329
+ border-bottom:1px solid rgba(39,39,42,.6);
330
+ }
331
+ .panel-card-body{padding:16px 20px;display:flex;flex-direction:column;gap:8px}
332
+ .modern-label{font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block}
333
+ .modern-textarea{
334
+ width:100%;background:#09090b;border:1px solid #27272a;border-radius:8px;
335
+ padding:10px 14px;font-family:'Inter',sans-serif;font-size:14px;
336
+ color:#e4e4e7;resize:vertical;outline:none;min-height:42px;transition:border-color .2s;
337
+ }
338
+ .modern-textarea:focus{border-color:#FF4500;box-shadow:0 0 0 3px rgba(255,69,0,.15)}
339
+ .modern-textarea::placeholder{color:#3f3f46}
340
+
341
+ /* ── Primary Button β€” White text+SVG in ALL themes ── */
342
+ .btn-run{
343
+ display:flex;align-items:center;justify-content:center;gap:8px;
344
+ width:100%;background:linear-gradient(135deg,#FF4500,#E63E00);
345
+ border:none;border-radius:10px;padding:12px 24px;cursor:pointer;
346
+ font-size:15px;font-weight:600;font-family:'Inter',sans-serif;
347
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
348
+ transition:all .2s ease;letter-spacing:-.2px;
349
+ box-shadow:0 4px 16px rgba(255,69,0,.3),inset 0 1px 0 rgba(255,255,255,.1);
350
+ }
351
+ .btn-run:hover{
352
+ background:linear-gradient(135deg,#FF6633,#FF4500);
353
+ box-shadow:0 6px 24px rgba(255,69,0,.45),inset 0 1px 0 rgba(255,255,255,.15);
354
+ transform:translateY(-1px);color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
355
+ }
356
+ .btn-run:active{
357
+ transform:translateY(0);box-shadow:0 2px 8px rgba(255,69,0,.3);
358
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
359
+ }
360
+ .btn-run:focus{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
361
+ .btn-run svg{width:18px;height:18px;fill:#ffffff!important;color:#ffffff!important}
362
+ .btn-run svg path{fill:#ffffff!important;color:#ffffff!important}
363
+ .btn-run span,
364
+ .btn-run #run-btn-label,
365
+ #run-btn-label{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
366
+
367
+ /* Force white on run button β€” every selector/theme */
368
+ #custom-run-btn,#custom-run-btn *,#custom-run-btn span,
369
+ #custom-run-btn svg,#custom-run-btn svg *,#custom-run-btn svg path,
370
+ #custom-run-btn #run-btn-label,.btn-run *{
371
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
372
+ }
373
+ body:not(.dark) .btn-run,body:not(.dark) .btn-run *,body:not(.dark) #custom-run-btn,
374
+ body:not(.dark) #custom-run-btn *,body:not(.dark) #run-btn-label{
375
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
376
+ }
377
+ .dark .btn-run,.dark .btn-run *,.dark #custom-run-btn,.dark #custom-run-btn *,
378
+ .dark #run-btn-label{
379
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
380
+ }
381
+ .gradio-container .btn-run,.gradio-container .btn-run *,.gradio-container #custom-run-btn,
382
+ .gradio-container #custom-run-btn *,.gradio-container #run-btn-label{
383
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
384
+ }
385
+
386
+ /* ── Toolbar btn white in all themes ── */
387
+ body:not(.dark) .modern-tb-btn,body:not(.dark) .modern-tb-btn *{
388
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
389
+ }
390
+ .dark .modern-tb-btn,.dark .modern-tb-btn *{
391
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
392
+ }
393
+ .gradio-container .modern-tb-btn,.gradio-container .modern-tb-btn *{
394
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
395
+ }
396
+
397
+ /* ── Output Frames ── */
398
+ .output-frame{
399
+ border-bottom:1px solid #27272a;display:flex;flex-direction:column;position:relative;
400
+ }
401
+ .output-frame .out-title{
402
+ padding:10px 20px;font-size:13px;font-weight:700;
403
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
404
+ text-transform:uppercase;letter-spacing:.8px;
405
+ border-bottom:1px solid rgba(39,39,42,.6);
406
+ display:flex;align-items:center;justify-content:space-between;
407
+ }
408
+ .output-frame .out-title span{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
409
+ .output-frame .out-body{
410
+ flex:1;background:#09090b;display:flex;align-items:center;justify-content:center;
411
+ overflow:hidden;min-height:240px;position:relative;
412
+ }
413
+ .output-frame .out-body img{max-width:100%;max-height:460px;image-rendering:auto}
414
+ .output-frame .out-placeholder{color:#3f3f46;font-size:13px;text-align:center;padding:20px}
415
+
416
+ .out-download-btn{
417
+ display:none;align-items:center;justify-content:center;
418
+ background:rgba(255,69,0,.1);border:1px solid rgba(255,69,0,.2);
419
+ border-radius:6px;cursor:pointer;padding:3px 10px;font-size:11px;
420
+ font-weight:500;color:#FF8C66!important;gap:4px;height:24px;transition:all .15s;
421
+ }
422
+ .out-download-btn:hover{
423
+ background:rgba(255,69,0,.2);border-color:rgba(255,69,0,.35);color:#ffffff!important;
424
+ }
425
+ .out-download-btn.visible{display:inline-flex}
426
+ .out-download-btn svg{width:12px;height:12px;fill:#FF8C66}
427
 
428
+ /* ── Loader ── */
429
+ .modern-loader{
430
+ display:none;position:absolute;top:0;left:0;right:0;bottom:0;
431
+ background:rgba(9,9,11,.92);z-index:15;flex-direction:column;
432
+ align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(4px);
433
+ }
434
+ .modern-loader.active{display:flex}
435
+ .modern-loader .loader-spinner{
436
+ width:36px;height:36px;border:3px solid #27272a;
437
+ border-top-color:#FF4500;border-radius:50%;animation:spin .8s linear infinite;
438
+ }
439
+ @keyframes spin{to{transform:rotate(360deg)}}
440
+ .modern-loader .loader-text{font-size:13px;color:#a1a1aa;font-weight:500}
441
+ .loader-bar-track{width:200px;height:4px;background:#27272a;border-radius:2px;overflow:hidden}
442
+ .loader-bar-fill{
443
+ height:100%;background:linear-gradient(90deg,#FF4500,#FF6633,#FF4500);
444
+ background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px;
445
+ }
446
+ @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
447
+
448
+ /* ── Settings ── */
449
+ .settings-group{
450
+ border:1px solid #27272a;border-radius:10px;margin:12px 16px;padding:0;overflow:hidden;
451
+ }
452
+ .settings-group-title{
453
+ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;
454
+ letter-spacing:.8px;padding:10px 16px;border-bottom:1px solid #27272a;
455
+ background:rgba(24,24,27,.5);
456
+ }
457
+ .settings-group-body{padding:14px 16px;display:flex;flex-direction:column;gap:12px}
458
+ .slider-row{display:flex;align-items:center;gap:10px;min-height:28px}
459
+ .slider-row label{font-size:13px;font-weight:500;color:#a1a1aa;min-width:72px;flex-shrink:0}
460
+ .slider-row input[type="range"]{
461
+ flex:1;-webkit-appearance:none;appearance:none;height:6px;
462
+ background:#27272a;border-radius:3px;outline:none;min-width:0;
463
+ }
464
+ .slider-row input[type="range"]::-webkit-slider-thumb{
465
+ -webkit-appearance:none;appearance:none;width:16px;height:16px;
466
+ background:linear-gradient(135deg,#FF4500,#E63E00);border-radius:50%;
467
+ cursor:pointer;box-shadow:0 2px 6px rgba(255,69,0,.4);transition:transform .15s;
468
+ }
469
+ .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)}
470
+ .slider-row input[type="range"]::-moz-range-thumb{
471
+ width:16px;height:16px;background:linear-gradient(135deg,#FF4500,#E63E00);
472
+ border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(255,69,0,.4);
473
+ }
474
+ .slider-row .slider-val{
475
+ min-width:52px;text-align:right;font-family:'JetBrains Mono',monospace;
476
+ font-size:12px;font-weight:500;padding:3px 8px;background:#09090b;
477
+ border:1px solid #27272a;border-radius:6px;color:#a1a1aa;flex-shrink:0;
478
+ }
479
+ .checkbox-row{display:flex;align-items:center;gap:8px;font-size:13px;color:#a1a1aa}
480
+ .checkbox-row input[type="checkbox"]{accent-color:#FF4500;width:16px;height:16px;cursor:pointer}
481
+ .checkbox-row label{color:#a1a1aa;font-size:13px;cursor:pointer}
482
+
483
+ /* ── Status Bar ── */
484
+ .app-statusbar{
485
+ background:#18181b;border-top:1px solid #27272a;padding:6px 20px;
486
+ display:flex;gap:12px;height:34px;align-items:center;font-size:12px;
487
+ }
488
+ .app-statusbar .sb-section{
489
+ padding:0 12px;flex:1;display:flex;align-items:center;
490
+ font-family:'JetBrains Mono',monospace;font-size:12px;color:#52525b;
491
+ overflow:hidden;white-space:nowrap;
492
+ }
493
+ .app-statusbar .sb-section.sb-fixed{
494
+ flex:0 0 auto;min-width:90px;text-align:center;justify-content:center;
495
+ padding:3px 12px;background:rgba(255,69,0,.08);border-radius:6px;
496
+ color:#FF6633;font-weight:500;
497
+ }
498
+
499
+ #gradio-run-btn{
500
+ position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;
501
+ opacity:0.01;pointer-events:none;overflow:hidden;
502
+ }
503
+
504
+ /* ── Experimental Note ── */
505
+ .exp-note{
506
+ padding:10px 20px;font-size:12px;color:#52525b;border-top:1px solid #27272a;
507
+ text-align:center;
508
+ }
509
+ .exp-note a{color:#FF6633;text-decoration:none}
510
+ .exp-note a:hover{text-decoration:underline}
511
+
512
+ /* ── Dark mode extras ── */
513
+ .dark .app-shell{background:#18181b}
514
+ .dark .upload-prompt-modern{background:transparent}
515
+ .dark .panel-card{background:#18181b}
516
+ .dark .settings-group{background:#18181b}
517
+ .dark .output-frame .out-title{color:#ffffff!important}
518
+ .dark .output-frame .out-title span{color:#ffffff!important}
519
+ .dark .out-download-btn{color:#FF8C66!important}
520
+ .dark .out-download-btn:hover{color:#ffffff!important}
521
+
522
+ /* ── Scrollbar ── */
523
+ ::-webkit-scrollbar{width:8px;height:8px}
524
+ ::-webkit-scrollbar-track{background:#09090b}
525
+ ::-webkit-scrollbar-thumb{background:#27272a;border-radius:4px}
526
+ ::-webkit-scrollbar-thumb:hover{background:#3f3f46}
527
+
528
+ /* ── Responsive ── */
529
+ @media(max-width:840px){
530
+ .app-main-row{flex-direction:column}
531
+ .app-main-right{width:100%}
532
+ .app-main-left{border-right:none;border-bottom:1px solid #27272a}
533
  }
 
534
  """
535
 
536
+ gallery_js = r"""
537
+ () => {
538
+ function init() {
539
+ if (window.__fireRedInitDone) return;
540
+
541
+ const galleryGrid = document.getElementById('image-gallery-grid');
542
+ const dropZone = document.getElementById('gallery-drop-zone');
543
+ const uploadPrompt = document.getElementById('upload-prompt');
544
+ const uploadClick = document.getElementById('upload-click-area');
545
+ const fileInput = document.getElementById('custom-file-input');
546
+ const btnUpload = document.getElementById('tb-upload');
547
+ const btnRemove = document.getElementById('tb-remove');
548
+ const btnClear = document.getElementById('tb-clear');
549
+ const promptInput = document.getElementById('custom-prompt-input');
550
+ const runBtnEl = document.getElementById('custom-run-btn');
551
+ const imgCountTb = document.getElementById('tb-image-count');
552
+ const imgCountSb = document.getElementById('sb-image-count');
553
+
554
+ if (!galleryGrid || !fileInput || !dropZone) {
555
+ setTimeout(init, 250);
556
+ return;
557
+ }
558
+
559
+ window.__fireRedInitDone = true;
560
+
561
+ let images = [];
562
+ window.__uploadedImages = images;
563
+ let selectedIdx = -1;
564
+
565
+ function setGradioValue(containerId, value) {
566
+ const container = document.getElementById(containerId);
567
+ if (!container) return;
568
+ container.querySelectorAll('input, textarea').forEach(el => {
569
+ if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return;
570
+ const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
571
+ const ns = Object.getOwnPropertyDescriptor(proto, 'value');
572
+ if (ns && ns.set) {
573
+ ns.set.call(el, value);
574
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
575
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
576
+ }
577
+ });
578
+ }
579
+
580
+ function syncImagesToGradio() {
581
+ window.__uploadedImages = images;
582
+ const b64Array = images.map(img => img.b64);
583
+ setGradioValue('hidden-images-b64', JSON.stringify(b64Array));
584
+ updateCounts();
585
+ }
586
+
587
+ function syncPromptToGradio() {
588
+ if (promptInput) setGradioValue('prompt-gradio-input', promptInput.value);
589
+ }
590
+
591
+ function updateCounts() {
592
+ const n = images.length;
593
+ const txt = n > 0 ? n + ' image' + (n > 1 ? 's' : '') : 'No images';
594
+ if (imgCountTb) imgCountTb.textContent = txt;
595
+ if (imgCountSb) imgCountSb.textContent = n > 0 ? txt + ' uploaded' : 'No images uploaded';
596
+ }
597
+
598
+ function addImage(b64, name) {
599
+ images.push({id: Date.now() + Math.random(), b64: b64, name: name});
600
+ renderGallery();
601
+ syncImagesToGradio();
602
+ }
603
+
604
+ function removeImage(idx) {
605
+ images.splice(idx, 1);
606
+ if (selectedIdx === idx) selectedIdx = -1;
607
+ else if (selectedIdx > idx) selectedIdx--;
608
+ renderGallery();
609
+ syncImagesToGradio();
610
+ }
611
+
612
+ function clearAll() {
613
+ images = [];
614
+ window.__uploadedImages = images;
615
+ selectedIdx = -1;
616
+ renderGallery();
617
+ syncImagesToGradio();
618
+ }
619
+
620
+ function selectImage(idx) {
621
+ selectedIdx = (selectedIdx === idx) ? -1 : idx;
622
+ renderGallery();
623
+ }
624
+
625
+ function renderGallery() {
626
+ if (images.length === 0) {
627
+ galleryGrid.innerHTML = '';
628
+ galleryGrid.style.display = 'none';
629
+ if (uploadPrompt) uploadPrompt.style.display = '';
630
+ return;
631
+ }
632
+ if (uploadPrompt) uploadPrompt.style.display = 'none';
633
+ galleryGrid.style.display = 'grid';
634
+
635
+ let html = '';
636
+ images.forEach((img, i) => {
637
+ const sel = i === selectedIdx ? ' selected' : '';
638
+ html += '<div class="gallery-thumb' + sel + '" data-idx="' + i + '">'
639
+ + '<img src="' + img.b64 + '" alt="' + (img.name||'image') + '">'
640
+ + '<span class="thumb-badge">#' + (i+1) + '</span>'
641
+ + '<button class="thumb-remove" data-remove="' + i + '">\u2715</button>'
642
+ + '</div>';
643
+ });
644
+ html += '<div class="gallery-add-card" id="gallery-add-card">'
645
+ + '<span class="add-icon">+</span>'
646
+ + '<span class="add-text">Add</span>'
647
+ + '</div>';
648
+
649
+ galleryGrid.innerHTML = html;
650
+
651
+ galleryGrid.querySelectorAll('.gallery-thumb').forEach(thumb => {
652
+ thumb.addEventListener('click', (e) => {
653
+ if (e.target.closest('.thumb-remove')) return;
654
+ selectImage(parseInt(thumb.dataset.idx));
655
+ });
656
+ });
657
+ galleryGrid.querySelectorAll('.thumb-remove').forEach(btn => {
658
+ btn.addEventListener('click', (e) => {
659
+ e.stopPropagation();
660
+ removeImage(parseInt(btn.dataset.remove));
661
+ });
662
+ });
663
+ const addCard = document.getElementById('gallery-add-card');
664
+ if (addCard) addCard.addEventListener('click', () => fileInput.click());
665
+ }
666
+
667
+ function processFiles(files) {
668
+ Array.from(files).forEach(file => {
669
+ if (!file.type.startsWith('image/')) return;
670
+ const reader = new FileReader();
671
+ reader.onload = (e) => addImage(e.target.result, file.name);
672
+ reader.readAsDataURL(file);
673
+ });
674
+ }
675
+
676
+ fileInput.addEventListener('change', (e) => {
677
+ processFiles(e.target.files);
678
+ e.target.value = '';
679
+ });
680
+
681
+ if (uploadClick) uploadClick.addEventListener('click', () => fileInput.click());
682
+ if (btnUpload) btnUpload.addEventListener('click', () => fileInput.click());
683
+ if (btnRemove) btnRemove.addEventListener('click', () => {
684
+ if (selectedIdx >= 0 && selectedIdx < images.length) removeImage(selectedIdx);
685
+ });
686
+ if (btnClear) btnClear.addEventListener('click', clearAll);
687
+
688
+ dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
689
+ dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); });
690
+ dropZone.addEventListener('drop', (e) => {
691
+ e.preventDefault(); dropZone.classList.remove('drag-over');
692
+ if (e.dataTransfer.files.length) processFiles(e.dataTransfer.files);
693
+ });
694
+
695
+ if (promptInput) promptInput.addEventListener('input', syncPromptToGradio);
696
+
697
+ window.__setPrompt = function(text) {
698
+ if (promptInput) { promptInput.value = text; syncPromptToGradio(); }
699
+ };
700
+
701
+ function syncSlider(customId, gradioId) {
702
+ const slider = document.getElementById(customId);
703
+ const valSpan = document.getElementById(customId + '-val');
704
+ if (!slider) return;
705
+ slider.addEventListener('input', () => {
706
+ if (valSpan) valSpan.textContent = slider.value;
707
+ const container = document.getElementById(gradioId);
708
+ if (!container) return;
709
+ container.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => {
710
+ const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
711
+ if (ns && ns.set) {
712
+ ns.set.call(el, slider.value);
713
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
714
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
715
+ }
716
+ });
717
+ });
718
+ }
719
+ syncSlider('custom-seed', 'gradio-seed');
720
+ syncSlider('custom-guidance', 'gradio-guidance');
721
+ syncSlider('custom-steps', 'gradio-steps');
722
+
723
+ const randCheck = document.getElementById('custom-randomize');
724
+ if (randCheck) {
725
+ randCheck.addEventListener('change', () => {
726
+ const container = document.getElementById('gradio-randomize');
727
+ if (!container) return;
728
+ const cb = container.querySelector('input[type="checkbox"]');
729
+ if (cb && cb.checked !== randCheck.checked) cb.click();
730
+ });
731
+ }
732
+
733
+ function showLoader() {
734
+ const l = document.getElementById('output-loader');
735
+ if (l) l.classList.add('active');
736
+ const sb = document.querySelector('.sb-fixed');
737
+ if (sb) sb.textContent = 'Processing...';
738
+ }
739
+ function hideLoader() {
740
+ const l = document.getElementById('output-loader');
741
+ if (l) l.classList.remove('active');
742
+ const sb = document.querySelector('.sb-fixed');
743
+ if (sb) sb.textContent = 'Done';
744
+ }
745
+ window.__showLoader = showLoader;
746
+ window.__hideLoader = hideLoader;
747
+
748
+ window.__clickGradioRunBtn = function() {
749
+ syncPromptToGradio();
750
+ syncImagesToGradio();
751
+ showLoader();
752
+ setTimeout(() => {
753
+ const gradioBtn = document.getElementById('gradio-run-btn');
754
+ if (!gradioBtn) return;
755
+ const btn = gradioBtn.querySelector('button');
756
+ if (btn) btn.click(); else gradioBtn.click();
757
+ }, 200);
758
+ };
759
+
760
+ if (runBtnEl) runBtnEl.addEventListener('click', () => window.__clickGradioRunBtn());
761
+
762
+ renderGallery();
763
+ updateCounts();
764
+ }
765
+ init();
766
+ }
767
+ """
768
+
769
+ wire_outputs_js = r"""
770
+ () => {
771
+ function watchOutputs() {
772
+ const resultContainer = document.getElementById('gradio-result');
773
+ const outBody = document.getElementById('output-image-container');
774
+ const outPh = document.getElementById('output-placeholder');
775
+ const dlBtn = document.getElementById('dl-btn-output');
776
+
777
+ if (!resultContainer || !outBody) { setTimeout(watchOutputs, 500); return; }
778
+
779
+ if (dlBtn) {
780
+ dlBtn.addEventListener('click', (e) => {
781
+ e.stopPropagation();
782
+ const img = outBody.querySelector('img.modern-out-img');
783
+ if (img && img.src) {
784
+ const a = document.createElement('a');
785
+ a.href = img.src; a.download = 'firered_output.png';
786
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
787
+ }
788
+ });
789
+ }
790
+
791
+ function syncImage() {
792
+ const resultImg = resultContainer.querySelector('img');
793
+ if (resultImg && resultImg.src) {
794
+ if (outPh) outPh.style.display = 'none';
795
+ let existing = outBody.querySelector('img.modern-out-img');
796
+ if (!existing) {
797
+ existing = document.createElement('img');
798
+ existing.className = 'modern-out-img';
799
+ outBody.appendChild(existing);
800
+ }
801
+ if (existing.src !== resultImg.src) {
802
+ existing.src = resultImg.src;
803
+ if (dlBtn) dlBtn.classList.add('visible');
804
+ if (window.__hideLoader) window.__hideLoader();
805
+ }
806
+ }
807
+ }
808
+
809
+ const observer = new MutationObserver(syncImage);
810
+ observer.observe(resultContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']});
811
+ setInterval(syncImage, 800);
812
+ }
813
+ watchOutputs();
814
+
815
+ function watchSeed() {
816
+ const seedContainer = document.getElementById('gradio-seed');
817
+ const seedSlider = document.getElementById('custom-seed');
818
+ const seedVal = document.getElementById('custom-seed-val');
819
+ if (!seedContainer || !seedSlider) { setTimeout(watchSeed, 500); return; }
820
+ function sync() {
821
+ const el = seedContainer.querySelector('input[type="range"],input[type="number"]');
822
+ if (el && el.value) {
823
+ seedSlider.value = el.value;
824
+ if (seedVal) seedVal.textContent = el.value;
825
+ }
826
+ }
827
+ const obs = new MutationObserver(sync);
828
+ obs.observe(seedContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']});
829
+ setInterval(sync, 1000);
830
+ }
831
+ watchSeed();
832
+ }
833
+ """
834
+
835
+ 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>'
836
+
837
  with gr.Blocks() as demo:
838
+
839
+ hidden_images_b64 = gr.Textbox(
840
+ value="[]", elem_id="hidden-images-b64",
841
+ elem_classes="hidden-input", container=False,
842
+ )
843
+ prompt = gr.Textbox(
844
+ value="", elem_id="prompt-gradio-input",
845
+ elem_classes="hidden-input", container=False,
846
+ )
847
+ seed = gr.Slider(
848
+ minimum=0, maximum=MAX_SEED, step=1, value=0,
849
+ elem_id="gradio-seed", elem_classes="hidden-input", container=False,
850
+ )
851
+ randomize_seed = gr.Checkbox(
852
+ value=True, elem_id="gradio-randomize",
853
+ elem_classes="hidden-input", container=False,
854
+ )
855
+ guidance_scale = gr.Slider(
856
+ minimum=1.0, maximum=10.0, step=0.1, value=1.0,
857
+ elem_id="gradio-guidance", elem_classes="hidden-input", container=False,
858
+ )
859
+ steps = gr.Slider(
860
+ minimum=1, maximum=50, step=1, value=4,
861
+ elem_id="gradio-steps", elem_classes="hidden-input", container=False,
862
+ )
863
+ result = gr.Image(
864
+ elem_id="gradio-result", elem_classes="hidden-input",
865
+ container=False, format="png",
866
+ )
867
+
868
+ gr.HTML(f"""
869
+ <div class="app-shell">
870
+
871
+ <!-- ══ Header ══ -->
872
+ <div class="app-header">
873
+ <div class="app-header-left">
874
+ <div class="app-logo">πŸ”₯</div>
875
+ <span class="app-title">FireRed-Image-Edit</span>
876
+ <span class="app-badge">v1.1</span>
877
+ <span class="app-badge fast">4-Step Fast</span>
878
+ </div>
879
+ </div>
880
+
881
+ <!-- ══ Toolbar ══ -->
882
+ <div class="app-toolbar">
883
+ <button id="tb-upload" class="modern-tb-btn" title="Upload images">
884
+ <span class="tb-icon">πŸ“</span><span class="tb-label">Upload</span>
885
+ </button>
886
+ <button id="tb-remove" class="modern-tb-btn" title="Remove selected image">
887
+ <span class="tb-icon">βœ•</span><span class="tb-label">Remove</span>
888
+ </button>
889
+ <button id="tb-clear" class="modern-tb-btn" title="Clear all images">
890
+ <span class="tb-icon">βœ–</span><span class="tb-label">Clear All</span>
891
+ </button>
892
+ <div class="tb-sep"></div>
893
+ <span id="tb-image-count" class="tb-info">No images</span>
894
+ </div>
895
+
896
+ <!-- ══ Main ══ -->
897
+ <div class="app-main-row">
898
+
899
+ <!-- Left: Gallery -->
900
+ <div class="app-main-left">
901
+ <div id="gallery-drop-zone">
902
+ <div id="upload-prompt" class="upload-prompt-modern">
903
+ <div id="upload-click-area" class="upload-click-area">
904
+ <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
905
+ <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#FF4500" stroke-width="2" stroke-dasharray="4 3"/>
906
+ <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(255,69,0,0.15)" stroke="#FF4500" stroke-width="1.5"/>
907
+ <circle cx="28" cy="30" r="6" fill="rgba(255,69,0,0.2)" stroke="#FF4500" stroke-width="1.5"/>
908
+ </svg>
909
+ <span class="upload-main-text">Click or drag images here</span>
910
+ <span class="upload-sub-text">Supports multiple images for reference-based editing</span>
911
+ </div>
912
+ </div>
913
+ <input id="custom-file-input" type="file" accept="image/*" multiple style="display:none;" />
914
+ <div id="image-gallery-grid" class="image-gallery-grid" style="display:none;"></div>
915
+ </div>
916
+
917
+ <div class="hint-bar">
918
+ <b>Upload:</b> Click or drag to add images &nbsp;Β·&nbsp;
919
+ <b>Multi-image:</b> Upload multiple images for reference-based editing &nbsp;Β·&nbsp;
920
+ <kbd>Remove</kbd> deletes selected &nbsp;Β·&nbsp;
921
+ <kbd>Clear All</kbd> removes everything
922
+ </div>
923
+
924
+ <div class="suggestions-section">
925
+ <div class="suggestions-title">Quick Prompts</div>
926
+ <div class="suggestions-wrap">
927
+ <button class="suggestion-chip" onclick="window.__setPrompt('Transform the image into a dotted cartoon style.')">🎨 Cartoon</button>
928
+ <button class="suggestion-chip" onclick="window.__setPrompt('Convert it to black and white.')">⚫ B&amp;W</button>
929
+ <button class="suggestion-chip" onclick="window.__setPrompt('Add cinematic lighting with warm orange tones and film grain.')">🎬 Cinematic</button>
930
+ <button class="suggestion-chip" onclick="window.__setPrompt('Transform into anime style illustration.')">✨ Anime</button>
931
+ <button class="suggestion-chip" onclick="window.__setPrompt('Apply oil painting effect with visible brush strokes.')">πŸ–ŒοΈ Oil Paint</button>
932
+ <button class="suggestion-chip" onclick="window.__setPrompt('Enhance and upscale with more detail and clarity.')">πŸ” Enhance</button>
933
+ <button class="suggestion-chip" onclick="window.__setPrompt('Make it look like a watercolor painting with soft edges.')">πŸ’§ Watercolor</button>
934
+ <button class="suggestion-chip" onclick="window.__setPrompt('Add dramatic sunset sky and warm lighting.')">πŸŒ… Sunset</button>
935
+ </div>
936
+ </div>
937
+ </div>
938
+
939
+ <!-- Right: Controls & Output -->
940
+ <div class="app-main-right">
941
+
942
+ <div class="panel-card">
943
+ <div class="panel-card-title">Edit Instruction</div>
944
+ <div class="panel-card-body">
945
+ <label class="modern-label" for="custom-prompt-input">Prompt</label>
946
+ <textarea id="custom-prompt-input" class="modern-textarea" rows="3" placeholder="e.g., transform into anime, upscale, change lighting..."></textarea>
947
+ </div>
948
+ </div>
949
+
950
+ <div style="padding:12px 20px;">
951
+ <button id="custom-run-btn" class="btn-run">
952
+ <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>
953
+ <span id="run-btn-label">Edit Image</span>
954
+ </button>
955
+ </div>
956
+
957
+ <div class="output-frame" style="flex:1">
958
+ <div class="out-title">
959
+ <span>Output</span>
960
+ <span id="dl-btn-output" class="out-download-btn" title="Download">
961
+ {DOWNLOAD_SVG} Save
962
+ </span>
963
+ </div>
964
+ <div class="out-body" id="output-image-container">
965
+ <div class="modern-loader" id="output-loader">
966
+ <div class="loader-spinner"></div>
967
+ <div class="loader-text">Processing image…</div>
968
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
969
+ </div>
970
+ <div class="out-placeholder" id="output-placeholder">Result will appear here</div>
971
+ </div>
972
+ </div>
973
+
974
+ <div class="settings-group">
975
+ <div class="settings-group-title">Advanced Settings</div>
976
+ <div class="settings-group-body">
977
+ <div class="slider-row">
978
+ <label>Seed</label>
979
+ <input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0">
980
+ <span class="slider-val" id="custom-seed-val">0</span>
981
+ </div>
982
+ <div class="checkbox-row">
983
+ <input type="checkbox" id="custom-randomize" checked>
984
+ <label for="custom-randomize">Randomize seed</label>
985
+ </div>
986
+ <div class="slider-row">
987
+ <label>Guidance</label>
988
+ <input type="range" id="custom-guidance" min="1" max="10" step="0.1" value="1.0">
989
+ <span class="slider-val" id="custom-guidance-val">1.0</span>
990
+ </div>
991
+ <div class="slider-row">
992
+ <label>Steps</label>
993
+ <input type="range" id="custom-steps" min="1" max="50" step="1" value="4">
994
+ <span class="slider-val" id="custom-steps-val">4</span>
995
+ </div>
996
+ </div>
997
+ </div>
998
+
999
+ </div>
1000
+ </div>
1001
+
1002
+ <!-- ══ Note ══ -->
1003
+ <div class="exp-note">
1004
+ Experimental Space for <a href="https://huggingface.co/FireRedTeam/FireRed-Image-Edit-1.1" target="_blank">FireRed-Image-Edit-1.1</a>
1005
+ Β· Open on <a href="https://github.com/PRITHIVSAKTHIUR/FireRed-Image-Edit-1.0-Fast" target="_blank">GitHub</a>
1006
+ </div>
1007
+
1008
+ <!-- ══ Status Bar ══ -->
1009
+ <div class="app-statusbar">
1010
+ <div class="sb-section" id="sb-image-count">No images uploaded</div>
1011
+ <div class="sb-section sb-fixed">Ready</div>
1012
+ </div>
1013
+
1014
+ </div>
1015
+ """)
1016
+
1017
+ run_btn = gr.Button("Run", elem_id="gradio-run-btn")
1018
+
1019
+ demo.load(fn=None, js=gallery_js)
1020
+ demo.load(fn=None, js=wire_outputs_js)
1021
+
1022
+ run_btn.click(
1023
  fn=infer,
1024
+ inputs=[hidden_images_b64, prompt, seed, randomize_seed, guidance_scale, steps],
1025
+ outputs=[result, seed],
1026
+ js=r"""(imgs, p, s, rs, gs, st) => {
1027
+ const images = window.__uploadedImages || [];
1028
+ const b64Array = images.map(img => img.b64);
1029
+ const imgsJson = JSON.stringify(b64Array);
1030
+ const promptEl = document.getElementById('custom-prompt-input');
1031
+ const promptVal = promptEl ? promptEl.value : p;
1032
+ return [imgsJson, promptVal, s, rs, gs, st];
1033
+ }""",
1034
  )
1035
 
1036
  if __name__ == "__main__":
1037
+ demo.queue(max_size=30).launch(
1038
+ css=css, mcp_server=True, ssr_mode=False, show_error=True
1039
+ )