Files changed (6) hide show
  1. README.md +1 -1
  2. app.py +240 -169
  3. index.html +0 -1383
  4. pre-requirements.txt +1 -1
  5. qwenimage/qwen_fa3_processor.py +60 -151
  6. requirements.txt +14 -13
README.md CHANGED
@@ -4,7 +4,7 @@ emoji: 🔥
4
  colorFrom: indigo
5
  colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.25.0
8
  app_file: app.py
9
  pinned: true
10
  license: apache-2.0
 
4
  colorFrom: indigo
5
  colorTo: gray
6
  sdk: gradio
7
+ sdk_version: 6.3.0
8
  app_file: app.py
9
  pinned: true
10
  license: apache-2.0
app.py CHANGED
@@ -1,41 +1,105 @@
1
  import os
2
  import gc
3
  import gradio as gr
4
- from gradio import Server
5
- from fastapi.responses import HTMLResponse
6
  import numpy as np
7
  import spaces
8
  import torch
9
  import random
10
- import base64
11
- import json
12
- from io import BytesIO
13
  from PIL import Image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  from diffusers import FlowMatchEulerDiscreteScheduler
16
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
17
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
18
  from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
19
 
20
- MAX_SEED = np.iinfo(np.int32).max
21
- LANCZOS = getattr(Image, "Resampling", Image).LANCZOS
22
-
23
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
24
  dtype = torch.bfloat16
25
 
26
- print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
27
- print("torch.__version__ =", torch.__version__)
28
- print("Using device:", device)
29
-
30
- print("Loading FLUX.2 Klein 9B model base...")
31
  pipe = QwenImageEditPlusPipeline.from_pretrained(
32
  "Qwen/Qwen-Image-Edit-2509",
33
  transformer=QwenImageTransformer2DModel.from_pretrained(
34
  "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19",
 
35
  torch_dtype=dtype,
36
- device_map="cuda",
37
  ),
38
- torch_dtype=dtype,
39
  ).to(device)
40
 
41
  try:
@@ -44,7 +108,8 @@ try:
44
  except Exception as e:
45
  print(f"Warning: Could not set FA3 processor: {e}")
46
 
47
- # ── LoRA adapter registry ──────────────────────────────────────────────────────
 
48
  ADAPTER_SPECS = {
49
  "Qwen-Image-Edit-2511-Object-Adder": {
50
  "repo": "prithivMLmods/Qwen-Image-Edit-2511-Object-Adder",
@@ -78,144 +143,86 @@ ADAPTER_SPECS = {
78
  },
79
  }
80
 
81
- LOADED_ADAPTERS: set = set()
82
- ADAPTER_NAMES = list(ADAPTER_SPECS.keys())
83
-
84
- EXAMPLES_CONFIG = [
85
- {"images": ["examples/D.jpg"], "prompt": "Add the batman logo to the image while preserving the background lighting and surrounding elements maintaining realism and original details.", "lora": "Qwen-Image-Edit-2511-Object-Adder"},
86
- {"images": ["examples/A.jpg"], "prompt": "Add the slim rectangular transparent frame sunglasses to the image while preserving the background lighting and surrounding elements maintaining realism and original details.", "lora": "Qwen-Image-Edit-2511-Object-Adder"},
87
- {"images": ["examples/B.jpeg"], "prompt": "Remove the necklace and goggles from the image while preserving the background and remaining elements, maintaining realism and original details.", "lora": "Qwen-Image-Edit-2511-Object-Remover"},
88
- {"images": ["examples/DL2.jpg"], "prompt": "add the nike tick design inside the red marked area.", "lora": "Outfit-Design-Layout"},
89
- {"images": ["examples/DL1.jpg"], "prompt": "add the akatsuki cloud design inside the red marked area.", "lora": "Outfit-Design-Layout"},
90
- {"images": ["examples/C.png"], "prompt": "Add the leather cowboy cap to the image while preserving the background lighting and surrounding elements maintaining realism and original details.", "lora": "Qwen-Image-Edit-2511-Object-Adder"},
91
- {"images": ["examples/ZM.jpg"], "prompt": "Zoom into the red highlighted area.", "lora": "Zoom-Master"},
92
- {"images": ["examples/OBJ1.jpg"], "prompt": "Remove the red highlighted object from the scene.", "lora": "QIE-2511-Object-Remover-v2"},
93
- {"images": ["examples/OBJ2.jpg"], "prompt": "Remove the red highlighted object from the scene.", "lora": "QIE-2511-Object-Remover-v2"},
94
- {"images": ["examples/OE.jpg"], "prompt": "Extract the clothing and create a flat mockup.", "lora": "Extract-Outfit"},
95
- ]
96
-
97
- def make_thumb_b64(path, max_dim=220):
98
- if not os.path.exists(path):
99
- return ""
100
- try:
101
- img = Image.open(path).convert("RGB")
102
- img.thumbnail((max_dim, max_dim), LANCZOS)
103
- buf = BytesIO()
104
- img.save(buf, format="JPEG", quality=65)
105
- return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
106
- except Exception as e:
107
- return ""
108
-
109
- def encode_full_image(path):
110
- if not os.path.exists(path):
111
- return ""
112
- try:
113
- with open(path, "rb") as f:
114
- data = f.read()
115
- ext = path.rsplit(".", 1)[-1].lower()
116
- mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp"}.get(ext, "image/jpeg")
117
- return f"data:{mime};base64,{base64.b64encode(data).decode()}"
118
- except Exception as e:
119
- return ""
120
-
121
- def build_client_config():
122
- examples = []
123
- for i, ex in enumerate(EXAMPLES_CONFIG):
124
- examples.append({
125
- "idx": i,
126
- "thumbs": [make_thumb_b64(p) for p in ex["images"]],
127
- "n_images": len(ex["images"]),
128
- "lora": ex["lora"],
129
- "prompt": ex["prompt"],
130
- })
131
- return {
132
- "loras": ADAPTER_NAMES,
133
- "default_lora": "Qwen-Image-Edit-2511-Object-Adder",
134
- "examples": examples,
135
- }
136
-
137
- print("Building client config (example thumbnails)…")
138
- CLIENT_CONFIG = build_client_config()
139
- print(f"Built config with {len(EXAMPLES_CONFIG)} examples and {len(ADAPTER_NAMES)} LoRAs.")
140
-
141
- def b64_to_pil_list(b64_json_str):
142
- if not b64_json_str or b64_json_str.strip() in ("", "[]"):
143
- return []
144
- try:
145
- b64_list = json.loads(b64_json_str)
146
- except Exception:
147
- return []
148
- pil_images = []
149
- for b64_str in b64_list:
150
- if not b64_str or not isinstance(b64_str, str):
151
- continue
152
- try:
153
- if b64_str.startswith("data:image"):
154
- _, data = b64_str.split(",", 1)
155
- else:
156
- data = b64_str
157
- image_data = base64.b64decode(data)
158
- pil_images.append(Image.open(BytesIO(image_data)).convert("RGB"))
159
- except Exception as e:
160
- print(f"Error decoding image: {e}")
161
- return pil_images
162
-
163
- def pil_to_b64_png(image: Image.Image) -> str:
164
- buf = BytesIO()
165
- image.save(buf, format="PNG")
166
- return f"data:image/png;base64,{base64.b64encode(buf.getvalue()).decode()}"
167
 
168
  def update_dimensions_on_upload(image):
169
  if image is None:
170
  return 1024, 1024
171
- w, h = image.size
172
- if w > h:
173
- nw = 1024
174
- nh = int(nw * h / w)
 
 
 
175
  else:
176
- nh = 1024
177
- nw = int(nh * w / h)
178
- return (nw // 8) * 8, (nh // 8) * 8
179
-
180
- # ── Gradio Server (Server mode): FastAPI + Gradio queue/API engine ────────────
181
- app = Server(title="Qwen-Image-Edit-Object-Manipulator")
182
-
183
- @app.mcp.tool(name="edit_image")
184
- @app.api(name="edit_image")
185
- @spaces.GPU(size="xlarge")
186
  def infer(
187
- images_b64_json: str,
188
- prompt: str,
189
- lora_adapter: str,
190
- seed: int,
191
- randomize_seed: bool,
192
- guidance_scale: float,
193
- steps: int,
194
- ) -> dict:
195
- """Edit one or more images with Qwen-Image-Edit + a lazily-loaded LoRA."""
196
  gc.collect()
197
  torch.cuda.empty_cache()
198
 
199
- pil_images = b64_to_pil_list(images_b64_json)
200
- if not pil_images:
201
  raise gr.Error("Please upload at least one image to edit.")
202
- if not prompt or prompt.strip() == "":
203
- raise gr.Error("Please enter an edit prompt.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
  spec = ADAPTER_SPECS.get(lora_adapter)
206
  if not spec:
207
  raise gr.Error(f"Configuration not found for: {lora_adapter}")
208
 
209
  adapter_name = spec["adapter_name"]
 
210
  if adapter_name not in LOADED_ADAPTERS:
211
  print(f"--- Downloading and Loading Adapter: {lora_adapter} ---")
212
  try:
213
- pipe.load_lora_weights(spec["repo"], weight_name=spec["weights"], adapter_name=adapter_name)
 
 
 
 
214
  LOADED_ADAPTERS.add(adapter_name)
215
  except Exception as e:
216
  raise gr.Error(f"Failed to load adapter {lora_adapter}: {e}")
217
  else:
218
- print(f"--- Adapter {lora_adapter} already loaded. ---")
219
 
220
  pipe.set_adapters([adapter_name], adapter_weights=[1.0])
221
 
@@ -223,10 +230,8 @@ def infer(
223
  seed = random.randint(0, MAX_SEED)
224
 
225
  generator = torch.Generator(device=device).manual_seed(seed)
226
- negative_prompt = (
227
- "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, "
228
- "extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
229
- )
230
  width, height = update_dimensions_on_upload(pil_images[0])
231
 
232
  try:
@@ -240,45 +245,111 @@ def infer(
240
  generator=generator,
241
  true_cfg_scale=guidance_scale,
242
  ).images[0]
243
- return {"image": pil_to_b64_png(result_image), "seed": seed}
 
 
244
  except Exception as e:
245
  raise e
246
  finally:
247
  gc.collect()
248
  torch.cuda.empty_cache()
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
- @app.api(name="load_example", queue=False)
252
- def load_example(idx: float) -> dict:
253
- """Return base64-encoded example images + prompt + LoRA for a given example index."""
254
- try:
255
- i = int(idx)
256
- except (ValueError, TypeError):
257
- i = -1
258
- if i < 0 or i >= len(EXAMPLES_CONFIG):
259
- return {"images": [], "prompt": "", "lora": "", "names": [], "status": "error"}
260
- ex = EXAMPLES_CONFIG[i]
261
- b64_list, names = [], []
262
- for path in ex["images"]:
263
- b64 = encode_full_image(path)
264
- if b64:
265
- b64_list.append(b64)
266
- names.append(os.path.basename(path))
267
- return {"images": b64_list, "prompt": ex["prompt"], "lora": ex["lora"], "names": names, "status": "ok"}
268
-
269
-
270
- @app.get("/api/config")
271
- def client_config():
272
- """Plain FastAPI route: LoRA choices + example card data for the frontend."""
273
- return CLIENT_CONFIG
274
-
275
-
276
- @app.get("/", response_class=HTMLResponse)
277
- async def homepage():
278
- html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
279
- with open(html_path, "r", encoding="utf-8") as f:
280
- return f.read()
281
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
  if __name__ == "__main__":
284
- app.launch(show_error=True, mcp_server=True)
 
1
  import os
2
  import gc
3
  import gradio as gr
 
 
4
  import numpy as np
5
  import spaces
6
  import torch
7
  import random
 
 
 
8
  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
+
83
+ print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
84
+ print("torch.__version__ =", torch.__version__)
85
+ print("Using device:", device)
86
 
87
  from diffusers import FlowMatchEulerDiscreteScheduler
88
  from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
89
  from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
90
  from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
91
 
 
 
 
 
92
  dtype = torch.bfloat16
93
 
 
 
 
 
 
94
  pipe = QwenImageEditPlusPipeline.from_pretrained(
95
  "Qwen/Qwen-Image-Edit-2509",
96
  transformer=QwenImageTransformer2DModel.from_pretrained(
97
  "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19",
98
+ #subfolder='transformer',
99
  torch_dtype=dtype,
100
+ device_map='cuda'
101
  ),
102
+ torch_dtype=dtype
103
  ).to(device)
104
 
105
  try:
 
108
  except Exception as e:
109
  print(f"Warning: Could not set FA3 processor: {e}")
110
 
111
+ MAX_SEED = np.iinfo(np.int32).max
112
+
113
  ADAPTER_SPECS = {
114
  "Qwen-Image-Edit-2511-Object-Adder": {
115
  "repo": "prithivMLmods/Qwen-Image-Edit-2511-Object-Adder",
 
143
  },
144
  }
145
 
146
+ LOADED_ADAPTERS = set()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  def update_dimensions_on_upload(image):
149
  if image is None:
150
  return 1024, 1024
151
+
152
+ original_width, original_height = image.size
153
+
154
+ if original_width > original_height:
155
+ new_width = 1024
156
+ aspect_ratio = original_height / original_width
157
+ new_height = int(new_width * aspect_ratio)
158
  else:
159
+ new_height = 1024
160
+ aspect_ratio = original_width / original_height
161
+ new_width = int(new_height * aspect_ratio)
162
+
163
+ new_width = (new_width // 8) * 8
164
+ new_height = (new_height // 8) * 8
165
+
166
+ return new_width, new_height
167
+
168
+ @spaces.GPU
169
  def infer(
170
+ images,
171
+ prompt,
172
+ lora_adapter,
173
+ seed,
174
+ randomize_seed,
175
+ guidance_scale,
176
+ steps,
177
+ progress=gr.Progress(track_tqdm=True)
178
+ ):
179
  gc.collect()
180
  torch.cuda.empty_cache()
181
 
182
+ if not images:
 
183
  raise gr.Error("Please upload at least one image to edit.")
184
+
185
+ pil_images = []
186
+ if images is not None:
187
+ for item in images:
188
+ try:
189
+ if isinstance(item, tuple) or isinstance(item, list):
190
+ path_or_img = item[0]
191
+ else:
192
+ path_or_img = item
193
+
194
+ if isinstance(path_or_img, str):
195
+ pil_images.append(Image.open(path_or_img).convert("RGB"))
196
+ elif isinstance(path_or_img, Image.Image):
197
+ pil_images.append(path_or_img.convert("RGB"))
198
+ else:
199
+ pil_images.append(Image.open(path_or_img.name).convert("RGB"))
200
+ except Exception as e:
201
+ print(f"Skipping invalid image item: {e}")
202
+ continue
203
+
204
+ if not pil_images:
205
+ raise gr.Error("Could not process uploaded images.")
206
 
207
  spec = ADAPTER_SPECS.get(lora_adapter)
208
  if not spec:
209
  raise gr.Error(f"Configuration not found for: {lora_adapter}")
210
 
211
  adapter_name = spec["adapter_name"]
212
+
213
  if adapter_name not in LOADED_ADAPTERS:
214
  print(f"--- Downloading and Loading Adapter: {lora_adapter} ---")
215
  try:
216
+ pipe.load_lora_weights(
217
+ spec["repo"],
218
+ weight_name=spec["weights"],
219
+ adapter_name=adapter_name
220
+ )
221
  LOADED_ADAPTERS.add(adapter_name)
222
  except Exception as e:
223
  raise gr.Error(f"Failed to load adapter {lora_adapter}: {e}")
224
  else:
225
+ print(f"--- Adapter {lora_adapter} is already loaded. ---")
226
 
227
  pipe.set_adapters([adapter_name], adapter_weights=[1.0])
228
 
 
230
  seed = random.randint(0, MAX_SEED)
231
 
232
  generator = torch.Generator(device=device).manual_seed(seed)
233
+ 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"
234
+
 
 
235
  width, height = update_dimensions_on_upload(pil_images[0])
236
 
237
  try:
 
245
  generator=generator,
246
  true_cfg_scale=guidance_scale,
247
  ).images[0]
248
+
249
+ return result_image, seed
250
+
251
  except Exception as e:
252
  raise e
253
  finally:
254
  gc.collect()
255
  torch.cuda.empty_cache()
256
 
257
+ @spaces.GPU
258
+ def infer_example(images, prompt, lora_adapter):
259
+ if not images:
260
+ return None, 0
261
+
262
+ if isinstance(images, str):
263
+ images_list = [images]
264
+ else:
265
+ images_list = images
266
+
267
+ result, seed = infer(
268
+ images=images_list,
269
+ prompt=prompt,
270
+ lora_adapter=lora_adapter,
271
+ seed=0,
272
+ randomize_seed=True,
273
+ guidance_scale=1.0,
274
+ steps=4
275
+ )
276
+ return result, seed
277
 
278
+ css="""
279
+ #col-container {
280
+ margin: 0 auto;
281
+ max-width: 1000px;
282
+ }
283
+ #main-title h1 {font-size: 2.3em !important;}
284
+ """
285
+
286
+ with gr.Blocks() as demo:
287
+ with gr.Column(elem_id="col-container"):
288
+ gr.Markdown("# **Qwen-Image-Edit-Object-Manipulator**", elem_id="main-title")
289
+ gr.Markdown("Perform diverse image edits using specialized [LoRA](https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2511) adapters. Upload one or more images.")
290
+
291
+ with gr.Row(equal_height=True):
292
+ with gr.Column():
293
+ images = gr.Gallery(
294
+ label="Upload Images",
295
+ type="filepath",
296
+ columns=2,
297
+ rows=1,
298
+ height=300,
299
+ allow_preview=True
300
+ )
301
+
302
+ prompt = gr.Text(
303
+ label="Edit Prompt",
304
+ show_label=True,
305
+ placeholder="e.g., transform into anime..",
306
+ )
307
+
308
+ run_button = gr.Button("Edit Image", variant="primary")
309
+
310
+ with gr.Column():
311
+ output_image = gr.Image(label="Output Image", interactive=False, format="png", height=363)
312
+
313
+ with gr.Row():
314
+ lora_adapter = gr.Dropdown(
315
+ label="Choose Manipulator",
316
+ choices=list(ADAPTER_SPECS.keys()),
317
+ value="Qwen-Image-Edit-2511-Object-Adder"
318
+ )
319
+
320
+ with gr.Accordion("Advanced Settings", open=False, visible=False):
321
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
322
+ randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
323
+ guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
324
+ steps = gr.Slider(label="Inference Steps", minimum=1, maximum=50, step=1, value=4)
325
+
326
+ gr.Examples(
327
+ examples=[
328
+ [["examples/D.jpg"], "Add the batman logo to the image while preserving the background lighting and surrounding elements maintaining realism and original details.", "Qwen-Image-Edit-2511-Object-Adder"],
329
+ [["examples/A.jpg"], "Add the slim rectangular transparent frame sunglasses to the image while preserving the background lighting and surrounding elements maintaining realism and original details.", "Qwen-Image-Edit-2511-Object-Adder"],
330
+ [["examples/B.jpeg"], "Remove the necklace and goggles from the image while preserving the background and remaining elements, maintaining realism and original details.", "Qwen-Image-Edit-2511-Object-Remover"],
331
+ [["examples/DL2.jpg"], "add the nike tick design inside the red marked area.", "Outfit-Design-Layout"],
332
+ [["examples/DL1.jpg"], "add the akatsuki cloud design inside the red marked area.", "Outfit-Design-Layout"],
333
+ [["examples/C.png"], "Add the leather cowboy cap to the image while preserving the background lighting and surrounding elements maintaining realism and original details.", "Qwen-Image-Edit-2511-Object-Adder"],
334
+ [["examples/ZM.jpg"], "Zoom into the red highlighted area.", "Zoom-Master"],
335
+ [["examples/OBJ1.jpg"], "Remove the red highlighted object from the scene.", "QIE-2511-Object-Remover-v2"],
336
+ [["examples/OBJ2.jpg"], "Remove the red highlighted object from the scene.", "QIE-2511-Object-Remover-v2"],
337
+ [["examples/OE.jpg"], "Extract the clothing and create a flat mockup.", "Extract-Outfit"],
338
+ ],
339
+ inputs=[images, prompt, lora_adapter],
340
+ outputs=[output_image, seed],
341
+ fn=infer_example,
342
+ cache_examples=False,
343
+ label="Examples"
344
+ )
345
+
346
+ gr.Markdown("[*](https://huggingface.co/spaces/prithivMLmods/Qwen-Image-Edit-2511-LoRAs-Fast)This is still an experimental Space for Qwen-Image-Edit-2511.")
347
+
348
+ run_button.click(
349
+ fn=infer,
350
+ inputs=[images, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps],
351
+ outputs=[output_image, seed]
352
+ )
353
 
354
  if __name__ == "__main__":
355
+ demo.queue(max_size=30).launch(css=css, theme=orange_red_theme, mcp_server=True, ssr_mode=False, show_error=True)
index.html DELETED
@@ -1,1383 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Qwen Studio · Object Manipulator</title>
7
- <style>
8
- @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');
9
- *{box-sizing:border-box;margin:0;padding:0}
10
- :root{
11
- --bg:#0a0a0c;--panel:#131316;--panel2:#0e0e11;--canvas:#08080a;
12
- --border:#2a1f24;--border2:#3f2a32;
13
- --text:#e4e4e7;--muted:#a1a1aa;--dim:#6b6b75;--faint:#4a4a52;
14
- --accent:#FF1493;--accent2:#FF40A3;--accent3:#FF80C5;
15
- --menubar-h:48px;--statusbar-h:30px;--rail-w:52px;--inspector-w:360px;--filmstrip-h:132px;
16
- }
17
- html,body{height:100%}
18
- body{
19
- background:var(--bg);font-family:'Inter',system-ui,-apple-system,sans-serif;
20
- font-size:13px;color:var(--text);overflow:hidden;
21
- }
22
-
23
- /* ══ Layout grid ══ */
24
- .studio{
25
- display:grid;height:100vh;
26
- grid-template-rows:var(--menubar-h) 1fr var(--statusbar-h);
27
- grid-template-columns:var(--rail-w) 1fr var(--inspector-w);
28
- grid-template-areas:
29
- "menubar menubar menubar"
30
- "rail workspace inspector"
31
- "statusbar statusbar statusbar";
32
- }
33
-
34
- /* ══ Menu bar ══ */
35
- .menubar{
36
- grid-area:menubar;display:flex;align-items:center;gap:14px;
37
- background:var(--panel);border-bottom:1px solid var(--border);
38
- padding:0 16px;z-index:50;
39
- }
40
- .menubar-logo{
41
- width:28px;height:28px;background:linear-gradient(135deg,#FF1493,#FF40A3,#FF80C5);
42
- border-radius:8px;display:flex;align-items:center;justify-content:center;
43
- box-shadow:0 2px 8px rgba(255,20,147,.35);flex-shrink:0;
44
- }
45
- .menubar-logo svg{width:15px;height:15px;fill:#fff}
46
- .menubar-title{font-size:14px;font-weight:700;letter-spacing:-.2px;white-space:nowrap}
47
- .menubar-title span{color:var(--dim);font-weight:500}
48
- .menubar-badge{
49
- font-size:10px;font-weight:700;padding:2px 8px;border-radius:12px;
50
- background:rgba(255,20,147,.14);color:var(--accent2);border:1px solid rgba(255,20,147,.25);
51
- letter-spacing:.4px;white-space:nowrap;
52
- }
53
- .menubar-badge.fast{background:rgba(34,197,94,.12);color:#4ade80;border-color:rgba(34,197,94,.25)}
54
- .menubar-spacer{flex:1}
55
- .menubar-status{
56
- display:flex;align-items:center;gap:7px;font-family:'JetBrains Mono',monospace;
57
- font-size:11px;color:var(--dim);
58
- }
59
- .menubar-status .dot{width:7px;height:7px;border-radius:50%;background:#4ade80;box-shadow:0 0 6px rgba(74,222,128,.6)}
60
- .menubar-status .dot.off{background:#f87171;box-shadow:0 0 6px rgba(248,113,113,.5)}
61
- .gh-btn{
62
- display:inline-flex;align-items:center;gap:6px;
63
- padding:5px 13px;border-radius:7px;text-decoration:none;
64
- font-size:12px;font-weight:600;background:var(--accent);color:#fff;
65
- border:1px solid rgba(255,255,255,.15);
66
- transition:all .15s ease;flex-shrink:0;
67
- }
68
- .gh-btn:hover{background:var(--accent2)}
69
- .gh-btn svg{fill:#fff;width:13px;height:13px}
70
-
71
- /* ══ Left icon rail ══ */
72
- .rail{
73
- grid-area:rail;background:var(--panel);border-right:1px solid var(--border);
74
- display:flex;flex-direction:column;align-items:center;padding:10px 0;gap:4px;
75
- overflow-y:auto;z-index:40;
76
- }
77
- .rail-btn{
78
- width:38px;height:38px;border-radius:9px;border:1px solid transparent;
79
- background:transparent;color:var(--dim);cursor:pointer;
80
- display:flex;align-items:center;justify-content:center;
81
- transition:all .15s ease;position:relative;
82
- }
83
- .rail-btn:hover:not(:disabled){background:rgba(255,20,147,.12);color:var(--accent3);border-color:rgba(255,20,147,.25)}
84
- .rail-btn.active{background:rgba(255,20,147,.2);color:var(--accent2);border-color:rgba(255,20,147,.4)}
85
- .rail-btn:disabled{opacity:.3;cursor:not-allowed}
86
- .rail-btn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
87
- .rail-btn svg.fill{fill:currentColor;stroke:none}
88
- .rail-sep{width:24px;height:1px;background:var(--border);margin:6px 0}
89
- .rail-label{
90
- font-size:8px;font-weight:700;color:var(--faint);letter-spacing:1px;
91
- text-transform:uppercase;margin:8px 0 2px;user-select:none;
92
- }
93
-
94
- /* ══ Workspace ══ */
95
- .workspace{
96
- grid-area:workspace;display:flex;flex-direction:column;min-width:0;min-height:0;
97
- background:var(--canvas);
98
- }
99
- .canvas-head{
100
- display:flex;align-items:center;gap:10px;padding:8px 16px;
101
- border-bottom:1px solid var(--border);background:var(--panel2);flex-shrink:0;
102
- }
103
- .seg-control{
104
- display:inline-flex;background:var(--panel);border:1px solid var(--border);
105
- border-radius:8px;padding:2px;gap:2px;
106
- }
107
- .seg-btn{
108
- padding:4px 14px;font-size:11px;font-weight:600;color:var(--dim);
109
- background:transparent;border:none;border-radius:6px;cursor:pointer;
110
- font-family:'Inter',sans-serif;transition:all .15s;letter-spacing:.2px;
111
- }
112
- .seg-btn:hover:not(.active){color:var(--muted)}
113
- .seg-btn.active{background:rgba(255,20,147,.18);color:var(--accent2)}
114
- .canvas-meta{
115
- font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--faint);
116
- overflow:hidden;white-space:nowrap;text-overflow:ellipsis;flex:1;
117
- }
118
- .canvas-actions{display:flex;align-items:center;gap:6px}
119
- .out-seed-chip{
120
- display:none;font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:500;
121
- color:var(--dim);background:var(--panel);border:1px solid var(--border);
122
- border-radius:6px;padding:4px 9px;cursor:pointer;transition:all .15s;
123
- }
124
- .out-seed-chip.visible{display:inline-flex}
125
- .out-seed-chip:hover{color:var(--accent3);border-color:rgba(255,20,147,.35)}
126
-
127
- .canvas-stage{
128
- flex:1;position:relative;overflow:hidden;min-height:0;
129
- display:flex;align-items:center;justify-content:center;
130
- background:
131
- repeating-conic-gradient(#0c0c0f 0% 25%, #08080a 0% 50%) 0 0/24px 24px;
132
- }
133
- .canvas-stage img.modern-out-img{
134
- max-width:calc(100% - 48px);max-height:calc(100% - 48px);
135
- box-shadow:0 12px 48px rgba(0,0,0,.7),0 0 0 1px var(--border);
136
- border-radius:4px;cursor:zoom-in;image-rendering:auto;
137
- animation:canvasIn .3s ease;
138
- }
139
- @keyframes canvasIn{from{opacity:0;transform:scale(.985)}to{opacity:1;transform:scale(1)}}
140
- .canvas-placeholder{
141
- text-align:center;color:var(--faint);user-select:none;
142
- display:flex;flex-direction:column;align-items:center;gap:14px;padding:24px;
143
- }
144
- .canvas-placeholder svg{width:72px;height:72px;opacity:.5}
145
- .canvas-placeholder .cp-title{font-size:15px;font-weight:600;color:var(--dim)}
146
- .canvas-placeholder .cp-sub{font-size:12px;line-height:1.7;max-width:340px}
147
- .canvas-placeholder kbd{
148
- padding:1px 6px;background:var(--panel);border:1px solid var(--border2);
149
- border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--muted);
150
- }
151
-
152
- /* Compare slider */
153
- .compare-wrap{
154
- position:relative;max-width:calc(100% - 48px);max-height:calc(100% - 48px);overflow:hidden;
155
- display:none;user-select:none;touch-action:none;cursor:ew-resize;
156
- box-shadow:0 12px 48px rgba(0,0,0,.7),0 0 0 1px var(--border);border-radius:4px;
157
- }
158
- .compare-wrap.visible{display:block}
159
- .compare-wrap img{display:block;max-width:100%;max-height:100%;pointer-events:none}
160
- .compare-top{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden}
161
- .compare-top img{position:absolute;top:0;left:0;height:100%;width:auto;max-width:none!important;max-height:none!important}
162
- .compare-divider{position:absolute;top:0;bottom:0;width:2px;background:#fff;box-shadow:0 0 8px rgba(0,0,0,.6);pointer-events:none}
163
- .compare-handle{
164
- position:absolute;top:50%;transform:translate(-50%,-50%);
165
- width:32px;height:32px;border-radius:50%;background:#fff;
166
- display:flex;align-items:center;justify-content:center;
167
- box-shadow:0 2px 8px rgba(0,0,0,.5);color:#111;font-size:13px;font-weight:800;
168
- }
169
- .compare-label{
170
- position:absolute;top:8px;padding:2px 8px;border-radius:4px;
171
- background:rgba(0,0,0,.65);color:#fff;font-size:10px;font-weight:600;
172
- font-family:'JetBrains Mono',monospace;pointer-events:none;
173
- }
174
- .compare-label.before{left:8px}
175
- .compare-label.after{right:8px}
176
-
177
- /* Loader */
178
- .modern-loader{
179
- display:none;position:absolute;inset:0;background:rgba(8,8,10,.9);
180
- z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(5px);
181
- }
182
- .modern-loader.active{display:flex}
183
- .loader-spinner{
184
- width:38px;height:38px;border:3px solid var(--border);border-top-color:var(--accent);
185
- border-radius:50%;animation:spin .8s linear infinite;
186
- }
187
- @keyframes spin{to{transform:rotate(360deg)}}
188
- .loader-text{font-size:13px;color:var(--muted);font-weight:500;text-align:center;padding:0 16px}
189
- .loader-bar-track{width:220px;height:4px;background:var(--border);border-radius:2px;overflow:hidden}
190
- .loader-bar-fill{
191
- height:100%;width:100%;background:linear-gradient(90deg,#FF1493,#FF40A3,#FF1493);
192
- background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px;
193
- transition:width .3s ease;
194
- }
195
- .loader-bar-fill.determinate{animation:none;background:var(--accent)}
196
- @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
197
-
198
- /* ══ Filmstrip ══ */
199
- .filmstrip{
200
- height:var(--filmstrip-h);flex-shrink:0;background:var(--panel);
201
- border-top:1px solid var(--border);display:flex;min-height:0;overflow:hidden;
202
- }
203
- .filmstrip-section{
204
- display:flex;flex-direction:column;min-width:0;padding:8px 0 8px 14px;
205
- }
206
- .filmstrip-section.inputs{flex:1.2;border-right:1px solid var(--border)}
207
- .filmstrip-section.history{flex:1}
208
- .filmstrip-label{
209
- font-size:9px;font-weight:700;color:var(--faint);letter-spacing:1.2px;
210
- text-transform:uppercase;margin-bottom:6px;display:flex;align-items:center;gap:8px;
211
- user-select:none;flex-shrink:0;
212
- }
213
- .filmstrip-label .count{
214
- font-family:'JetBrains Mono',monospace;font-weight:500;color:var(--dim);
215
- background:var(--panel2);border:1px solid var(--border);border-radius:4px;padding:0 5px;font-size:9px;
216
- }
217
- .filmstrip-row{
218
- display:flex;gap:8px;overflow-x:auto;overflow-y:hidden;flex:1;
219
- align-items:flex-start;padding-bottom:4px;padding-right:14px;
220
- }
221
- .filmstrip-row::-webkit-scrollbar{height:5px}
222
- .filmstrip-row::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
223
- .fs-thumb{
224
- position:relative;flex-shrink:0;width:76px;height:76px;border-radius:8px;overflow:hidden;
225
- border:2px solid var(--border);cursor:pointer;transition:all .15s;background:var(--panel2);
226
- animation:thumbIn .25s ease;
227
- }
228
- @keyframes thumbIn{from{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}
229
- .fs-thumb:hover{border-color:var(--border2);transform:translateY(-2px)}
230
- .fs-thumb.selected{border-color:var(--accent);box-shadow:0 0 0 2px rgba(255,20,147,.25)}
231
- .fs-thumb img{width:100%;height:100%;object-fit:cover}
232
- .fs-badge{
233
- position:absolute;bottom:4px;left:4px;background:rgba(0,0,0,.75);color:#fff;
234
- padding:1px 6px;border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:9px;font-weight:600;
235
- }
236
- .fs-remove{
237
- position:absolute;top:4px;right:4px;width:20px;height:20px;background:rgba(0,0,0,.8);
238
- color:#fff;border:1px solid rgba(255,255,255,.2);border-radius:50%;cursor:pointer;
239
- display:none;align-items:center;justify-content:center;font-size:10px;transition:all .15s;line-height:1;
240
- }
241
- .fs-thumb:hover .fs-remove{display:flex}
242
- .fs-remove:hover{background:#ef4444;border-color:#ef4444}
243
- .fs-add{
244
- flex-shrink:0;width:76px;height:76px;border-radius:8px;border:2px dashed var(--border2);
245
- display:flex;flex-direction:column;align-items:center;justify-content:center;
246
- cursor:pointer;transition:all .2s;background:rgba(255,20,147,.03);gap:3px;
247
- }
248
- .fs-add:hover{border-color:var(--accent);background:rgba(255,20,147,.08)}
249
- .fs-add .add-icon{font-size:22px;color:var(--dim);font-weight:300}
250
- .fs-add .add-text{font-size:9px;color:var(--dim);font-weight:600;text-transform:uppercase;letter-spacing:.5px}
251
- .fs-empty{
252
- flex-shrink:0;display:flex;align-items:center;height:76px;
253
- color:var(--faint);font-size:11px;font-style:italic;padding-right:12px;
254
- }
255
-
256
- /* ══ Inspector ══ */
257
- .inspector{
258
- grid-area:inspector;background:var(--panel);border-left:1px solid var(--border);
259
- display:flex;flex-direction:column;min-height:0;z-index:40;
260
- }
261
- .inspector-tabs{
262
- display:flex;border-bottom:1px solid var(--border);flex-shrink:0;
263
- }
264
- .insp-tab{
265
- flex:1;padding:11px 8px;font-size:11px;font-weight:700;color:var(--dim);
266
- background:transparent;border:none;border-bottom:2px solid transparent;
267
- cursor:pointer;font-family:'Inter',sans-serif;text-transform:uppercase;letter-spacing:.8px;
268
- transition:all .15s;
269
- }
270
- .insp-tab:hover:not(.active){color:var(--muted)}
271
- .insp-tab.active{color:var(--accent2);border-bottom-color:var(--accent)}
272
- .insp-page{display:none;flex:1;overflow-y:auto;min-height:0;flex-direction:column}
273
- .insp-page.active{display:flex}
274
-
275
- .insp-section{border-bottom:1px solid var(--border);padding:14px 18px}
276
- .insp-section-title{
277
- font-size:10px;font-weight:700;color:var(--dim);text-transform:uppercase;
278
- letter-spacing:1px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between;
279
- }
280
- .char-count{font-size:9px;font-weight:500;color:var(--faint);font-family:'JetBrains Mono',monospace;text-transform:none;letter-spacing:0}
281
- .modern-textarea{
282
- width:100%;background:var(--panel2);border:1px solid var(--border);border-radius:8px;
283
- padding:10px 13px;font-family:'Inter',sans-serif;font-size:13px;color:var(--text);
284
- resize:vertical;outline:none;min-height:64px;transition:border-color .2s;line-height:1.55;
285
- }
286
- .modern-textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,20,147,.15)}
287
- .modern-textarea::placeholder{color:var(--faint)}
288
- .modern-textarea.error-flash{
289
- border-color:#ef4444!important;box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;animation:shake .4s ease;
290
- }
291
- @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}}
292
-
293
- .suggestions-wrap{display:flex;flex-wrap:wrap;gap:5px}
294
- .suggestion-chip{
295
- display:inline-flex;align-items:center;padding:4px 11px;
296
- background:rgba(255,20,147,.07);border:1px solid rgba(255,20,147,.18);border-radius:14px;
297
- color:var(--accent3);font-size:11px;font-weight:500;font-family:'Inter',sans-serif;
298
- cursor:pointer;transition:all .15s;white-space:nowrap;
299
- }
300
- .suggestion-chip:hover{background:rgba(255,20,147,.15);border-color:rgba(255,20,147,.35);color:var(--accent2)}
301
- .suggestion-chip.active{background:rgba(255,20,147,.25);border-color:var(--accent);color:#fff}
302
-
303
- .lora-native-select{
304
- width:100%;background:var(--panel2);
305
- border:1px solid var(--border2);border-radius:8px;
306
- padding:9px 34px 9px 13px;
307
- font-family:'Inter',sans-serif;font-size:12px;font-weight:600;color:var(--text);
308
- outline:none;appearance:none;-webkit-appearance:none;
309
- background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23FF1493' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
310
- background-repeat:no-repeat;background-position:right 11px center;
311
- cursor:pointer;transition:border-color .2s;color-scheme:dark;
312
- }
313
- .lora-native-select:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(255,20,147,.15)}
314
- .lora-native-select option{background:var(--panel);color:var(--text)}
315
-
316
- .btn-run{
317
- display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
318
- background:linear-gradient(135deg,#FF1493,#C01079);border:none;border-radius:9px;
319
- padding:12px 24px;cursor:pointer;font-size:14px;font-weight:700;font-family:'Inter',sans-serif;
320
- color:#fff;transition:all .2s ease;letter-spacing:-.2px;
321
- box-shadow:0 4px 16px rgba(255,20,147,.3),inset 0 1px 0 rgba(255,255,255,.1);
322
- }
323
- .btn-run:hover:not(:disabled){
324
- background:linear-gradient(135deg,#FF40A3,#FF1493);transform:translateY(-1px);
325
- box-shadow:0 6px 24px rgba(255,20,147,.45),inset 0 1px 0 rgba(255,255,255,.15);
326
- }
327
- .btn-run:active:not(:disabled){transform:translateY(0)}
328
- .btn-run:disabled{opacity:.55;cursor:not-allowed}
329
- .btn-cancel{
330
- display:flex;align-items:center;justify-content:center;gap:6px;width:100%;
331
- margin-top:8px;background:transparent;border:1px solid var(--border2);border-radius:9px;
332
- padding:8px 24px;cursor:pointer;font-size:12px;font-weight:600;font-family:'Inter',sans-serif;
333
- color:var(--muted);transition:all .15s;
334
- }
335
- .btn-cancel:hover{border-color:#ef4444;color:#ef4444;background:rgba(239,68,68,.08)}
336
- .run-hint{
337
- text-align:center;font-size:10px;color:var(--faint);margin-top:8px;
338
- font-family:'JetBrains Mono',monospace;
339
- }
340
-
341
- .slider-row{display:flex;align-items:center;gap:9px;min-height:26px;margin-bottom:10px}
342
- .slider-row:last-child{margin-bottom:0}
343
- .slider-row label{font-size:12px;font-weight:500;color:var(--muted);min-width:64px;flex-shrink:0}
344
- .slider-row input[type="range"]{
345
- flex:1;-webkit-appearance:none;appearance:none;height:5px;background:var(--border);
346
- border-radius:3px;outline:none;min-width:0;
347
- }
348
- .slider-row input[type="range"]::-webkit-slider-thumb{
349
- -webkit-appearance:none;width:14px;height:14px;background:linear-gradient(135deg,#FF1493,#C01079);
350
- border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(255,20,147,.4);transition:transform .15s;
351
- }
352
- .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)}
353
- .slider-row input[type="range"]::-moz-range-thumb{
354
- width:14px;height:14px;background:linear-gradient(135deg,#FF1493,#C01079);
355
- border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(255,20,147,.4);
356
- }
357
- .slider-val{
358
- min-width:48px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:11px;
359
- font-weight:500;padding:2px 7px;background:var(--panel2);border:1px solid var(--border);
360
- border-radius:5px;color:var(--muted);flex-shrink:0;
361
- }
362
- .dice-btn{
363
- display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;
364
- background:var(--panel2);border:1px solid var(--border);border-radius:5px;cursor:pointer;
365
- color:var(--dim);transition:all .15s;flex-shrink:0;padding:0;
366
- }
367
- .dice-btn:hover{border-color:var(--accent);color:var(--accent3)}
368
- .dice-btn svg{width:13px;height:13px;fill:currentColor}
369
- .checkbox-row{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--muted);margin-bottom:10px}
370
- .checkbox-row input[type="checkbox"]{accent-color:var(--accent);width:15px;height:15px;cursor:pointer}
371
- .checkbox-row label{color:var(--muted);font-size:12px;cursor:pointer}
372
-
373
- /* Examples page */
374
- .examples-list{padding:12px 14px;display:flex;flex-direction:column;gap:10px}
375
- .example-card{
376
- background:var(--panel2);border:1px solid var(--border);
377
- border-radius:10px;overflow:hidden;cursor:pointer;transition:all .2s ease;flex-shrink:0;
378
- display:flex;align-items:stretch;
379
- }
380
- .example-card:hover{border-color:var(--accent);box-shadow:0 4px 14px rgba(255,20,147,.15)}
381
- .example-card.loading{opacity:.5;pointer-events:none}
382
- .example-thumb-wrap{width:96px;flex-shrink:0;background:var(--panel);overflow:hidden;display:flex}
383
- .example-thumb-wrap img{width:100%;height:100%;object-fit:cover}
384
- .example-thumb-placeholder{
385
- width:100%;height:100%;display:flex;align-items:center;justify-content:center;
386
- background:var(--panel);color:var(--border2);font-size:10px;
387
- }
388
- .example-meta{padding:6px 10px 2px;display:flex;align-items:center;gap:5px;flex-wrap:wrap}
389
- .example-lora-badge{
390
- display:inline-flex;padding:2px 7px;background:rgba(255,20,147,.1);border-radius:4px;
391
- font-size:9px;font-weight:600;color:var(--accent2);font-family:'JetBrains Mono',monospace;
392
- white-space:nowrap;border:1px solid rgba(255,20,147,.25);
393
- }
394
- .example-body{flex:1;display:flex;flex-direction:column;justify-content:center;min-width:0}
395
- .example-prompt-text{
396
- padding:4px 10px 9px;font-size:11px;color:var(--muted);line-height:1.45;
397
- display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
398
- }
399
-
400
- /* ══ Status bar ══ */
401
- .statusbar{
402
- grid-area:statusbar;background:var(--panel);border-top:1px solid var(--border);
403
- display:flex;align-items:center;gap:4px;padding:0 14px;font-size:11px;z-index:50;
404
- }
405
- .statusbar .sb-section{
406
- padding:0 10px;display:flex;align-items:center;font-family:'JetBrains Mono',monospace;
407
- font-size:11px;color:var(--faint);overflow:hidden;white-space:nowrap;gap:6px;
408
- }
409
- .statusbar .sb-right{margin-left:auto;display:flex;align-items:center;gap:10px}
410
- .sb-pill{
411
- padding:2px 12px;border-radius:5px;font-weight:600;font-size:10px;
412
- background:rgba(255,20,147,.1);color:var(--accent2);
413
- }
414
- .sb-pill.error{background:rgba(239,68,68,.12);color:#f87171}
415
- .sb-pill.done{background:rgba(34,197,94,.12);color:#4ade80}
416
- .sb-link{color:var(--faint);text-decoration:none;font-family:'JetBrains Mono',monospace;font-size:10px}
417
- .sb-link:hover{color:var(--accent3)}
418
-
419
- /* ══ Toast ══ */
420
- .toast-notification{
421
- position:fixed;top:60px;left:50%;transform:translateX(-50%) translateY(-140%);
422
- z-index:9999;padding:10px 22px;border-radius:9px;font-family:'Inter',sans-serif;
423
- font-size:13px;font-weight:600;display:flex;align-items:center;gap:8px;
424
- box-shadow:0 8px 24px rgba(0,0,0,.5);
425
- transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none;
426
- }
427
- .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}
428
- .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)}
429
- .toast-notification.warning{background:linear-gradient(135deg,#d97706,#b45309);color:#fff;border:1px solid rgba(255,255,255,.15)}
430
- .toast-notification.info{background:linear-gradient(135deg,#FF1493,#C01079);color:#fff;border:1px solid rgba(255,255,255,.15)}
431
- .toast-notification .toast-icon{font-size:15px;line-height:1}
432
-
433
- /* ══ Lightbox ══ */
434
- .lightbox{
435
- position:fixed;inset:0;background:rgba(0,0,0,.92);z-index:10000;
436
- display:none;align-items:center;justify-content:center;cursor:zoom-out;
437
- backdrop-filter:blur(6px);animation:fadeIn .2s ease;
438
- }
439
- .lightbox.visible{display:flex}
440
- .lightbox img{max-width:94vw;max-height:94vh;border-radius:6px;box-shadow:0 20px 60px rgba(0,0,0,.8)}
441
- @keyframes fadeIn{from{opacity:0}to{opacity:1}}
442
-
443
- /* ══ Drop overlay ══ */
444
- .drop-overlay{
445
- position:fixed;inset:0;z-index:9000;background:rgba(8,8,10,.85);
446
- display:none;align-items:center;justify-content:center;pointer-events:none;
447
- backdrop-filter:blur(3px);
448
- }
449
- .drop-overlay.visible{display:flex}
450
- .drop-overlay-inner{
451
- border:3px dashed var(--accent);border-radius:22px;padding:56px 76px;
452
- font-size:19px;font-weight:700;color:var(--accent2);background:rgba(255,20,147,.06);
453
- }
454
-
455
- /* ══ Scrollbars ══ */
456
- ::-webkit-scrollbar{width:8px;height:8px}
457
- ::-webkit-scrollbar-track{background:transparent}
458
- ::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
459
- ::-webkit-scrollbar-thumb:hover{background:var(--border2)}
460
-
461
- /* ══ Touch devices ══ */
462
- @media (pointer:coarse){
463
- .fs-remove{display:flex}
464
- .rail-btn{width:44px;height:44px}
465
- .rail-btn svg{width:19px;height:19px}
466
- }
467
-
468
- /* ══ Responsive ══ */
469
- @media(max-width:960px){
470
- html,body{height:auto}
471
- body{overflow:auto!important}
472
- .studio{
473
- display:flex;flex-direction:column;height:auto;min-height:100vh;
474
- }
475
- .menubar{
476
- flex-wrap:wrap;gap:8px;padding:10px 12px;min-height:var(--menubar-h);
477
- position:sticky;top:0;
478
- }
479
- .menubar-title{font-size:13px}
480
- .menubar-status{display:none}
481
- .menubar-spacer{flex:1}
482
- .gh-btn{padding:6px 10px}
483
- .gh-btn span{display:none}
484
- .rail{
485
- flex-direction:row;justify-content:flex-start;flex-wrap:nowrap;
486
- overflow-x:auto;overflow-y:hidden;border-right:none;
487
- border-bottom:1px solid var(--border);padding:6px 10px;gap:6px;
488
- }
489
- .rail-sep{width:1px;height:26px;margin:0 4px;flex-shrink:0}
490
- .rail-label{display:none}
491
- .rail-btn{flex-shrink:0}
492
- .workspace{min-height:0}
493
- .canvas-head{flex-wrap:wrap;gap:8px;padding:8px 12px}
494
- .canvas-meta{flex-basis:100%;order:3;font-size:10px}
495
- .canvas-stage{min-height:42vh}
496
- .canvas-stage img.modern-out-img{max-width:calc(100% - 20px);max-height:52vh}
497
- .compare-wrap{max-width:calc(100% - 20px);max-height:52vh}
498
- .canvas-placeholder .cp-sub{font-size:11px;max-width:280px}
499
- .filmstrip{flex-direction:column;height:auto;max-height:none}
500
- .filmstrip-section.inputs{border-right:none;border-bottom:1px solid var(--border)}
501
- .filmstrip-section{padding:8px 0 8px 12px}
502
- .fs-thumb{width:68px;height:68px}
503
- .fs-add{width:68px;height:68px}
504
- .fs-empty{height:68px}
505
- .inspector{border-left:none;border-top:1px solid var(--border)}
506
- .inspector-tabs{position:sticky;top:0;background:var(--panel);z-index:30}
507
- .insp-tab{padding:13px 8px;font-size:12px}
508
- .insp-page{overflow-y:visible}
509
- .insp-section{padding:14px 16px}
510
- .modern-textarea{font-size:16px}
511
- .btn-run{padding:14px 24px;font-size:15px}
512
- .suggestion-chip{padding:6px 13px;font-size:12px}
513
- .statusbar{flex-wrap:wrap;height:auto;min-height:var(--statusbar-h);padding:4px 10px;gap:2px}
514
- .sb-link{display:none}
515
- .toast-notification{max-width:calc(100vw - 32px);font-size:12px;padding:10px 16px}
516
- .drop-overlay-inner{padding:32px 28px;font-size:15px;border-radius:16px;margin:0 16px}
517
- .example-card{flex-direction:column}
518
- .example-thumb-wrap{width:100%;height:96px}
519
- }
520
-
521
- @media(max-width:400px){
522
- .menubar-badge.fast{display:none}
523
- .seg-btn{padding:4px 12px}
524
- .canvas-stage{min-height:36vh}
525
- }
526
- </style>
527
- </head>
528
- <body>
529
- <div class="studio">
530
-
531
- <!-- ══ Menu bar ══ -->
532
- <div class="menubar">
533
- <div class="menubar-logo">
534
- <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>
535
- </div>
536
- <div class="menubar-title">Qwen Studio <span>/ Object Manipulator</span></div>
537
- <span class="menubar-badge">2511</span>
538
- <span class="menubar-badge fast">4-STEP FAST</span>
539
- <div class="menubar-spacer"></div>
540
- <div class="menubar-status"><span class="dot" id="conn-dot"></span><span id="conn-text">connecting…</span></div>
541
- <a href="https://github.com/PRITHIVSAKTHIUR/Qwen-Image-Edit-Object-Manipulator"
542
- target="_blank" rel="noopener" class="gh-btn">
543
- <svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
544
- <span>GitHub</span>
545
- </a>
546
- </div>
547
-
548
- <!-- ══ Left icon rail ══ -->
549
- <div class="rail">
550
- <div class="rail-label">Input</div>
551
- <button class="rail-btn" id="tb-upload" title="Upload images">
552
- <svg viewBox="0 0 24 24"><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>
553
- </button>
554
- <button class="rail-btn" id="tb-remove" title="Remove selected image" disabled>
555
- <svg viewBox="0 0 24 24"><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>
556
- </button>
557
- <button class="rail-btn" id="tb-clear" title="Clear all images" disabled>
558
- <svg viewBox="0 0 24 24"><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>
559
- </button>
560
- <div class="rail-sep"></div>
561
- <div class="rail-label">Result</div>
562
- <button class="rail-btn" id="compare-btn" title="Compare before / after" disabled>
563
- <svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/></svg>
564
- </button>
565
- <button class="rail-btn" id="use-as-input-btn" title="Use result as input (chain edits)" disabled>
566
- <svg viewBox="0 0 24 24"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 014-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 01-4 4H3"/></svg>
567
- </button>
568
- <button class="rail-btn" id="dl-btn-output" title="Download result" disabled>
569
- <svg viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
570
- </button>
571
- <button class="rail-btn" id="zoom-btn" title="Zoom result" disabled>
572
- <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/><line x1="11" y1="8" x2="11" y2="14"/><line x1="8" y1="11" x2="14" y2="11"/></svg>
573
- </button>
574
- </div>
575
-
576
- <!-- ══ Workspace ══ -->
577
- <div class="workspace">
578
- <div class="canvas-head">
579
- <div class="seg-control">
580
- <button class="seg-btn active" id="seg-result">Result</button>
581
- <button class="seg-btn" id="seg-input">Input</button>
582
- </div>
583
- <div class="canvas-meta" id="canvas-meta">No image loaded</div>
584
- <div class="canvas-actions">
585
- <span id="out-seed-chip" class="out-seed-chip" title="Click to copy seed"></span>
586
- </div>
587
- </div>
588
-
589
- <div class="canvas-stage" id="output-image-container">
590
- <div class="modern-loader" id="output-loader">
591
- <div class="loader-spinner"></div>
592
- <div class="loader-text" id="loader-text">Processing image&hellip;</div>
593
- <div class="loader-bar-track"><div class="loader-bar-fill" id="loader-bar-fill"></div></div>
594
- </div>
595
-
596
- <div class="canvas-placeholder" id="output-placeholder">
597
- <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
598
- <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#3f2a32" stroke-width="2" stroke-dasharray="4 3"/>
599
- <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(255,20,147,0.1)" stroke="#3f2a32" stroke-width="1.5"/>
600
- <circle cx="28" cy="30" r="6" fill="rgba(255,20,147,0.12)" stroke="#3f2a32" stroke-width="1.5"/>
601
- </svg>
602
- <div class="cp-title">Canvas is empty</div>
603
- <div class="cp-sub">
604
- Drop images anywhere, paste with <kbd>⌘/Ctrl+V</kbd>, or use the rail to upload.
605
- Then write an instruction and press <kbd>⌘/Ctrl</kbd>+<kbd>↵</kbd>.
606
- </div>
607
- </div>
608
-
609
- <div class="compare-wrap" id="compare-wrap">
610
- <img id="compare-after-img" alt="after">
611
- <div class="compare-top" id="compare-top"><img id="compare-before-img" alt="before"></div>
612
- <div class="compare-divider" id="compare-divider"><div class="compare-handle">⇄</div></div>
613
- <span class="compare-label before">Before</span>
614
- <span class="compare-label after">After</span>
615
- </div>
616
- </div>
617
-
618
- <!-- Filmstrip -->
619
- <div class="filmstrip">
620
- <div class="filmstrip-section inputs">
621
- <div class="filmstrip-label">Inputs <span class="count" id="tb-image-count">0</span></div>
622
- <div class="filmstrip-row" id="image-gallery-grid"></div>
623
- </div>
624
- <div class="filmstrip-section history">
625
- <div class="filmstrip-label">History <span class="count" id="history-count">0</span></div>
626
- <div class="filmstrip-row" id="history-strip"></div>
627
- </div>
628
- </div>
629
- </div>
630
-
631
- <!-- ══ Inspector ══ -->
632
- <div class="inspector">
633
- <div class="inspector-tabs">
634
- <button class="insp-tab active" id="tab-edit">Edit</button>
635
- <button class="insp-tab" id="tab-examples">Examples</button>
636
- </div>
637
-
638
- <!-- Edit page -->
639
- <div class="insp-page active" id="page-edit">
640
- <div class="insp-section">
641
- <div class="insp-section-title"><span>Instruction</span><span class="char-count" id="char-count">0</span></div>
642
- <textarea id="custom-prompt-input" class="modern-textarea" rows="3"
643
- placeholder="e.g., Add the batman logo, Remove the necklace, Zoom in..."></textarea>
644
- </div>
645
-
646
- <div class="insp-section">
647
- <div class="insp-section-title"><span>Quick Prompts</span></div>
648
- <div class="suggestions-wrap" id="suggestions-wrap"></div>
649
- </div>
650
-
651
- <div class="insp-section">
652
- <div class="insp-section-title"><span>Style / LoRA</span></div>
653
- <select id="custom-lora-select" class="lora-native-select"></select>
654
- </div>
655
-
656
- <div class="insp-section">
657
- <button id="custom-run-btn" class="btn-run">
658
- <span id="run-btn-label">Edit Image</span>
659
- </button>
660
- <button id="custom-cancel-btn" class="btn-cancel" style="display:none;">Cancel</button>
661
- <div class="run-hint">⌘/Ctrl + Enter</div>
662
- </div>
663
-
664
- <div class="insp-section">
665
- <div class="insp-section-title"><span>Advanced</span></div>
666
- <div class="slider-row">
667
- <label>Seed</label>
668
- <input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0">
669
- <span class="slider-val" id="custom-seed-val">0</span>
670
- <button class="dice-btn" id="dice-btn" title="Random seed">
671
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM7.5 18c-.83 0-1.5-.67-1.5-1.5S6.67 15 7.5 15s1.5.67 1.5 1.5S8.33 18 7.5 18zm0-9C6.67 9 6 8.33 6 7.5S6.67 6 7.5 6 9 6.67 9 7.5 8.33 9 7.5 9zm4.5 4.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm4.5 4.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5zm0-9c-.83 0-1.5-.67-1.5-1.5S15.67 6 16.5 6s1.5.67 1.5 1.5S17.33 9 16.5 9z"/></svg>
672
- </button>
673
- </div>
674
- <div class="checkbox-row">
675
- <input type="checkbox" id="custom-randomize" checked>
676
- <label for="custom-randomize">Randomize seed</label>
677
- </div>
678
- <div class="slider-row">
679
- <label>Guidance</label>
680
- <input type="range" id="custom-guidance" min="1" max="10" step="0.1" value="1.0">
681
- <span class="slider-val" id="custom-guidance-val">1.0</span>
682
- </div>
683
- <div class="slider-row">
684
- <label>Steps</label>
685
- <input type="range" id="custom-steps" min="1" max="50" step="1" value="4">
686
- <span class="slider-val" id="custom-steps-val">4</span>
687
- </div>
688
- </div>
689
- </div>
690
-
691
- <!-- Examples page -->
692
- <div class="insp-page" id="page-examples">
693
- <div class="examples-list" id="examples-scroll"></div>
694
- </div>
695
- </div>
696
-
697
- <!-- ══ Status bar ══ -->
698
- <div class="statusbar">
699
- <div class="sb-section" id="sb-image-count">No inputs</div>
700
- <div class="sb-right">
701
- <a class="sb-link" href="https://huggingface.co/Qwen/Qwen-Image-Edit-2509" target="_blank" rel="noopener">Qwen-Image-Edit-2509</a>
702
- <span class="sb-pill" id="sb-status">Ready</span>
703
- </div>
704
- </div>
705
-
706
- </div>
707
-
708
- <input id="custom-file-input" type="file" accept="image/*" multiple style="display:none;" />
709
-
710
- <!-- Lightbox -->
711
- <div class="lightbox" id="lightbox"><img id="lightbox-img" alt="zoom"></div>
712
-
713
- <!-- Global drop overlay -->
714
- <div class="drop-overlay" id="drop-overlay"><div class="drop-overlay-inner">Drop images to upload</div></div>
715
-
716
- <script type="module">
717
- import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
718
-
719
- /* ── DOM refs ── */
720
- const galleryGrid = document.getElementById('image-gallery-grid');
721
- const fileInput = document.getElementById('custom-file-input');
722
- const btnUpload = document.getElementById('tb-upload');
723
- const btnRemove = document.getElementById('tb-remove');
724
- const btnClear = document.getElementById('tb-clear');
725
- const promptInput = document.getElementById('custom-prompt-input');
726
- const charCount = document.getElementById('char-count');
727
- const loraSelect = document.getElementById('custom-lora-select');
728
- const runBtn = document.getElementById('custom-run-btn');
729
- const runBtnLabel = document.getElementById('run-btn-label');
730
- const cancelBtn = document.getElementById('custom-cancel-btn');
731
- const imgCountTb = document.getElementById('tb-image-count');
732
- const imgCountSb = document.getElementById('sb-image-count');
733
- const sbStatus = document.getElementById('sb-status');
734
- const loader = document.getElementById('output-loader');
735
- const loaderText = document.getElementById('loader-text');
736
- const loaderFill = document.getElementById('loader-bar-fill');
737
- const outBody = document.getElementById('output-image-container');
738
- const outPh = document.getElementById('output-placeholder');
739
- const dlBtn = document.getElementById('dl-btn-output');
740
- const compareBtn = document.getElementById('compare-btn');
741
- const useAsInputBtn= document.getElementById('use-as-input-btn');
742
- const zoomBtn = document.getElementById('zoom-btn');
743
- const seedChip = document.getElementById('out-seed-chip');
744
- const historyStrip = document.getElementById('history-strip');
745
- const historyCount = document.getElementById('history-count');
746
- const seedSlider = document.getElementById('custom-seed');
747
- const seedVal = document.getElementById('custom-seed-val');
748
- const lightbox = document.getElementById('lightbox');
749
- const lightboxImg = document.getElementById('lightbox-img');
750
- const dropOverlay = document.getElementById('drop-overlay');
751
- const compareWrap = document.getElementById('compare-wrap');
752
- const compareTop = document.getElementById('compare-top');
753
- const compareDivider = document.getElementById('compare-divider');
754
- const compareBeforeImg = document.getElementById('compare-before-img');
755
- const compareAfterImg = document.getElementById('compare-after-img');
756
- const canvasMeta = document.getElementById('canvas-meta');
757
- const segResult = document.getElementById('seg-result');
758
- const segInput = document.getElementById('seg-input');
759
- const connDot = document.getElementById('conn-dot');
760
- const connText = document.getElementById('conn-text');
761
-
762
- /* ── State (multi-image) ── */
763
- let images = [];
764
- let selectedIdx = -1;
765
- let toastTimer = null;
766
- let running = false;
767
- let currentJob = null;
768
- let currentResult = null; // {b64, seed, prompt, beforeB64}
769
- let history = [];
770
- let historyIdx = -1;
771
- let compareMode = false;
772
- let viewMode = 'result'; // 'result' | 'input'
773
-
774
- const SUGGESTIONS = [
775
- ['Add Logo', 'Add the batman logo to the image while preserving the background lighting and surrounding elements maintaining realism and original details.'],
776
- ['Add Glasses', 'Add the slim rectangular transparent frame sunglasses to the image while preserving the background lighting and surrounding elements maintaining realism and original details.'],
777
- ['Remove Objects', 'Remove the necklace and goggles from the image while preserving the background and remaining elements, maintaining realism and original details.'],
778
- ['Zoom', 'Zoom into the red highlighted area.'],
779
- ['Extract', 'Extract the clothing and create a flat mockup.'],
780
- ];
781
-
782
- /* ── Toast ── */
783
- function showToast(message, type) {
784
- let toast = document.getElementById('app-toast');
785
- if (!toast) {
786
- toast = document.createElement('div');
787
- toast.id = 'app-toast';
788
- toast.className = 'toast-notification';
789
- toast.innerHTML = '<span class="toast-icon"></span><span class="toast-text"></span>';
790
- document.body.appendChild(toast);
791
- }
792
- const icon = toast.querySelector('.toast-icon');
793
- const text = toast.querySelector('.toast-text');
794
- toast.className = 'toast-notification ' + (type || 'error');
795
- icon.textContent = type === 'warning' ? '⚠' : type === 'info' ? 'ℹ' : '✗';
796
- text.textContent = message;
797
- if (toastTimer) clearTimeout(toastTimer);
798
- void toast.offsetWidth;
799
- toast.classList.add('visible');
800
- toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500);
801
- }
802
-
803
- function flashPromptError() {
804
- promptInput.classList.add('error-flash');
805
- promptInput.focus();
806
- setTimeout(() => promptInput.classList.remove('error-flash'), 800);
807
- }
808
-
809
- function setStatus(text, cls) {
810
- sbStatus.textContent = text;
811
- sbStatus.className = 'sb-pill' + (cls ? ' ' + cls : '');
812
- }
813
-
814
- /* ── View mode ── */
815
- function setViewMode(mode) {
816
- viewMode = mode;
817
- segResult.classList.toggle('active', mode === 'result');
818
- segInput.classList.toggle('active', mode === 'input');
819
- renderCanvas();
820
- }
821
- segResult.addEventListener('click', () => setViewMode('result'));
822
- segInput.addEventListener('click', () => setViewMode('input'));
823
-
824
- function getCanvasImg() { return outBody.querySelector('img.modern-out-img'); }
825
-
826
- function renderCanvas() {
827
- exitCompareMode();
828
- let img = getCanvasImg();
829
- const showResult = viewMode === 'result' && currentResult;
830
- const showInput = viewMode === 'input' && images.length > 0;
831
-
832
- if (showResult || showInput) {
833
- outPh.style.display = 'none';
834
- if (!img) {
835
- img = document.createElement('img');
836
- img.className = 'modern-out-img';
837
- img.addEventListener('click', () => {
838
- if (img.src) { lightboxImg.src = img.src; lightbox.classList.add('visible'); }
839
- });
840
- outBody.appendChild(img);
841
- }
842
- img.style.display = '';
843
- if (showResult) {
844
- img.src = currentResult.b64;
845
- canvasMeta.textContent = (currentResult.lora || 'result') + ' · seed ' + currentResult.seed;
846
- } else {
847
- const i = selectedIdx >= 0 ? selectedIdx : 0;
848
- img.src = images[i].b64;
849
- canvasMeta.textContent = 'input #' + (i + 1) + ' · ' + (images[i].name || 'image');
850
- }
851
- } else {
852
- if (img) img.style.display = 'none';
853
- outPh.style.display = '';
854
- canvasMeta.textContent = images.length > 0
855
- ? images.length + ' input' + (images.length > 1 ? 's' : '') + ' ready — run an edit'
856
- : 'No image loaded';
857
- }
858
- updateRailState();
859
- }
860
-
861
- function updateRailState() {
862
- const hasResult = !!currentResult;
863
- dlBtn.disabled = !hasResult;
864
- zoomBtn.disabled = !hasResult && images.length === 0;
865
- useAsInputBtn.disabled = !hasResult;
866
- compareBtn.disabled = !(hasResult && currentResult.beforeB64);
867
- btnRemove.disabled = selectedIdx < 0;
868
- btnClear.disabled = images.length === 0;
869
- }
870
-
871
- /* ── Inputs filmstrip ── */
872
- function updateCounts() {
873
- const n = images.length;
874
- imgCountTb.textContent = n;
875
- imgCountSb.textContent = n > 0 ? n + ' input' + (n > 1 ? 's' : '') + ' · ' + history.length + ' result' + (history.length === 1 ? '' : 's') : 'No inputs';
876
- }
877
-
878
- function addImage(b64, name) {
879
- images.push({id: Date.now() + Math.random(), b64, name});
880
- if (selectedIdx < 0) selectedIdx = 0;
881
- renderFilmstrip(); updateCounts();
882
- if (!currentResult || viewMode === 'input') renderCanvas();
883
- }
884
-
885
- function removeImage(idx) {
886
- images.splice(idx, 1);
887
- if (selectedIdx === idx) selectedIdx = images.length ? 0 : -1;
888
- else if (selectedIdx > idx) selectedIdx--;
889
- renderFilmstrip(); updateCounts(); renderCanvas();
890
- }
891
-
892
- function clearAll() {
893
- images = []; selectedIdx = -1;
894
- renderFilmstrip(); updateCounts(); renderCanvas();
895
- }
896
-
897
- function renderFilmstrip() {
898
- galleryGrid.innerHTML = '';
899
- if (images.length === 0) {
900
- const empty = document.createElement('div');
901
- empty.className = 'fs-empty';
902
- empty.textContent = 'Drop or paste images…';
903
- galleryGrid.appendChild(empty);
904
- }
905
- images.forEach((img, i) => {
906
- const t = document.createElement('div');
907
- t.className = 'fs-thumb' + (i === selectedIdx ? ' selected' : '');
908
- t.innerHTML = '<img src="' + img.b64 + '" alt="">' +
909
- '<span class="fs-badge">#' + (i + 1) + '</span>' +
910
- '<button class="fs-remove">✕</button>';
911
- t.addEventListener('click', (e) => {
912
- if (e.target.closest('.fs-remove')) return;
913
- selectedIdx = i;
914
- renderFilmstrip();
915
- setViewMode('input');
916
- });
917
- t.querySelector('.fs-remove').addEventListener('click', (e) => {
918
- e.stopPropagation(); removeImage(i);
919
- });
920
- galleryGrid.appendChild(t);
921
- });
922
- const add = document.createElement('div');
923
- add.className = 'fs-add';
924
- add.innerHTML = '<span class="add-icon">+</span><span class="add-text">Add</span>';
925
- add.addEventListener('click', () => fileInput.click());
926
- galleryGrid.appendChild(add);
927
- updateRailState();
928
- }
929
-
930
- function processFiles(files) {
931
- let added = 0;
932
- Array.from(files).forEach(file => {
933
- if (!file.type.startsWith('image/')) return;
934
- added++;
935
- const reader = new FileReader();
936
- reader.onload = (e) => addImage(e.target.result, file.name);
937
- reader.readAsDataURL(file);
938
- });
939
- return added;
940
- }
941
-
942
- fileInput.addEventListener('change', (e) => { processFiles(e.target.files); e.target.value = ''; });
943
- btnUpload.addEventListener('click', () => fileInput.click());
944
- btnRemove.addEventListener('click', () => { if (selectedIdx >= 0) removeImage(selectedIdx); });
945
- btnClear.addEventListener('click', clearAll);
946
-
947
- /* ── Drop anywhere ── */
948
- let dragDepth = 0;
949
- window.addEventListener('dragenter', (e) => {
950
- if (!e.dataTransfer || !Array.from(e.dataTransfer.types).includes('Files')) return;
951
- e.preventDefault();
952
- dragDepth++;
953
- dropOverlay.classList.add('visible');
954
- });
955
- window.addEventListener('dragleave', (e) => {
956
- if (!e.dataTransfer || !Array.from(e.dataTransfer.types).includes('Files')) return;
957
- dragDepth = Math.max(0, dragDepth - 1);
958
- if (dragDepth === 0) dropOverlay.classList.remove('visible');
959
- });
960
- window.addEventListener('dragover', (e) => { e.preventDefault(); });
961
- window.addEventListener('drop', (e) => {
962
- e.preventDefault();
963
- dragDepth = 0;
964
- dropOverlay.classList.remove('visible');
965
- if (e.dataTransfer.files.length) {
966
- const n = processFiles(e.dataTransfer.files);
967
- if (n > 0) showToast('Added ' + n + ' image' + (n > 1 ? 's' : ''), 'info');
968
- }
969
- });
970
-
971
- /* ── Paste ── */
972
- document.addEventListener('paste', (e) => {
973
- if (e.target === promptInput) return;
974
- const items = e.clipboardData && e.clipboardData.items;
975
- if (!items) return;
976
- const files = [];
977
- for (const item of items) {
978
- if (item.kind === 'file' && item.type.startsWith('image/')) {
979
- const f = item.getAsFile();
980
- if (f) files.push(f);
981
- }
982
- }
983
- if (files.length) {
984
- e.preventDefault();
985
- processFiles(files);
986
- showToast('Pasted ' + files.length + ' image' + (files.length > 1 ? 's' : ''), 'info');
987
- }
988
- });
989
-
990
- /* ── Inspector tabs ── */
991
- const tabEdit = document.getElementById('tab-edit');
992
- const tabExamples = document.getElementById('tab-examples');
993
- const pageEdit = document.getElementById('page-edit');
994
- const pageExamples = document.getElementById('page-examples');
995
- tabEdit.addEventListener('click', () => {
996
- tabEdit.classList.add('active'); tabExamples.classList.remove('active');
997
- pageEdit.classList.add('active'); pageExamples.classList.remove('active');
998
- });
999
- tabExamples.addEventListener('click', () => {
1000
- tabExamples.classList.add('active'); tabEdit.classList.remove('active');
1001
- pageExamples.classList.add('active'); pageEdit.classList.remove('active');
1002
- });
1003
-
1004
- /* ── Suggestion chips ── */
1005
- const suggWrap = document.getElementById('suggestions-wrap');
1006
- SUGGESTIONS.forEach(([label, prompt]) => {
1007
- const chip = document.createElement('button');
1008
- chip.className = 'suggestion-chip';
1009
- chip.textContent = label;
1010
- chip.addEventListener('click', () => {
1011
- promptInput.value = prompt;
1012
- updateCharCount();
1013
- suggWrap.querySelectorAll('.suggestion-chip').forEach(c => c.classList.remove('active'));
1014
- chip.classList.add('active');
1015
- promptInput.focus();
1016
- });
1017
- suggWrap.appendChild(chip);
1018
- });
1019
-
1020
- function updateCharCount() { charCount.textContent = promptInput.value.length; }
1021
- promptInput.addEventListener('input', () => {
1022
- updateCharCount();
1023
- suggWrap.querySelectorAll('.suggestion-chip').forEach(c => c.classList.remove('active'));
1024
- });
1025
- updateCharCount();
1026
-
1027
- /* ── Sliders ── */
1028
- function bindSlider(id) {
1029
- const slider = document.getElementById(id);
1030
- const valSpan = document.getElementById(id + '-val');
1031
- slider.addEventListener('input', () => { valSpan.textContent = slider.value; });
1032
- }
1033
- bindSlider('custom-seed');
1034
- bindSlider('custom-guidance');
1035
- bindSlider('custom-steps');
1036
-
1037
- document.getElementById('dice-btn').addEventListener('click', () => {
1038
- const s = Math.floor(Math.random() * 2147483647);
1039
- seedSlider.value = s; seedVal.textContent = s;
1040
- });
1041
-
1042
- /* ── Loader ── */
1043
- function showLoader(text) {
1044
- loaderText.textContent = text || 'Processing image…';
1045
- loaderFill.classList.remove('determinate');
1046
- loaderFill.style.width = '100%';
1047
- loader.classList.add('active');
1048
- setStatus('Processing…');
1049
- }
1050
- function setLoaderProgress(text, frac) {
1051
- loaderText.textContent = text;
1052
- if (frac != null && frac >= 0 && frac <= 1) {
1053
- loaderFill.classList.add('determinate');
1054
- loaderFill.style.width = Math.max(4, Math.round(frac * 100)) + '%';
1055
- }
1056
- }
1057
- function hideLoader(statusText, cls) {
1058
- loader.classList.remove('active');
1059
- setStatus(statusText || 'Done', cls || 'done');
1060
- }
1061
-
1062
- /* ── Result / history ── */
1063
- function showResult(entry) {
1064
- currentResult = entry;
1065
- setViewMode('result');
1066
- dlBtn.disabled = false;
1067
- useAsInputBtn.disabled = false;
1068
- zoomBtn.disabled = false;
1069
- if (entry.beforeB64) compareBtn.disabled = false;
1070
- if (entry.seed != null) {
1071
- seedChip.textContent = 'seed ' + entry.seed;
1072
- seedChip.classList.add('visible');
1073
- }
1074
- }
1075
-
1076
- function renderHistory() {
1077
- historyStrip.innerHTML = '';
1078
- historyCount.textContent = history.length;
1079
- if (history.length === 0) {
1080
- const empty = document.createElement('div');
1081
- empty.className = 'fs-empty';
1082
- empty.textContent = 'No results yet';
1083
- historyStrip.appendChild(empty);
1084
- return;
1085
- }
1086
- history.forEach((entry, i) => {
1087
- const t = document.createElement('div');
1088
- t.className = 'fs-thumb' + (i === historyIdx ? ' selected' : '');
1089
- t.title = (entry.lora || '') + ' · seed ' + entry.seed;
1090
- const im = document.createElement('img');
1091
- im.src = entry.b64;
1092
- t.appendChild(im);
1093
- t.addEventListener('click', () => { historyIdx = i; showResult(history[i]); renderHistory(); });
1094
- historyStrip.appendChild(t);
1095
- });
1096
- }
1097
-
1098
- function pushHistory(entry) {
1099
- history.push(entry);
1100
- historyIdx = history.length - 1;
1101
- renderHistory();
1102
- updateCounts();
1103
- }
1104
-
1105
- /* ── Compare slider ── */
1106
- function enterCompareMode() {
1107
- if (!currentResult || !currentResult.beforeB64) return;
1108
- compareMode = true;
1109
- compareBtn.classList.add('active');
1110
- const img = getCanvasImg();
1111
- if (img) img.style.display = 'none';
1112
- compareAfterImg.src = currentResult.b64;
1113
- compareBeforeImg.src = currentResult.beforeB64;
1114
- compareWrap.classList.add('visible');
1115
- requestAnimationFrame(() => setComparePos(0.5));
1116
- }
1117
- function exitCompareMode() {
1118
- if (!compareMode) return;
1119
- compareMode = false;
1120
- compareBtn.classList.remove('active');
1121
- compareWrap.classList.remove('visible');
1122
- const img = getCanvasImg();
1123
- if (img && (viewMode === 'result' && currentResult || viewMode === 'input' && images.length)) img.style.display = '';
1124
- }
1125
- function setComparePos(frac) {
1126
- frac = Math.max(0, Math.min(1, frac));
1127
- const w = compareWrap.clientWidth;
1128
- const x = frac * w;
1129
- compareTop.style.clipPath = 'inset(0 ' + (w - x) + 'px 0 0)';
1130
- compareDivider.style.left = x + 'px';
1131
- }
1132
- compareBtn.addEventListener('click', () => { compareMode ? exitCompareMode() : enterCompareMode(); });
1133
-
1134
- let compareDragging = false;
1135
- function compareMove(clientX) {
1136
- const rect = compareWrap.getBoundingClientRect();
1137
- setComparePos((clientX - rect.left) / rect.width);
1138
- }
1139
- compareWrap.addEventListener('pointerdown', (e) => {
1140
- compareDragging = true;
1141
- compareWrap.setPointerCapture(e.pointerId);
1142
- compareMove(e.clientX);
1143
- });
1144
- compareWrap.addEventListener('pointermove', (e) => { if (compareDragging) compareMove(e.clientX); });
1145
- compareWrap.addEventListener('pointerup', () => { compareDragging = false; });
1146
- compareWrap.addEventListener('pointercancel', () => { compareDragging = false; });
1147
-
1148
- /* ── Lightbox ── */
1149
- lightbox.addEventListener('click', () => lightbox.classList.remove('visible'));
1150
- document.addEventListener('keydown', (e) => { if (e.key === 'Escape') lightbox.classList.remove('visible'); });
1151
- zoomBtn.addEventListener('click', () => {
1152
- const img = getCanvasImg();
1153
- const src = (viewMode === 'result' && currentResult) ? currentResult.b64
1154
- : (images.length ? images[selectedIdx >= 0 ? selectedIdx : 0].b64 : null);
1155
- if (src) { lightboxImg.src = src; lightbox.classList.add('visible'); }
1156
- });
1157
-
1158
- /* ── Download / chain / seed ── */
1159
- dlBtn.addEventListener('click', () => {
1160
- if (currentResult && currentResult.b64) {
1161
- const a = document.createElement('a');
1162
- a.href = currentResult.b64;
1163
- a.download = 'qwen_manipulator_' + (currentResult.seed != null ? currentResult.seed : 'output') + '.png';
1164
- document.body.appendChild(a); a.click(); document.body.removeChild(a);
1165
- }
1166
- });
1167
- useAsInputBtn.addEventListener('click', () => {
1168
- if (!currentResult) return;
1169
- addImage(currentResult.b64, 'edit_seed_' + (currentResult.seed != null ? currentResult.seed : 'x') + '.png');
1170
- showToast('Result added as input image', 'info');
1171
- });
1172
- seedChip.addEventListener('click', () => {
1173
- if (currentResult && currentResult.seed != null) {
1174
- navigator.clipboard.writeText(String(currentResult.seed)).then(
1175
- () => showToast('Seed copied: ' + currentResult.seed, 'info'), () => {});
1176
- }
1177
- });
1178
-
1179
- /* ── Backend connection ── */
1180
- let client = null;
1181
- try {
1182
- client = await Client.connect(window.location.origin);
1183
- setStatus('Ready');
1184
- connDot.classList.remove('off');
1185
- connText.textContent = 'connected';
1186
- } catch (e) {
1187
- console.error('Failed to connect to Gradio backend:', e);
1188
- setStatus('Offline', 'error');
1189
- connDot.classList.add('off');
1190
- connText.textContent = 'offline';
1191
- showToast('Could not connect to the backend API', 'error');
1192
- }
1193
-
1194
- /* ── Config: LoRAs + examples ── */
1195
- function renderConfig(cfg) {
1196
- loraSelect.innerHTML = '';
1197
- cfg.loras.forEach(name => {
1198
- const opt = document.createElement('option');
1199
- opt.value = name; opt.textContent = name;
1200
- loraSelect.appendChild(opt);
1201
- });
1202
- if (cfg.default_lora) loraSelect.value = cfg.default_lora;
1203
-
1204
- const list = document.getElementById('examples-scroll');
1205
- list.innerHTML = '';
1206
- cfg.examples.forEach(ex => {
1207
- const card = document.createElement('div');
1208
- card.className = 'example-card';
1209
- const thumbs = ex.thumbs.map(t =>
1210
- t ? '<img src="' + t + '" alt="">' : '<div class="example-thumb-placeholder">Preview</div>'
1211
- ).join('');
1212
- const promptShort = ex.prompt.length > 90 ? ex.prompt.slice(0, 90) + '…' : ex.prompt;
1213
- card.innerHTML =
1214
- '<div class="example-thumb-wrap">' + thumbs + '</div>'
1215
- + '<div class="example-body">'
1216
- + '<div class="example-meta">'
1217
- + '<span class="example-lora-badge">' + ex.n_images + ' img' + (ex.n_images > 1 ? 's' : '') + '</span>'
1218
- + '</div>'
1219
- + '<div class="example-prompt-text"></div>'
1220
- + '</div>';
1221
- card.querySelector('.example-prompt-text').textContent = promptShort;
1222
- card.addEventListener('click', () => loadExample(card, ex.idx));
1223
- list.appendChild(card);
1224
- });
1225
- }
1226
-
1227
- async function loadExample(card, idx) {
1228
- if (!client) { showToast('Backend not connected', 'error'); return; }
1229
- document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
1230
- card.classList.add('loading');
1231
- showToast('Loading example…', 'info');
1232
- try {
1233
- const result = await client.predict('/load_example', { idx: idx });
1234
- const data = result.data[0];
1235
- if (data && data.status === 'ok' && data.images && data.images.length > 0) {
1236
- clearAll();
1237
- if (data.prompt) { promptInput.value = data.prompt; updateCharCount(); }
1238
- if (data.lora) loraSelect.value = data.lora;
1239
- data.images.forEach((b64, i) => {
1240
- const name = (data.names && data.names[i]) ? data.names[i] : ('example_' + (i + 1) + '.jpg');
1241
- addImage(b64, name);
1242
- });
1243
- tabEdit.click();
1244
- showToast('Example loaded — ' + data.images.length + ' image(s)', 'info');
1245
- } else {
1246
- showToast('Could not load example images', 'error');
1247
- }
1248
- } catch (e) {
1249
- console.error('Example load error:', e);
1250
- showToast('Could not load example images', 'error');
1251
- } finally {
1252
- card.classList.remove('loading');
1253
- }
1254
- }
1255
-
1256
- try {
1257
- const cfg = await fetch('api/config').then(r => r.json());
1258
- renderConfig(cfg);
1259
- } catch (e) {
1260
- console.error('Failed to load config:', e);
1261
- }
1262
-
1263
- /* ── Run ── */
1264
- function validateBeforeRun() {
1265
- const promptVal = promptInput.value.trim();
1266
- const hasImages = images.length > 0;
1267
- if (!hasImages && !promptVal) { showToast('Please upload an image and enter a prompt', 'error'); flashPromptError(); return false; }
1268
- if (!hasImages) { showToast('Please upload at least one image', 'error'); return false; }
1269
- if (!promptVal) { showToast('Please enter an edit prompt', 'warning'); flashPromptError(); return false; }
1270
- return true;
1271
- }
1272
-
1273
- function setRunningUI(on) {
1274
- running = on;
1275
- runBtn.disabled = on;
1276
- runBtnLabel.textContent = on ? 'Editing…' : 'Edit Image';
1277
- cancelBtn.style.display = on ? 'flex' : 'none';
1278
- }
1279
-
1280
- async function runEdit() {
1281
- if (running) return;
1282
- if (!client) { showToast('Backend not connected', 'error'); return; }
1283
- if (!validateBeforeRun()) return;
1284
-
1285
- setRunningUI(true);
1286
- setViewMode('result');
1287
- showLoader('Submitting to queue…');
1288
-
1289
- const beforeB64 = images[0] ? images[0].b64 : null;
1290
- const usedPrompt = promptInput.value;
1291
- const usedLora = loraSelect.value;
1292
-
1293
- const payload = {
1294
- images_b64_json: JSON.stringify(images.map(img => img.b64)),
1295
- prompt: usedPrompt,
1296
- lora_adapter: usedLora,
1297
- seed: parseInt(seedSlider.value) || 0,
1298
- randomize_seed: document.getElementById('custom-randomize').checked,
1299
- guidance_scale: parseFloat(document.getElementById('custom-guidance').value),
1300
- steps: parseInt(document.getElementById('custom-steps').value),
1301
- };
1302
-
1303
- try {
1304
- currentJob = client.submit('/edit_image', payload);
1305
- for await (const msg of currentJob) {
1306
- if (msg.type === 'status') {
1307
- if (msg.status === 'pending') {
1308
- const pos = (msg.position != null && msg.position >= 0)
1309
- ? 'In queue — position ' + (msg.position + 1) + (msg.queue_size ? ' of ' + msg.queue_size : '')
1310
- : 'Waiting in queue…';
1311
- setLoaderProgress(pos, null);
1312
- setStatus('Queued');
1313
- } else if (msg.status === 'generating') {
1314
- setStatus('Processing…');
1315
- const pd = msg.progress_data;
1316
- if (pd && pd.length > 0) {
1317
- const p = pd[pd.length - 1];
1318
- if (p.index != null && p.length) {
1319
- const pct = Math.round((p.index / p.length) * 100);
1320
- setLoaderProgress('Generating… ' + p.index + '/' + p.length + ' (' + pct + '%)', p.index / p.length);
1321
- } else {
1322
- setLoaderProgress('Generating…', null);
1323
- }
1324
- } else {
1325
- setLoaderProgress('Generating image…', null);
1326
- }
1327
- } else if (msg.status === 'error') {
1328
- throw new Error(msg.message || 'Generation failed');
1329
- }
1330
- } else if (msg.type === 'data') {
1331
- const out = msg.data && msg.data[0];
1332
- if (out && out.image) {
1333
- const entry = { b64: out.image, seed: out.seed, prompt: usedPrompt, lora: usedLora, beforeB64 };
1334
- showResult(entry);
1335
- pushHistory(entry);
1336
- if (out.seed != null) {
1337
- seedSlider.value = out.seed;
1338
- seedVal.textContent = out.seed;
1339
- }
1340
- hideLoader('Done', 'done');
1341
- showToast('Image edited successfully', 'info');
1342
- } else {
1343
- hideLoader('Done', 'done');
1344
- }
1345
- }
1346
- }
1347
- } catch (e) {
1348
- console.error('Edit error:', e);
1349
- hideLoader('Error', 'error');
1350
- const msg = (e && e.message) ? e.message : 'Generation failed';
1351
- showToast(msg.length > 120 ? msg.slice(0, 120) + '…' : msg, 'error');
1352
- } finally {
1353
- setRunningUI(false);
1354
- currentJob = null;
1355
- }
1356
- }
1357
-
1358
- runBtn.addEventListener('click', runEdit);
1359
-
1360
- cancelBtn.addEventListener('click', async () => {
1361
- if (currentJob) {
1362
- try { await currentJob.cancel(); } catch (e) { console.warn('Cancel error:', e); }
1363
- currentJob = null;
1364
- }
1365
- setRunningUI(false);
1366
- hideLoader('Cancelled');
1367
- showToast('Generation cancelled', 'warning');
1368
- });
1369
-
1370
- document.addEventListener('keydown', (e) => {
1371
- if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
1372
- e.preventDefault();
1373
- runEdit();
1374
- }
1375
- });
1376
-
1377
- renderFilmstrip();
1378
- renderHistory();
1379
- updateCounts();
1380
- renderCanvas();
1381
- </script>
1382
- </body>
1383
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
pre-requirements.txt CHANGED
@@ -1 +1 @@
1
- pip==26.1.2
 
1
+ pip>=23.0.0
qwenimage/qwen_fa3_processor.py CHANGED
@@ -1,183 +1,89 @@
1
  """
2
  Paired with a good language model. Thanks!
3
-
4
- FA3 is currently broken on Blackwell (sm_100) GPUs; this module detects that
5
- at import time and falls back to PyTorch scaled-dot-product attention (SDPA)
6
- automatically. The public class name / call signature are unchanged.
7
  """
8
 
9
  import torch
10
- import torch.nn.functional as F
11
  from typing import Optional, Tuple
12
  from diffusers.models.transformers.transformer_qwenimage import apply_rotary_emb_qwen
13
 
14
-
15
- # ---------------------------------------------------------------------------
16
- # FA3 availability check
17
- # ---------------------------------------------------------------------------
18
-
19
- def _is_blackwell() -> bool:
20
- """Return True when the current default CUDA device is an sm_100 (Blackwell) GPU."""
21
- if not torch.cuda.is_available():
22
- return False
23
- cap = torch.cuda.get_device_capability()
24
- # Blackwell compute capability 10.x (sm_100)
25
- return cap[0] >= 10
26
-
27
-
28
- _fa3_available: bool = False
29
- _fa3_unavailable_reason: str = ""
30
- _flash_attn_func = None
31
-
32
- if _is_blackwell():
33
- _fa3_unavailable_reason = (
34
- "FlashAttention-3 is not yet supported on Blackwell (sm_100) GPUs. "
35
- "Falling back to scaled-dot-product attention (SDPA)."
36
- )
37
- else:
38
- try:
39
- from kernels import get_kernel
40
- _k = get_kernel("kernels-community/vllm-flash-attn3")
41
- _flash_attn_func = _k.flash_attn_func
42
- _fa3_available = True
43
- except Exception as e:
44
- _fa3_unavailable_reason = (
45
- "FlashAttention-3 via Hugging Face `kernels` is unavailable. "
46
- f"Tried `get_kernel('kernels-community/vllm-flash-attn3')` and failed with:\n{e}\n"
47
- "Falling back to scaled-dot-product attention (SDPA)."
48
  )
49
 
50
-
51
- # ---------------------------------------------------------------------------
52
- # FA3 custom op (registered only when the kernel loaded successfully)
53
- # ---------------------------------------------------------------------------
54
-
55
- if _fa3_available:
56
- @torch.library.custom_op("flash::flash_attn_func", mutates_args=())
57
- def flash_attn_func(
58
- q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, causal: bool = False
59
- ) -> torch.Tensor:
60
- # _flash_attn_func returns (output, softmax_lse); we only need output.
61
- output, _lse = _flash_attn_func(q, k, v, causal=causal)
62
- return output
63
-
64
- @flash_attn_func.register_fake
65
- def _flash_attn_func_fake(q, k, v, causal=False):
66
- # output shape mirrors q: (batch, seq_len, num_heads, head_dim)
67
- return torch.empty_like(q).contiguous()
68
-
69
- else:
70
- # Provide a stub so call-sites that import the symbol don't break at
71
- # module load; the processor will route around it at runtime.
72
- def flash_attn_func(
73
- q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, causal: bool = False
74
- ) -> torch.Tensor:
75
- raise RuntimeError(_fa3_unavailable_reason)
76
-
77
-
78
- # ---------------------------------------------------------------------------
79
- # SDPA fallback helper
80
- # ---------------------------------------------------------------------------
81
-
82
- def _sdpa_attention(
83
- q: torch.Tensor,
84
- k: torch.Tensor,
85
- v: torch.Tensor,
86
- causal: bool = False,
87
  ) -> torch.Tensor:
88
- """
89
- Scaled dot-product attention using torch.nn.functional.scaled_dot_product_attention.
90
-
91
- Input / output layout: (B, S, H, D_h) — same as the FA3 kernel.
92
- """
93
- # SDPA expects (B, H, S, D_h)
94
- q = q.transpose(1, 2)
95
- k = k.transpose(1, 2)
96
- v = v.transpose(1, 2)
97
 
98
- out = F.scaled_dot_product_attention(q, k, v, is_causal=causal)
 
 
 
 
 
 
99
 
100
- # Back to (B, S, H, D_h)
101
- return out.transpose(1, 2)
102
-
103
-
104
- # ---------------------------------------------------------------------------
105
- # Attention processor
106
- # ---------------------------------------------------------------------------
107
 
108
  class QwenDoubleStreamAttnProcessorFA3:
109
  """
110
- Attention processor for the Qwen double-stream architecture.
111
-
112
- Preferred backend: vLLM FlashAttention-3 via Hugging Face ``kernels``.
113
- Automatic fallback: PyTorch ``scaled_dot_product_attention`` (SDPA) when
114
- FA3 is unavailable — e.g. on Blackwell (sm_100) GPUs where FA3 is not yet
115
- supported, or when the ``kernels`` package is absent.
116
-
117
- Notes / limitations
118
- -------------------
119
- - Arbitrary attention masks are not supported on the FA3 path. Pass
120
- ``attention_mask=None`` (the default) to stay on the fast path.
121
- - On the SDPA path, ``attention_mask`` is likewise ignored; add explicit
122
- support here if you need it.
123
- - ``encoder_hidden_states`` (text stream) is required.
124
  """
125
 
126
- _attention_backend: str # set in __init__ after capability detection
127
 
128
  def __init__(self):
129
- if _fa3_available:
130
- self._attention_backend = "fa3"
131
- else:
132
- import warnings
133
- warnings.warn(
134
- f"QwenDoubleStreamAttnProcessorFA3: {_fa3_unavailable_reason}",
135
- stacklevel=2,
136
- )
137
- self._attention_backend = "sdpa"
138
-
139
- def _attend(
140
- self,
141
- q: torch.Tensor,
142
- k: torch.Tensor,
143
- v: torch.Tensor,
144
- causal: bool = False,
145
- ) -> torch.Tensor:
146
- """Dispatch to FA3 or SDPA depending on what is available."""
147
- if self._attention_backend == "fa3":
148
- return flash_attn_func(q, k, v, causal=causal)
149
- return _sdpa_attention(q, k, v, causal=causal)
150
 
151
  @torch.no_grad()
152
  def __call__(
153
  self,
154
- attn,
155
- hidden_states: torch.FloatTensor, # (B, S_img, D_model)
156
- encoder_hidden_states: torch.FloatTensor = None, # (B, S_txt, D_model)
157
- encoder_hidden_states_mask: torch.FloatTensor = None, # unused
158
- attention_mask: Optional[torch.FloatTensor] = None, # unsupported on FA3 path
159
- image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
160
  ) -> Tuple[torch.FloatTensor, torch.FloatTensor]:
161
-
162
  if encoder_hidden_states is None:
163
- raise ValueError(
164
- "QwenDoubleStreamAttnProcessorFA3 requires encoder_hidden_states (text stream)."
165
- )
166
- if attention_mask is not None and self._attention_backend == "fa3":
167
- raise NotImplementedError(
168
- "attention_mask is not supported on the FA3 path. "
169
- "Either drop the mask or let the processor fall back to SDPA."
170
- )
171
 
172
  B, S_img, _ = hidden_states.shape
173
  S_txt = encoder_hidden_states.shape[1]
174
 
175
- # ---- QKV projections ----
176
- img_q = attn.to_q(hidden_states)
177
  img_k = attn.to_k(hidden_states)
178
  img_v = attn.to_v(hidden_states)
179
 
180
- txt_q = attn.add_q_proj(encoder_hidden_states)
 
181
  txt_k = attn.add_k_proj(encoder_hidden_states)
182
  txt_v = attn.add_v_proj(encoder_hidden_states)
183
 
@@ -191,7 +97,7 @@ class QwenDoubleStreamAttnProcessorFA3:
191
  txt_k = txt_k.unflatten(-1, (H, -1))
192
  txt_v = txt_v.unflatten(-1, (H, -1))
193
 
194
- # ---- Q/K normalization ----
195
  if getattr(attn, "norm_q", None) is not None:
196
  img_q = attn.norm_q(img_q)
197
  if getattr(attn, "norm_k", None) is not None:
@@ -204,22 +110,25 @@ class QwenDoubleStreamAttnProcessorFA3:
204
  # ---- RoPE (Qwen variant) ----
205
  if image_rotary_emb is not None:
206
  img_freqs, txt_freqs = image_rotary_emb
 
207
  img_q = apply_rotary_emb_qwen(img_q, img_freqs, use_real=False)
208
  img_k = apply_rotary_emb_qwen(img_k, img_freqs, use_real=False)
209
  txt_q = apply_rotary_emb_qwen(txt_q, txt_freqs, use_real=False)
210
  txt_k = apply_rotary_emb_qwen(txt_k, txt_freqs, use_real=False)
211
 
212
  # ---- Joint attention over [text, image] along sequence axis ----
213
- q = torch.cat([txt_q, img_q], dim=1) # (B, S_txt + S_img, H, D_h)
 
214
  k = torch.cat([txt_k, img_k], dim=1)
215
  v = torch.cat([txt_v, img_v], dim=1)
216
 
217
- out = self._attend(q, k, v, causal=False) # (B, S_total, H, D_h)
 
218
 
219
  # ---- Back to (B, S, D_model) ----
220
  out = out.flatten(2, 3).to(q.dtype)
221
 
222
- # ---- Split text / image segments ----
223
  txt_attn_out = out[:, :S_txt, :]
224
  img_attn_out = out[:, S_txt:, :]
225
 
 
1
  """
2
  Paired with a good language model. Thanks!
 
 
 
 
3
  """
4
 
5
  import torch
 
6
  from typing import Optional, Tuple
7
  from diffusers.models.transformers.transformer_qwenimage import apply_rotary_emb_qwen
8
 
9
+ try:
10
+ from kernels import get_kernel
11
+ _k = get_kernel("kernels-community/vllm-flash-attn3")
12
+ _flash_attn_func = _k.flash_attn_func
13
+ except Exception as e:
14
+ _flash_attn_func = None
15
+ _kernels_err = e
16
+
17
+
18
+ def _ensure_fa3_available():
19
+ if _flash_attn_func is None:
20
+ raise ImportError(
21
+ "FlashAttention-3 via Hugging Face `kernels` is required. "
22
+ "Tried `get_kernel('kernels-community/vllm-flash-attn3')` and failed with:\n"
23
+ f"{_kernels_err}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  )
25
 
26
+ @torch.library.custom_op("flash::flash_attn_func", mutates_args=())
27
+ def flash_attn_func(
28
+ q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, causal: bool = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  ) -> torch.Tensor:
30
+ outputs, lse = _flash_attn_func(q, k, v, causal=causal)
31
+ return outputs
 
 
 
 
 
 
 
32
 
33
+ @flash_attn_func.register_fake
34
+ def _(q, k, v, **kwargs):
35
+ # two outputs:
36
+ # 1. output: (batch, seq_len, num_heads, head_dim)
37
+ # 2. softmax_lse: (batch, num_heads, seq_len) with dtype=torch.float32
38
+ meta_q = torch.empty_like(q).contiguous()
39
+ return meta_q #, q.new_empty((q.size(0), q.size(2), q.size(1)), dtype=torch.float32)
40
 
 
 
 
 
 
 
 
41
 
42
  class QwenDoubleStreamAttnProcessorFA3:
43
  """
44
+ FA3-based attention processor for Qwen double-stream architecture.
45
+ Computes joint attention over concatenated [text, image] streams using vLLM FlashAttention-3
46
+ accessed via Hugging Face `kernels`.
47
+
48
+ Notes / limitations:
49
+ - General attention masks are not supported here (FA3 path). `is_causal=False` and no arbitrary mask.
50
+ - Optional windowed attention / sink tokens / softcap can be plumbed through if you use those features.
51
+ - Expects an available `apply_rotary_emb_qwen` in scope (same as your non-FA3 processor).
 
 
 
 
 
 
52
  """
53
 
54
+ _attention_backend = "fa3" # for parity with your other processors, not used internally
55
 
56
  def __init__(self):
57
+ _ensure_fa3_available()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
  @torch.no_grad()
60
  def __call__(
61
  self,
62
+ attn, # Attention module with to_q/to_k/to_v/add_*_proj, norms, to_out, to_add_out, and .heads
63
+ hidden_states: torch.FloatTensor, # (B, S_img, D_model) image stream
64
+ encoder_hidden_states: torch.FloatTensor = None, # (B, S_txt, D_model) text stream
65
+ encoder_hidden_states_mask: torch.FloatTensor = None, # unused in FA3 path
66
+ attention_mask: Optional[torch.FloatTensor] = None, # unused in FA3 path
67
+ image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, # (img_freqs, txt_freqs)
68
  ) -> Tuple[torch.FloatTensor, torch.FloatTensor]:
 
69
  if encoder_hidden_states is None:
70
+ raise ValueError("QwenDoubleStreamAttnProcessorFA3 requires encoder_hidden_states (text stream).")
71
+ if attention_mask is not None:
72
+ # FA3 kernel path here does not consume arbitrary masks; fail fast to avoid silent correctness issues.
73
+ raise NotImplementedError("attention_mask is not supported in this FA3 implementation.")
74
+
75
+ _ensure_fa3_available()
 
 
76
 
77
  B, S_img, _ = hidden_states.shape
78
  S_txt = encoder_hidden_states.shape[1]
79
 
80
+ # ---- QKV projections (image/sample stream) ----
81
+ img_q = attn.to_q(hidden_states) # (B, S_img, D)
82
  img_k = attn.to_k(hidden_states)
83
  img_v = attn.to_v(hidden_states)
84
 
85
+ # ---- QKV projections (text/context stream) ----
86
+ txt_q = attn.add_q_proj(encoder_hidden_states) # (B, S_txt, D)
87
  txt_k = attn.add_k_proj(encoder_hidden_states)
88
  txt_v = attn.add_v_proj(encoder_hidden_states)
89
 
 
97
  txt_k = txt_k.unflatten(-1, (H, -1))
98
  txt_v = txt_v.unflatten(-1, (H, -1))
99
 
100
+ # ---- Q/K normalization (per your module contract) ----
101
  if getattr(attn, "norm_q", None) is not None:
102
  img_q = attn.norm_q(img_q)
103
  if getattr(attn, "norm_k", None) is not None:
 
110
  # ---- RoPE (Qwen variant) ----
111
  if image_rotary_emb is not None:
112
  img_freqs, txt_freqs = image_rotary_emb
113
+ # expects tensors shaped (B, S, H, D_h)
114
  img_q = apply_rotary_emb_qwen(img_q, img_freqs, use_real=False)
115
  img_k = apply_rotary_emb_qwen(img_k, img_freqs, use_real=False)
116
  txt_q = apply_rotary_emb_qwen(txt_q, txt_freqs, use_real=False)
117
  txt_k = apply_rotary_emb_qwen(txt_k, txt_freqs, use_real=False)
118
 
119
  # ---- Joint attention over [text, image] along sequence axis ----
120
+ # Shapes: (B, S_total, H, D_h)
121
+ q = torch.cat([txt_q, img_q], dim=1)
122
  k = torch.cat([txt_k, img_k], dim=1)
123
  v = torch.cat([txt_v, img_v], dim=1)
124
 
125
+ # FlashAttention-3 path expects (B, S, H, D_h) and returns (out, softmax_lse)
126
+ out = flash_attn_func(q, k, v, causal=False) # out: (B, S_total, H, D_h)
127
 
128
  # ---- Back to (B, S, D_model) ----
129
  out = out.flatten(2, 3).to(q.dtype)
130
 
131
+ # Split back to text / image segments
132
  txt_attn_out = out[:, :S_txt, :]
133
  img_attn_out = out[:, S_txt:, :]
134
 
requirements.txt CHANGED
@@ -1,13 +1,14 @@
1
- --extra-index-url https://download.pytorch.org/whl/cu130
2
-
3
- torch==2.11.0
4
- torchvision==0.26.0
5
- transformers==5.14.1
6
- accelerate==1.14.0
7
- diffusers==0.39.0
8
- peft==0.19.1
9
- gradio==6.25.0
10
- av==17.1.0
11
- spaces==0.51.1
12
- huggingface-hub==1.24.0
13
- kernels==0.16.0
 
 
1
+ git+https://github.com/huggingface/transformers.git@v4.57.3
2
+ git+https://github.com/huggingface/accelerate.git
3
+ git+https://github.com/huggingface/diffusers.git
4
+ git+https://github.com/huggingface/peft.git
5
+ huggingface_hub
6
+ sentencepiece
7
+ torchvision
8
+ supervision
9
+ kernels
10
+ spaces
11
+ hf_xet
12
+ torch
13
+ numpy
14
+ av