diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -1,452 +1,1695 @@
-import os
-import gc
-import gradio as gr
-import numpy as np
-import spaces
-import torch
-import random
-from PIL import Image, ImageOps
-from typing import Iterable
-from gradio.themes import Soft
-from gradio.themes.utils import colors, fonts, sizes
-
-colors.orange_red = colors.Color(
- name="orange_red",
- c50="#FFF0E5",
- c100="#FFE0CC",
- c200="#FFC299",
- c300="#FFA366",
- c400="#FF8533",
- c500="#FF4500",
- c600="#E63E00",
- c700="#CC3700",
- c800="#B33000",
- c900="#992900",
- c950="#802200",
-)
-
-class OrangeRedTheme(Soft):
- def __init__(
- self,
- *,
- primary_hue: colors.Color | str = colors.gray,
- secondary_hue: colors.Color | str = colors.orange_red,
- neutral_hue: colors.Color | str = colors.slate,
- text_size: sizes.Size | str = sizes.text_lg,
- font: fonts.Font | str | Iterable[fonts.Font | str] = (
- fonts.GoogleFont("Outfit"), "Arial", "sans-serif",
- ),
- font_mono: fonts.Font | str | Iterable[fonts.Font | str] = (
- fonts.GoogleFont("IBM Plex Mono"), "ui-monospace", "monospace",
- ),
- ):
- super().__init__(
- primary_hue=primary_hue,
- secondary_hue=secondary_hue,
- neutral_hue=neutral_hue,
- text_size=text_size,
- font=font,
- font_mono=font_mono,
- )
- super().set(
- background_fill_primary="*primary_50",
- background_fill_primary_dark="*primary_900",
- body_background_fill="linear-gradient(135deg, *primary_200, *primary_100)",
- body_background_fill_dark="linear-gradient(135deg, *primary_900, *primary_800)",
- button_primary_text_color="white",
- button_primary_text_color_hover="white",
- button_primary_background_fill="linear-gradient(90deg, *secondary_500, *secondary_600)",
- button_primary_background_fill_hover="linear-gradient(90deg, *secondary_600, *secondary_700)",
- button_primary_background_fill_dark="linear-gradient(90deg, *secondary_600, *secondary_700)",
- button_primary_background_fill_hover_dark="linear-gradient(90deg, *secondary_500, *secondary_600)",
- button_secondary_text_color="black",
- button_secondary_text_color_hover="white",
- button_secondary_background_fill="linear-gradient(90deg, *primary_300, *primary_300)",
- button_secondary_background_fill_hover="linear-gradient(90deg, *primary_400, *primary_400)",
- button_secondary_background_fill_dark="linear-gradient(90deg, *primary_500, *primary_600)",
- button_secondary_background_fill_hover_dark="linear-gradient(90deg, *primary_500, *primary_500)",
- slider_color="*secondary_500",
- slider_color_dark="*secondary_600",
- block_title_text_weight="600",
- block_border_width="3px",
- block_shadow="*shadow_drop_lg",
- button_primary_shadow="*shadow_drop_lg",
- button_large_padding="11px",
- color_accent_soft="*primary_100",
- block_label_background_fill="*primary_200",
- )
-
-orange_red_theme = OrangeRedTheme()
-
-device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
-
-print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
-print("torch.__version__ =", torch.__version__)
-print("Using device:", device)
-
-from diffusers import FlowMatchEulerDiscreteScheduler
-from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
-from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
-
-dtype = torch.bfloat16
-
-pipe = QwenImageEditPlusPipeline.from_pretrained(
- "Qwen/Qwen-Image-Edit-2511",
- transformer=QwenImageTransformer2DModel.from_pretrained(
- "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19",
- torch_dtype=dtype,
- device_map='cuda'
- ),
- torch_dtype=dtype
-).to(device)
-
-print("Flash Attention 3 disabled for ZeroGPU A10G compatibility; using default attention.")
-
-MAX_SEED = np.iinfo(np.int32).max
-
-ADAPTER_SPECS = {
- "Multiple-Angles": {
- "repo": "dx8152/Qwen-Edit-2509-Multiple-angles",
- "weights": "镜头转换.safetensors",
- "adapter_name": "multiple-angles"
- },
- "Photo-to-Anime": {
- "repo": "autoweeb/Qwen-Image-Edit-2509-Photo-to-Anime",
- "weights": "Qwen-Image-Edit-2509-Photo-to-Anime_000001000.safetensors",
- "adapter_name": "photo-to-anime"
- },
- "Anime-V2": {
- "repo": "prithivMLmods/Qwen-Image-Edit-2511-Anime",
- "weights": "Qwen-Image-Edit-2511-Anime-2000.safetensors",
- "adapter_name": "anime-v2"
- },
- "Light-Migration": {
- "repo": "dx8152/Qwen-Edit-2509-Light-Migration",
- "weights": "参考色调.safetensors",
- "adapter_name": "light-migration"
- },
- "Upscaler": {
- "repo": "starsfriday/Qwen-Image-Edit-2511-Upscale2K",
- "weights": "qwen_image_edit_2511_upscale.safetensors",
- "adapter_name": "upscale-2k"
- },
- "Style-Transfer": {
- "repo": "zooeyy/Style-Transfer",
- "weights": "Style Transfer-Alpha-V0.1.safetensors",
- "adapter_name": "style-transfer"
- },
- "Manga-Tone": {
- "repo": "nappa114514/Qwen-Image-Edit-2509-Manga-Tone",
- "weights": "tone001.safetensors",
- "adapter_name": "manga-tone"
- },
- "Anything2Real": {
- "repo": "lrzjason/Anything2Real_2601",
- "weights": "anything2real_2601.safetensors",
- "adapter_name": "anything2real"
- },
- "Fal-Multiple-Angles": {
- "repo": "fal/Qwen-Image-Edit-2511-Multiple-Angles-LoRA",
- "weights": "qwen-image-edit-2511-multiple-angles-lora.safetensors",
- "adapter_name": "fal-multiple-angles"
- },
- "Polaroid-Photo": {
- "repo": "prithivMLmods/Qwen-Image-Edit-2511-Polaroid-Photo",
- "weights": "Qwen-Image-Edit-2511-Polaroid-Photo.safetensors",
- "adapter_name": "polaroid-photo"
- },
- "Unblur-Anything": {
- "repo": "prithivMLmods/Qwen-Image-Edit-2511-Unblur-Upscale",
- "weights": "Qwen-Image-Edit-Unblur-Upscale_15.safetensors",
- "adapter_name": "unblur-anything"
- },
- "Midnight-Noir-Eyes-Spotlight": {
- "repo": "prithivMLmods/Qwen-Image-Edit-2511-Midnight-Noir-Eyes-Spotlight",
- "weights": "Qwen-Image-Edit-2511-Midnight-Noir-Eyes-Spotlight.safetensors",
- "adapter_name": "midnight-noir-eyes-spotlight"
- },
- "Hyper-Realistic-Portrait": {
- "repo": "prithivMLmods/Qwen-Image-Edit-2511-Hyper-Realistic-Portrait",
- "weights": "HRP_20.safetensors",
- "adapter_name": "hyper-realistic-portrait"
- },
- "Ultra-Realistic-Portrait": {
- "repo": "prithivMLmods/Qwen-Image-Edit-2511-Ultra-Realistic-Portrait",
- "weights": "URP_20.safetensors",
- "adapter_name": "ultra-realistic-portrait"
- },
- "Pixar-Inspired-3D": {
- "repo": "prithivMLmods/Qwen-Image-Edit-2511-Pixar-Inspired-3D",
- "weights": "PI3_20.safetensors",
- "adapter_name": "pi3"
- },
- "Noir-Comic-Book": {
- "repo": "prithivMLmods/Qwen-Image-Edit-2511-Noir-Comic-Book-Panel",
- "weights": "Noir-Comic-Book-Panel_20.safetensors",
- "adapter_name": "ncb"
- },
- "Any-light": {
- "repo": "lilylilith/QIE-2511-MP-AnyLight",
- "weights": "QIE-2511-AnyLight_.safetensors",
- "adapter_name": "any-light"
- },
- "Studio-DeLight": {
- "repo": "prithivMLmods/QIE-2511-Studio-DeLight",
- "weights": "QIE-2511-Studio-DeLight-5000.safetensors",
- "adapter_name": "studio-delight"
- },
- "Cinematic-FlatLog": {
- "repo": "prithivMLmods/QIE-2511-Cinematic-FlatLog-Control",
- "weights": "QIE-2511-Cinematic-FlatLog-Control-3200.safetensors",
- "adapter_name": "flat-log"
- },
-}
-
-LOADED_ADAPTERS = set()
-
-def update_dimensions_on_upload(image):
- if image is None:
- return 1024, 1024
-
- original_width, original_height = image.size
- if original_width <= 0 or original_height <= 0:
- return 1024, 1024
-
- # Keep roughly a 1MP canvas while preserving the source aspect ratio.
- # The old long-side=1024 rule shrank portrait/wide photos too aggressively.
- target_area = 1024 * 1024
- max_side = 1536
- aspect_ratio = original_width / original_height
- new_width = int((target_area * aspect_ratio) ** 0.5)
- new_height = int(new_width / aspect_ratio)
-
- if max(new_width, new_height) > max_side:
- scale = max_side / max(new_width, new_height)
- new_width = int(new_width * scale)
- new_height = int(new_height * scale)
-
- new_width = max(64, (new_width // 32) * 32)
- new_height = max(64, (new_height // 32) * 32)
-
- return new_width, new_height
-
-def get_qwen_gpu_duration(*args, **kwargs):
- steps = kwargs.get("steps")
- if steps is None and len(args) >= 7:
- steps = args[6]
- try:
- steps = int(steps)
- except Exception:
- steps = 4
-
- if steps <= 8:
- return 25
- if steps <= 16:
- return 35
- return min(120, max(50, 20 + steps * 2))
-
-@spaces.GPU(duration=get_qwen_gpu_duration)
-def infer(
- images,
- prompt,
- lora_adapter,
- seed,
- randomize_seed,
- guidance_scale,
- steps,
- progress=gr.Progress(track_tqdm=True)
-):
- gc.collect()
- torch.cuda.empty_cache()
-
- if not images:
- raise gr.Error("Please upload at least one image to edit.")
-
- pil_images = []
- if images is not None:
- if isinstance(images, (str, Image.Image, dict)) or hasattr(images, "name"):
- images = [images]
- for item in images:
- try:
- if isinstance(item, tuple) or isinstance(item, list):
- path_or_img = item[0]
- elif isinstance(item, dict):
- path_or_img = item.get("path") or item.get("name") or item.get("image") or item
- if isinstance(path_or_img, dict):
- path_or_img = path_or_img.get("path") or path_or_img.get("name")
- else:
- path_or_img = item
-
- if isinstance(path_or_img, str):
- pil_images.append(ImageOps.exif_transpose(Image.open(path_or_img)).convert("RGB"))
- elif isinstance(path_or_img, Image.Image):
- pil_images.append(ImageOps.exif_transpose(path_or_img).convert("RGB"))
- elif hasattr(path_or_img, "path"):
- pil_images.append(ImageOps.exif_transpose(Image.open(path_or_img.path)).convert("RGB"))
- else:
- pil_images.append(ImageOps.exif_transpose(Image.open(path_or_img.name)).convert("RGB"))
- except Exception as e:
- print(f"Skipping invalid image item: {e}")
- continue
-
- if not pil_images:
- raise gr.Error("Could not process uploaded images.")
-
- spec = ADAPTER_SPECS.get(lora_adapter)
- if not spec:
- raise gr.Error(f"Configuration not found for: {lora_adapter}")
-
- adapter_name = spec["adapter_name"]
-
- if adapter_name not in LOADED_ADAPTERS:
- print(f"--- Downloading and Loading Adapter: {lora_adapter} ---")
- try:
- pipe.load_lora_weights(
- spec["repo"],
- weight_name=spec["weights"],
- adapter_name=adapter_name
- )
- LOADED_ADAPTERS.add(adapter_name)
- except Exception as e:
- raise gr.Error(f"Failed to load adapter {lora_adapter}: {e}")
- else:
- print(f"--- Adapter {lora_adapter} is already loaded. ---")
-
- pipe.set_adapters([adapter_name], adapter_weights=[1.0])
-
- if randomize_seed:
- seed = random.randint(0, MAX_SEED)
-
- generator = torch.Generator(device=device).manual_seed(seed)
- 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"
-
- width, height = update_dimensions_on_upload(pil_images[0])
-
- try:
- result_image = pipe(
- image=pil_images,
- prompt=prompt,
- negative_prompt=negative_prompt,
- height=height,
- width=width,
- num_inference_steps=steps,
- generator=generator,
- true_cfg_scale=guidance_scale,
- ).images[0]
-
- return result_image, seed
-
- except Exception as e:
- raise e
- finally:
- gc.collect()
- torch.cuda.empty_cache()
-
-@spaces.GPU(duration=get_qwen_gpu_duration)
-def infer_example(images, prompt, lora_adapter):
- if not images:
- return None, 0
-
- if isinstance(images, str):
- images_list = [images]
- else:
- images_list = images
-
- result, seed = infer(
- images=images_list,
- prompt=prompt,
- lora_adapter=lora_adapter,
- seed=0,
- randomize_seed=True,
- guidance_scale=1.0,
- steps=4
- )
- return result, seed
-
-css="""
-#col-container {
- margin: 0 auto;
- max-width: 1000px;
-}
-#main-title h1 {font-size: 2.4em !important;}
-"""
-
-with gr.Blocks() as demo:
- with gr.Column(elem_id="col-container"):
- gr.Markdown("# **Qwen-Image-Edit-2511-LoRAs-Fast**", elem_id="main-title")
- gr.Markdown("Perform diverse image edits using specialized [LoRA](https://huggingface.co/models?other=base_model:adapter:Qwen/Qwen-Image-Edit-2511) adapters. Open on [GitHub](https://github.com/PRITHIVSAKTHIUR/Qwen-Image-Edit-2511-LoRAs-Fast-Lazy-Load).")
-
- with gr.Row(equal_height=True):
- with gr.Column():
- images = gr.File(
- label="Upload Images",
- file_count="multiple",
- type="filepath",
- file_types=["image"],
- height=300,
- )
-
- prompt = gr.Text(
- label="Edit Prompt",
- #max_lines=1,
- show_label=True,
- placeholder="e.g., transform into anime..",
- )
-
- run_button = gr.Button("Edit Image", variant="primary")
-
- with gr.Column():
- output_image = gr.Image(label="Output Image", interactive=False, format="png", height=365)
-
- with gr.Row():
- lora_adapter = gr.Dropdown(
- label="Choose Editing Style",
- choices=list(ADAPTER_SPECS.keys()),
- value="Photo-to-Anime"
- )
-
- with gr.Accordion("Advanced Settings", open=False, visible=False):
- seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0)
- randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
- guidance_scale = gr.Slider(label="Guidance Scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0)
- steps = gr.Slider(label="Inference Steps", minimum=1, maximum=50, step=1, value=4)
-
- gr.Examples(
- examples=[
- [["examples/B.jpg"], "Transform into anime.", "Photo-to-Anime"],
- [["examples/HRP.jpg"], "Transform into a hyper-realistic face portrait.", "Hyper-Realistic-Portrait"],
- [["examples/A.jpeg"], "Rotate the camera 45 degrees to the right.", "Multiple-Angles"],
- [["examples/U.jpg"], "Upscale this picture to 4K resolution.", "Upscaler"],
- [["examples/L1.jpg", "examples/L2.jpg"], "Apply the lighting from image 2 to image 1.", "Any-light"],
- [["examples/PP1.jpg"], "cinematic polaroid with soft grain subtle vignette gentle lighting white frame handwritten photographed by hf preserving realistic texture and details", "Polaroid-Photo"],
- [["examples/Z1.jpg"], "Front-right quarter view.", "Fal-Multiple-Angles"],
- [["examples/URP.jpg"], "Transform into a cinematic flat log.", "Cinematic-FlatLog"],
- [["examples/SL.jpg"], "Neutral uniform lighting Preserve identity and composition.", "Studio-DeLight"],
- [["examples/PI.jpg"], "Transform it into Pixar-inspired 3D.", "Pixar-Inspired-3D"],
- [["examples/MT.jpg"], "Paint with manga tone.", "Manga-Tone"],
- [["examples/NCB.jpg"], "Transform into a noir comic book style.", "Noir-Comic-Book"],
- [["examples/URP.jpg"], "ultra-realistic portrait.", "Ultra-Realistic-Portrait"],
- [["examples/MN.jpg"], "Transform into Midnight Noir Eyes Spotlight.", "Midnight-Noir-Eyes-Spotlight"],
- [["examples/ST1.jpg", "examples/ST2.jpg"], "Convert Image 1 to the style of Image 2.", "Style-Transfer"],
- [["examples/R1.jpg"], "Change the picture to realistic photograph.", "Anything2Real"],
- [["examples/UA.jpeg"], "Unblur and upscale.", "Unblur-Anything"],
- [["examples/L1.jpg", "examples/L2.jpg"], "Refer to the color tone, remove the original lighting from Image 1, and relight Image 1 based on the lighting and color tone of Image 2.", "Light-Migration"],
- [["examples/P1.jpg"], "Transform into anime (while preserving the background and remaining elements maintaining realism and original details.)", "Anime-V2"],
- ],
- inputs=[images, prompt, lora_adapter],
- outputs=[output_image, seed],
- fn=infer_example,
- cache_examples=False,
- label="Examples"
- )
-
- gr.Markdown("[*](https://huggingface.co/spaces/prithivMLmods/Qwen-Image-Edit-2511-LoRAs-Fast)This is still an experimental Space for Qwen-Image-Edit-2511.")
-
- run_button.click(
- fn=infer,
- inputs=[images, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps],
- outputs=[output_image, seed]
- )
-
-if __name__ == "__main__":
- demo.queue(max_size=30).launch(css=css, theme=orange_red_theme, mcp_server=True, ssr_mode=False, show_error=True)
+import os
+import gc
+import gradio as gr
+import numpy as np
+import spaces
+import torch
+import random
+import base64
+import json
+import html as html_lib
+from io import BytesIO
+from PIL import Image
+
+MAX_SEED = np.iinfo(np.int32).max
+LANCZOS = getattr(Image, "Resampling", Image).LANCZOS
+
+device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
+
+print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
+print("torch.__version__ =", torch.__version__)
+print("torch.version.cuda =", torch.version.cuda)
+print("cuda available:", torch.cuda.is_available())
+print("cuda device count:", torch.cuda.device_count())
+if torch.cuda.is_available():
+ print("current device:", torch.cuda.current_device())
+ print("device name:", torch.cuda.get_device_name(torch.cuda.current_device()))
+
+print("Using device:", device)
+
+from diffusers import FlowMatchEulerDiscreteScheduler
+from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
+from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
+from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
+
+dtype = torch.bfloat16
+
+pipe = QwenImageEditPlusPipeline.from_pretrained(
+ "Qwen/Qwen-Image-Edit-2511",
+ transformer=QwenImageTransformer2DModel.from_pretrained(
+ "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19",
+ torch_dtype=dtype,
+ device_map="cuda",
+ ),
+ torch_dtype=dtype,
+).to(device)
+
+# FA3 has no kernels on Blackwell (sm_100) ZeroGPU hardware -> keep default SDPA attention.
+print("FA3 disabled for Blackwell (sm_100) compatibility; using default SDPA attention.")
+
+ADAPTER_SPECS = {
+ "XXX-Action": {
+ "repo": "systms/SYSTMS-ACTION-LoRA-Qwen-Image-Edit-2511",
+ "weights": "QWEN_EDIT_ACTION_V1.safetensors",
+ "adapter_name": "xxx-action",
+ },
+ "XXX-HugeBoobs": {
+ "repo": "houseofboern/Huge-Boobs-Qwen-Edit-2511",
+ "weights": "huge_boobs_v13_qwen2511_000001750.safetensors",
+ "adapter_name": "xxx-hugeboobs",
+ },
+ "XXX-Infl8": {
+ "repo": "systms/SYSTMS-INFL8-LoRA-Qwen-Image-Edit-2511",
+ "weights": "SYSTMS_INFL8_LoRA_Qwen_Image_Edit_2511.safetensors",
+ "adapter_name": "xxx-infl8",
+ },
+ "Util-FixBody": {
+ "repo": "nbeerbower/FIXBODYr128-QwenImageEdit2509",
+ "weights": "pytorch_lora_weights.safetensors",
+ "adapter_name": "util-fixbody",
+ },
+ "Util-InScene": {
+ "repo": "flymy-ai/qwen-image-edit-inscene-lora",
+ "weights": "flymy_qwen_image_edit_inscene_lora.safetensors",
+ "adapter_name": "util-inscene",
+ },
+ "XXX-Masturbate": {
+ "repo": "SexGod1979/QwenEdit_2511_Masturbate",
+ "weights": "SEXGOD_masturbate_QwenEdit2511_v1.safetensors",
+ "adapter_name": "xxx-masturbate",
+ },
+ "XXX-TornClothes": {
+ "repo": "AliIbrahim10/Qwen-Image-Edit-2511-torn-clothes",
+ "weights": "tc3_002.safetensors",
+ "adapter_name": "xxx-torn",
+ },
+ "Util-InSubject": {
+ "repo": "peteromallet/Qwen-Image-Edit-InSubject",
+ "weights": "InSubject-0.5.safetensors",
+ "adapter_name": "insubject",
+ },
+ "XXX-Couples": {
+ "repo": "SexGod1979/QwenEdit_2511_CouplesNudity",
+ "weights": "SexGod_CouplesNudity_QwenEdit_2511_v1.safetensors",
+ "adapter_name": "xxx-couples",
+ },
+ "XXX-Nudes": {
+ "repo": "SexGod1979/QwenEdit_2511_FemaleNudity",
+ "weights": "SEXGOD_FemaleNudity_QwenEdit_2511_v2.safetensors",
+ "adapter_name": "xxx-nudes",
+ },
+ "XXX-Unchained": {
+ "repo": "urogin/QWEN-Edit-2509-Unchained-XXX",
+ "weights": "Unchained-XXX.safetensors",
+ "adapter_name": "xxx-unchained",
+ },
+ "XXX-Acts": {
+ "repo": "ScottzillaSystems/qwen-image-edit-plus-nsfw-lora",
+ "weights": "qwen-image-edit-plus-nsfw-lora.safetensors",
+ "adapter_name": "xxx-acts",
+ },
+ "Multiple-Angles": {
+ "repo": "dx8152/Qwen-Edit-2509-Multiple-angles",
+ "weights": "镜头转换.safetensors",
+ "adapter_name": "multiple-angles",
+ },
+ "Photo-to-Anime": {
+ "repo": "autoweeb/Qwen-Image-Edit-2509-Photo-to-Anime",
+ "weights": "Qwen-Image-Edit-2509-Photo-to-Anime_000001000.safetensors",
+ "adapter_name": "photo-to-anime",
+ },
+ "Anime-V2": {
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Anime",
+ "weights": "Qwen-Image-Edit-2511-Anime-2000.safetensors",
+ "adapter_name": "anime-v2",
+ },
+ "Light-Migration": {
+ "repo": "dx8152/Qwen-Edit-2509-Light-Migration",
+ "weights": "参考色调.safetensors",
+ "adapter_name": "light-migration",
+ },
+ "Upscaler": {
+ "repo": "starsfriday/Qwen-Image-Edit-2511-Upscale2K",
+ "weights": "qwen_image_edit_2511_upscale.safetensors",
+ "adapter_name": "upscale-2k",
+ },
+ "Style-Transfer": {
+ "repo": "zooeyy/Style-Transfer",
+ "weights": "Style Transfer-Alpha-V0.1.safetensors",
+ "adapter_name": "style-transfer",
+ },
+ "Manga-Tone": {
+ "repo": "nappa114514/Qwen-Image-Edit-2509-Manga-Tone",
+ "weights": "tone001.safetensors",
+ "adapter_name": "manga-tone",
+ },
+ "Anything2Real": {
+ "repo": "lrzjason/Anything2Real_2601",
+ "weights": "anything2real_2601.safetensors",
+ "adapter_name": "anything2real",
+ },
+ "Fal-Multiple-Angles": {
+ "repo": "fal/Qwen-Image-Edit-2511-Multiple-Angles-LoRA",
+ "weights": "qwen-image-edit-2511-multiple-angles-lora.safetensors",
+ "adapter_name": "fal-multiple-angles",
+ },
+ "Polaroid-Photo": {
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Polaroid-Photo",
+ "weights": "Qwen-Image-Edit-2511-Polaroid-Photo.safetensors",
+ "adapter_name": "polaroid-photo",
+ },
+ "Unblur-Anything": {
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Unblur-Upscale",
+ "weights": "Qwen-Image-Edit-Unblur-Upscale_15.safetensors",
+ "adapter_name": "unblur-anything",
+ },
+ "Midnight-Noir-Eyes-Spotlight": {
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Midnight-Noir-Eyes-Spotlight",
+ "weights": "Qwen-Image-Edit-2511-Midnight-Noir-Eyes-Spotlight.safetensors",
+ "adapter_name": "midnight-noir-eyes-spotlight",
+ },
+ "Hyper-Realistic-Portrait": {
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Hyper-Realistic-Portrait",
+ "weights": "HRP_20.safetensors",
+ "adapter_name": "hyper-realistic-portrait",
+ },
+ "Ultra-Realistic-Portrait": {
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Ultra-Realistic-Portrait",
+ "weights": "URP_20.safetensors",
+ "adapter_name": "ultra-realistic-portrait",
+ },
+ "Pixar-Inspired-3D": {
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Pixar-Inspired-3D",
+ "weights": "PI3_20.safetensors",
+ "adapter_name": "pi3",
+ },
+ "Noir-Comic-Book": {
+ "repo": "prithivMLmods/Qwen-Image-Edit-2511-Noir-Comic-Book-Panel",
+ "weights": "Noir-Comic-Book-Panel_20.safetensors",
+ "adapter_name": "ncb",
+ },
+ "Any-light": {
+ "repo": "lilylilith/QIE-2511-MP-AnyLight",
+ "weights": "QIE-2511-AnyLight_.safetensors",
+ "adapter_name": "any-light",
+ },
+ "Studio-DeLight": {
+ "repo": "prithivMLmods/QIE-2511-Studio-DeLight",
+ "weights": "QIE-2511-Studio-DeLight-5000.safetensors",
+ "adapter_name": "studio-delight",
+ },
+ "Cinematic-FlatLog": {
+ "repo": "prithivMLmods/QIE-2511-Cinematic-FlatLog-Control",
+ "weights": "QIE-2511-Cinematic-FlatLog-Control-3200.safetensors",
+ "adapter_name": "flat-log",
+ },
+}
+
+LOADED_ADAPTERS: set = set()
+ADAPTER_NAMES = list(ADAPTER_SPECS.keys())
+
+EXAMPLES_CONFIG = [
+ {"images": ["examples/B.jpg"], "prompt": "Transform into anime.", "lora": "Photo-to-Anime"},
+ {"images": ["examples/HRP.jpg"], "prompt": "Transform into a hyper-realistic face portrait.", "lora": "Hyper-Realistic-Portrait"},
+ {"images": ["examples/A.jpeg"], "prompt": "Rotate the camera 45 degrees to the right.", "lora": "Multiple-Angles"},
+ {"images": ["examples/U.jpg"], "prompt": "Upscale this picture to 4K resolution.", "lora": "Upscaler"},
+ {"images": ["examples/L1.jpg", "examples/L2.jpg"], "prompt": "Apply the lighting from image 2 to image 1.", "lora": "Any-light"},
+ {"images": ["examples/PP1.jpg"], "prompt": "cinematic polaroid with soft grain subtle vignette gentle lighting white frame handwritten photographed preserving realistic texture and details.", "lora": "Polaroid-Photo"},
+ {"images": ["examples/Z1.jpg"], "prompt": "Front-right quarter view.", "lora": "Fal-Multiple-Angles"},
+ {"images": ["examples/URP.jpg"], "prompt": "Transform into a cinematic flat log.", "lora": "Cinematic-FlatLog"},
+ {"images": ["examples/SL.jpg"], "prompt": "Neutral uniform lighting. Preserve identity and composition.", "lora": "Studio-DeLight"},
+ {"images": ["examples/PI.jpg"], "prompt": "Transform it into Pixar-inspired 3D.", "lora": "Pixar-Inspired-3D"},
+ {"images": ["examples/MT.jpg"], "prompt": "Paint with manga tone.", "lora": "Manga-Tone"},
+ {"images": ["examples/NCB.jpg"], "prompt": "Transform into a noir comic book style.", "lora": "Noir-Comic-Book"},
+ {"images": ["examples/URP.jpg"], "prompt": "Ultra-realistic portrait.", "lora": "Ultra-Realistic-Portrait"},
+ {"images": ["examples/MN.jpg"], "prompt": "Transform into Midnight Noir Eyes Spotlight.", "lora": "Midnight-Noir-Eyes-Spotlight"},
+ {"images": ["examples/ST1.jpg", "examples/ST2.jpg"], "prompt": "Convert Image 1 to the style of Image 2.", "lora": "Style-Transfer"},
+ {"images": ["examples/R1.jpg"], "prompt": "Change the picture to realistic photograph.", "lora": "Anything2Real"},
+ {"images": ["examples/UA.jpeg"], "prompt": "Unblur and upscale.", "lora": "Unblur-Anything"},
+ {"images": ["examples/L1.jpg", "examples/L2.jpg"], "prompt": "Refer to the color tone, remove the original lighting from Image 1, and relight Image 1 based on the lighting and color tone of Image 2.", "lora": "Light-Migration"},
+ {"images": ["examples/P1.jpg"], "prompt": "Transform into anime (while preserving the background and remaining elements maintaining realism and original details.)", "lora": "Anime-V2"},
+]
+
+
+def make_thumb_b64(path, max_dim=220):
+ if not os.path.exists(path):
+ return ""
+ try:
+ img = Image.open(path).convert("RGB")
+ img.thumbnail((max_dim, max_dim), LANCZOS)
+ buf = BytesIO()
+ img.save(buf, format="JPEG", quality=65)
+ return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
+ except Exception as e:
+ print(f"Thumbnail error for {path}: {e}")
+ return ""
+
+
+def encode_full_image(path):
+ if not os.path.exists(path):
+ return ""
+ try:
+ with open(path, "rb") as f:
+ data = f.read()
+ ext = path.rsplit(".", 1)[-1].lower()
+ mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp"}.get(ext, "image/jpeg")
+ return f"data:{mime};base64,{base64.b64encode(data).decode()}"
+ except Exception as e:
+ print(f"Encode error for {path}: {e}")
+ return ""
+
+
+def build_example_cards_html():
+ cards = ""
+ for i, ex in enumerate(EXAMPLES_CONFIG):
+ thumbs_html = ""
+ for path in ex["images"]:
+ thumb = make_thumb_b64(path)
+ if thumb:
+ thumbs_html += f'
'
+ else:
+ thumbs_html += '
Preview
'
+ n = len(ex["images"])
+ img_badge = f'{n} image{"s" if n > 1 else ""}'
+ lora_badge = html_lib.escape(ex["lora"])
+ prompt_short = html_lib.escape(ex["prompt"][:85])
+ if len(ex["prompt"]) > 85:
+ prompt_short += "…"
+ cards += f'''
+
{thumbs_html}
+
+ {img_badge}
+ {lora_badge}
+
+
{prompt_short}
+
'''
+ return cards
+
+
+def load_example_data(idx_str):
+ try:
+ idx = int(float(idx_str)) if idx_str and idx_str.strip() else -1
+ except (ValueError, TypeError):
+ idx = -1
+ if idx < 0 or idx >= len(EXAMPLES_CONFIG):
+ return json.dumps({"images": [], "prompt": "", "lora": "", "names": [], "status": "error"})
+ ex = EXAMPLES_CONFIG[idx]
+ b64_list, names = [], []
+ for path in ex["images"]:
+ b64 = encode_full_image(path)
+ if b64:
+ b64_list.append(b64)
+ names.append(os.path.basename(path))
+ return json.dumps({"images": b64_list, "prompt": ex["prompt"], "lora": ex["lora"], "names": names, "status": "ok"})
+
+
+print("Building example thumbnails…")
+EXAMPLE_CARDS_HTML = build_example_cards_html()
+print(f"Built {len(EXAMPLES_CONFIG)} example cards.")
+
+
+def b64_to_pil_list(b64_json_str):
+ if not b64_json_str or b64_json_str.strip() in ("", "[]"):
+ return []
+ try:
+ b64_list = json.loads(b64_json_str)
+ except Exception:
+ return []
+ pil_images = []
+ for b64_str in b64_list:
+ if not b64_str or not isinstance(b64_str, str):
+ continue
+ try:
+ if b64_str.startswith("data:image"):
+ _, data = b64_str.split(",", 1)
+ else:
+ data = b64_str
+ image_data = base64.b64decode(data)
+ pil_images.append(Image.open(BytesIO(image_data)).convert("RGB"))
+ except Exception as e:
+ print(f"Error decoding image: {e}")
+ return pil_images
+
+
+def update_dimensions_on_upload(image):
+ if image is None:
+ return 1024, 1024
+ w, h = image.size
+ if w > h:
+ nw = 1024
+ nh = int(nw * h / w)
+ else:
+ nh = 1024
+ nw = int(nh * w / h)
+ return (nw // 8) * 8, (nh // 8) * 8
+
+
+@spaces.GPU(size="xlarge")
+def infer(
+ images_b64_json,
+ prompt,
+ lora_adapter,
+ seed,
+ randomize_seed,
+ guidance_scale,
+ steps,
+ lora_scale=1.0,
+ progress=gr.Progress(track_tqdm=True),
+):
+ gc.collect()
+ torch.cuda.empty_cache()
+
+ pil_images = b64_to_pil_list(images_b64_json)
+ if not pil_images:
+ raise gr.Error("Please upload at least one image to edit.")
+ if not prompt or prompt.strip() == "":
+ raise gr.Error("Please enter an edit prompt.")
+
+ spec = ADAPTER_SPECS.get(lora_adapter)
+ if not spec:
+ raise gr.Error(f"Configuration not found for: {lora_adapter}")
+
+ adapter_name = spec["adapter_name"]
+ if adapter_name not in LOADED_ADAPTERS:
+ print(f"--- Downloading and Loading Adapter: {lora_adapter} ---")
+ try:
+ pipe.load_lora_weights(spec["repo"], weight_name=spec["weights"], adapter_name=adapter_name)
+ LOADED_ADAPTERS.add(adapter_name)
+ except Exception as e:
+ raise gr.Error(f"Failed to load adapter {lora_adapter}: {e}")
+ else:
+ print(f"--- Adapter {lora_adapter} already loaded. ---")
+
+ pipe.set_adapters([adapter_name], adapter_weights=[float(lora_scale)])
+
+ if randomize_seed:
+ seed = random.randint(0, MAX_SEED)
+
+ generator = torch.Generator(device=device).manual_seed(seed)
+ 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"
+ )
+ width, height = update_dimensions_on_upload(pil_images[0])
+
+ try:
+ result_image = pipe(
+ image=pil_images,
+ prompt=prompt,
+ negative_prompt=negative_prompt,
+ height=height,
+ width=width,
+ num_inference_steps=steps,
+ generator=generator,
+ true_cfg_scale=guidance_scale,
+ ).images[0]
+ return result_image, seed
+ except Exception as e:
+ raise e
+ finally:
+ gc.collect()
+ torch.cuda.empty_cache()
+
+
+css = r"""
+@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');
+
+*{box-sizing:border-box;margin:0;padding:0}
+
+:root{
+ --or: #FF4500;
+ --or-dim: #CC3700;
+ --or-bright:#FF6A33;
+ --or-glow: rgba(255,69,0,.28);
+ --or-soft: rgba(255,69,0,.12);
+ --or-xsoft: rgba(255,69,0,.06);
+ --bg: #0d0d0f;
+ --bg1: #141416;
+ --bg2: #1a1a1d;
+ --bg3: #222226;
+ --border: #2a2a2e;
+ --border2: #333338;
+ --text: #e8e8ec;
+ --text2: #9898a8;
+ --text3: #555560;
+ --mono: 'JetBrains Mono', monospace;
+}
+
+html, body, .gradio-container {
+ color-scheme: dark !important;
+ background: #0d0d0f !important;
+}
+
+body, .gradio-container {
+ font-family: 'Inter', system-ui, -apple-system, sans-serif !important;
+ font-size: 14px !important;
+ color: #e8e8ec !important;
+ min-height: 100vh;
+ background: #0d0d0f !important;
+}
+
+footer { display: none !important; }
+.hidden-input { display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important; }
+
+#example-load-btn, #gradio-run-btn {
+ position: absolute !important;
+ left: -9999px !important;
+ top: -9999px !important;
+ width: 1px !important;
+ height: 1px !important;
+ opacity: .01 !important;
+ pointer-events: none !important;
+ overflow: hidden !important;
+}
+
+/* ── App shell ─────────────────────────────────────────────────────────────── */
+.app-shell {
+ background: #141416 !important;
+ border: 1px solid #2a2a2e !important;
+ border-radius: 18px;
+ margin: 12px auto;
+ max-width: 1440px;
+ overflow: hidden;
+ box-shadow:
+ 0 32px 64px -16px rgba(0,0,0,.8),
+ 0 0 0 1px rgba(255,255,255,.03),
+ 0 0 60px -20px rgba(255,69,0,.2);
+}
+
+/* ── Header ────────────────────────────────────────────────────────────────── */
+.app-header {
+ background: #1a1a1d !important;
+ border-bottom: 1px solid #2a2a2e !important;
+ padding: 14px 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 12px;
+}
+.app-header-left { display:flex; align-items:center; gap:12px; }
+.app-logo {
+ width: 38px; height: 38px;
+ background: linear-gradient(135deg, #FF4500, #FF6A33);
+ border-radius: 11px;
+ display: flex; align-items: center; justify-content: center;
+ box-shadow: 0 4px 14px rgba(255,69,0,.3);
+ flex-shrink: 0;
+}
+.app-logo svg { width:22px; height:22px; fill:#fff; }
+.app-title {
+ font-size: 18px; font-weight: 800; letter-spacing: -.4px;
+ background: linear-gradient(135deg, #fff, #aaa);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ font-family: 'Inter', sans-serif;
+}
+.app-badge {
+ font-size: 11px; font-weight: 700; padding: 3px 10px;
+ border-radius: 20px; letter-spacing: .3px;
+ background: rgba(255,69,0,.12);
+ color: #FF6A33 !important;
+ -webkit-text-fill-color: #FF6A33 !important;
+ border: 1px solid rgba(255,69,0,.3);
+ font-family: 'Inter', sans-serif;
+}
+.app-badge.fast {
+ background: rgba(34,197,94,.1);
+ color: #4ade80 !important;
+ -webkit-text-fill-color: #4ade80 !important;
+ border: 1px solid rgba(34,197,94,.25);
+}
+
+/* ── GitHub button - highlighted, always same in light & dark ────────────── */
+.gh-btn {
+ display: inline-flex !important;
+ align-items: center !important;
+ gap: 7px !important;
+ padding: 7px 14px !important;
+ border-radius: 8px !important;
+ text-decoration: none !important;
+ font-family: 'Inter', sans-serif !important;
+ font-size: 13px !important;
+ font-weight: 700 !important;
+ letter-spacing: .1px !important;
+ /* Fixed colors - never change with theme */
+ background: #FF4500 !important;
+ color: #ffffff !important;
+ -webkit-text-fill-color: #ffffff !important;
+ border: 1px solid rgba(255,255,255,.15) !important;
+ box-shadow:
+ 0 2px 8px rgba(255,69,0,.4),
+ 0 1px 0 rgba(255,255,255,.1) inset !important;
+ transition: transform .15s ease, box-shadow .15s ease, background .15s ease !important;
+ cursor: pointer !important;
+ flex-shrink: 0 !important;
+}
+.gh-btn:hover {
+ background: #FF6A33 !important;
+ color: #ffffff !important;
+ -webkit-text-fill-color: #ffffff !important;
+ transform: translateY(-1px) !important;
+ box-shadow:
+ 0 4px 16px rgba(255,69,0,.55),
+ 0 1px 0 rgba(255,255,255,.12) inset !important;
+}
+.gh-btn:active {
+ background: #CC3700 !important;
+ transform: translateY(0) !important;
+ box-shadow: 0 1px 4px rgba(255,69,0,.3) !important;
+}
+.gh-btn svg {
+ fill: #ffffff !important;
+ flex-shrink: 0;
+ width: 15px !important;
+ height: 15px !important;
+}
+.gh-btn span {
+ color: #ffffff !important;
+ -webkit-text-fill-color: #ffffff !important;
+}
+
+/* Lock GitHub button against ANY browser/OS theme override */
+@media (prefers-color-scheme: light) {
+ .gh-btn {
+ background: #FF4500 !important;
+ color: #ffffff !important;
+ -webkit-text-fill-color: #ffffff !important;
+ border-color: rgba(255,255,255,.15) !important;
+ }
+ .gh-btn:hover {
+ background: #FF6A33 !important;
+ color: #ffffff !important;
+ -webkit-text-fill-color: #ffffff !important;
+ }
+ .gh-btn svg { fill: #ffffff !important; }
+ .gh-btn span { color: #ffffff !important; -webkit-text-fill-color: #ffffff !important; }
+}
+@media (prefers-color-scheme: dark) {
+ .gh-btn {
+ background: #FF4500 !important;
+ color: #ffffff !important;
+ -webkit-text-fill-color: #ffffff !important;
+ }
+ .gh-btn:hover { background: #FF6A33 !important; }
+ .gh-btn svg { fill: #ffffff !important; }
+ .gh-btn span { color: #ffffff !important; -webkit-text-fill-color: #ffffff !important; }
+}
+
+/* ── Toolbar ───────────────────────────────────────────────────────────────── */
+.app-toolbar {
+ background: #141416 !important;
+ border-bottom: 1px solid #2a2a2e !important;
+ padding: 7px 16px;
+ display: flex; gap: 4px; align-items: center; flex-wrap: wrap;
+}
+.tb-sep { width:1px; height:28px; background:#2a2a2e; margin:0 8px; }
+.modern-tb-btn {
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
+ min-width: 32px; height: 34px;
+ background: transparent; border: 1px solid transparent;
+ border-radius: 8px; cursor: pointer;
+ font-size: 13px; font-weight: 700; padding: 0 12px;
+ font-family: 'Inter', sans-serif;
+ color: #ffffff !important;
+ -webkit-text-fill-color: #ffffff !important;
+ transition: all .15s ease;
+}
+.modern-tb-btn:hover { background: rgba(255,69,0,.12); border-color: rgba(255,69,0,.4); }
+.modern-tb-btn:active { background: rgba(255,69,0,.22); border-color: #FF4500; }
+.modern-tb-btn .tb-label { font-size:13px; color:#fff!important; -webkit-text-fill-color:#fff!important; font-weight:700; }
+.modern-tb-btn .tb-svg { width:15px; height:15px; flex-shrink:0; }
+.modern-tb-btn .tb-svg, .modern-tb-btn .tb-svg * { stroke:#fff!important; fill:none!important; }
+.tb-info { font-family:var(--mono); font-size:12px; color:#555560; padding:0 8px; display:flex; align-items:center; }
+
+@media (prefers-color-scheme: light) {
+ .modern-tb-btn { color:#fff!important; -webkit-text-fill-color:#fff!important; }
+ .modern-tb-btn .tb-label { color:#fff!important; -webkit-text-fill-color:#fff!important; }
+ .modern-tb-btn .tb-svg, .modern-tb-btn .tb-svg * { stroke:#fff!important; }
+}
+
+/* ── Main layout ───────────────────────────────────────────────────────────── */
+.app-main-row { display:flex; gap:0; flex:1; overflow:hidden; }
+.app-main-left { flex:1; display:flex; flex-direction:column; min-width:0; border-right:1px solid #2a2a2e; }
+.app-main-right { width:440px; display:flex; flex-direction:column; flex-shrink:0; background:#141416!important; }
+
+/* ── Drop zone ─────────────────────────────────────────────────────────────── */
+#gallery-drop-zone { position:relative; background:#09090b!important; min-height:440px; overflow:auto; }
+#gallery-drop-zone.drag-over { outline:2px solid #FF4500; outline-offset:-2px; background:rgba(255,69,0,.05)!important; }
+
+.upload-prompt-modern { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); z-index:20; }
+.upload-click-area {
+ display:flex; flex-direction:column; align-items:center; justify-content:center;
+ cursor:pointer; padding:36px 52px; border:2px dashed #333338; border-radius:16px;
+ background:rgba(255,69,0,.05); transition:all .2s ease; gap:8px;
+}
+.upload-click-area:hover { background:rgba(255,69,0,.1); border-color:#FF4500; transform:scale(1.03); }
+.upload-click-area:active { background:rgba(255,69,0,.15); transform:scale(.98); }
+.upload-click-area svg { width:80px; height:80px; }
+.upload-main-text { color:#9898a8; font-size:14px; font-weight:600; margin-top:4px; font-family:'Inter',sans-serif; }
+.upload-sub-text { color:#555560; font-size:12px; font-weight:400; text-align:center; max-width:280px; line-height:1.5; font-family:'Inter',sans-serif; }
+
+/* ── Gallery grid ──────────────────────────────────────────────────────────── */
+.image-gallery-grid {
+ display:grid; grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
+ gap:12px; padding:16px; align-content:start;
+}
+.gallery-thumb {
+ position:relative; aspect-ratio:1; border-radius:10px; overflow:hidden;
+ cursor:pointer; border:2px solid #2a2a2e; transition:all .2s ease; background:#1a1a1d;
+}
+.gallery-thumb:hover { border-color:#333338; transform:translateY(-2px); box-shadow:0 4px 12px rgba(0,0,0,.4); }
+.gallery-thumb.selected { border-color:#FF4500!important; box-shadow:0 0 0 3px rgba(255,69,0,.25); }
+.gallery-thumb img { width:100%; height:100%; object-fit:cover; }
+.thumb-badge {
+ position:absolute; top:6px; left:6px; background:#FF4500; color:#fff;
+ padding:2px 8px; border-radius:4px; font-family:var(--mono); font-size:11px; font-weight:600;
+}
+.thumb-remove {
+ position:absolute; top:6px; right:6px; width:24px; height:24px;
+ background:rgba(0,0,0,.75); color:#fff; border:1px solid rgba(255,255,255,.15);
+ border-radius:50%; cursor:pointer; display:none;
+ align-items:center; justify-content:center; font-size:12px; transition:all .15s; line-height:1;
+}
+.gallery-thumb:hover .thumb-remove { display:flex; }
+.thumb-remove:hover { background:#FF4500; border-color:#FF4500; }
+.gallery-add-card {
+ aspect-ratio:1; border-radius:10px; border:2px dashed #333338;
+ display:flex; flex-direction:column; align-items:center; justify-content:center;
+ cursor:pointer; transition:all .2s ease; background:rgba(255,69,0,.05); gap:4px;
+}
+.gallery-add-card:hover { border-color:#FF4500; background:rgba(255,69,0,.1); }
+.gallery-add-card .add-icon { font-size:28px; color:#555560; font-weight:300; }
+.gallery-add-card .add-text { font-size:12px; color:#555560; font-weight:600; font-family:'Inter',sans-serif; }
+
+/* ── Hint bar ──────────────────────────────────────────────────────────────── */
+.hint-bar {
+ background: rgba(255,69,0,.05) !important;
+ border-top:1px solid #2a2a2e; border-bottom:1px solid #2a2a2e;
+ padding:10px 20px; font-size:13px; color:#9898a8; line-height:1.7;
+ font-weight:400; font-family:'Inter',sans-serif;
+}
+.hint-bar b { color:#FF7A4D; font-weight:700; }
+.hint-bar kbd {
+ display:inline-block; padding:1px 6px; background:#222226;
+ border:1px solid #333338; border-radius:4px;
+ font-family:var(--mono); font-size:11px; color:#9898a8;
+}
+
+/* ── Suggestions ───────────────────────────────────────────────────────────── */
+.suggestions-section { border-top:1px solid #2a2a2e; padding:12px 16px; background:#141416!important; }
+.suggestions-title, .examples-title {
+ font-size:11px; font-weight:700; color:#555560;
+ text-transform:uppercase; letter-spacing:1px; margin-bottom:10px;
+ font-family:'Inter',sans-serif;
+}
+.suggestions-wrap { display:flex; flex-wrap:wrap; gap:6px; }
+.suggestion-chip {
+ display:inline-flex; align-items:center; gap:4px; padding:5px 13px;
+ background:rgba(255,69,0,.1); border:1px solid rgba(255,69,0,.25); border-radius:20px;
+ color:#FF7A4D; font-size:12px; font-weight:600; font-family:'Inter',sans-serif;
+ cursor:pointer; transition:all .15s; white-space:nowrap;
+}
+.suggestion-chip:hover { background:rgba(255,69,0,.2); border-color:#FF4500; color:#fff; transform:translateY(-1px); }
+
+/* ── Examples ──────────────────────────────────────────────────────────────── */
+.examples-section { border-top:1px solid #2a2a2e; padding:14px 16px 18px; background:#141416!important; }
+.examples-scroll { display:flex; gap:10px; overflow-x:auto; padding-bottom:10px; padding-top:2px; }
+.examples-scroll::-webkit-scrollbar { height:5px; }
+.examples-scroll::-webkit-scrollbar-track { background:#0d0d0f; border-radius:3px; }
+.examples-scroll::-webkit-scrollbar-thumb { background:#333338; border-radius:3px; }
+.examples-scroll::-webkit-scrollbar-thumb:hover { background:#CC3700; }
+.example-card {
+ flex-shrink:0; width:220px; background:#1a1a1d!important; border:1px solid #2a2a2e;
+ border-radius:12px; overflow:hidden; cursor:pointer; transition:all .2s ease;
+}
+.example-card:hover { border-color:#FF4500; transform:translateY(-3px); box-shadow:0 6px 20px rgba(255,69,0,.22); }
+.example-card.loading { opacity:.5; pointer-events:none; }
+.example-thumbs { display:flex; height:115px; overflow:hidden; background:#222226!important; }
+.example-thumbs img { flex:1; object-fit:cover; min-width:0; }
+.example-thumb-placeholder {
+ flex:1; display:flex; align-items:center; justify-content:center;
+ background:#222226!important; color:#555560; font-size:11px; min-width:0;
+}
+.example-meta { padding:7px 10px 3px; display:flex; align-items:center; gap:5px; flex-wrap:wrap; }
+.example-badge {
+ display:inline-flex; padding:2px 7px; background:rgba(255,69,0,.1); border-radius:4px;
+ font-size:10px; font-weight:700; color:#FF6A33; font-family:var(--mono);
+ white-space:nowrap; border:1px solid rgba(255,69,0,.2);
+}
+.example-lora-badge {
+ display:inline-flex; padding:2px 7px; background:rgba(255,255,255,.06); border-radius:4px;
+ font-size:10px; font-weight:600; color:#9898a8; font-family:var(--mono);
+ white-space:nowrap; border:1px solid #2a2a2e;
+ max-width:120px; overflow:hidden; text-overflow:ellipsis;
+}
+.example-prompt-text {
+ padding:2px 10px 10px; font-size:11.5px; color:#9898a8; line-height:1.45;
+ display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
+ overflow:hidden; font-weight:400; font-family:'Inter',sans-serif;
+}
+
+/* ── Right panel ───────────────────────────────────────────────────────────── */
+.panel-card { border-bottom:1px solid #2a2a2e; background:#141416!important; }
+.panel-card-title {
+ padding:11px 20px; font-size:11px; font-weight:700; color:#555560;
+ text-transform:uppercase; letter-spacing:1px;
+ border-bottom:1px solid rgba(42,42,46,.6);
+ background:#141416!important; font-family:'Inter',sans-serif;
+}
+.panel-card-body { padding:14px 18px; display:flex; flex-direction:column; gap:8px; background:#141416!important; }
+.modern-label { font-size:13px; font-weight:600; color:#9898a8; margin-bottom:4px; display:block; font-family:'Inter',sans-serif; }
+.modern-textarea {
+ width:100%; background:#09090b!important; border:1px solid #2a2a2e; border-radius:8px;
+ padding:10px 14px; font-family:'Inter',sans-serif; font-size:14px;
+ color:#e8e8ec!important; -webkit-text-fill-color:#e8e8ec!important;
+ resize:vertical; outline:none; min-height:44px; transition:border-color .2s; font-weight:400;
+}
+.modern-textarea:focus { border-color:#FF4500; box-shadow:0 0 0 3px rgba(255,69,0,.22); }
+.modern-textarea::placeholder { color:#555560!important; -webkit-text-fill-color:#555560!important; }
+.modern-textarea.error-flash {
+ border-color:#ef4444!important;
+ box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;
+ animation:shake .4s ease;
+}
+@keyframes shake {
+ 0%,100%{transform:translateX(0)}
+ 20%,60%{transform:translateX(-4px)}
+ 40%,80%{transform:translateX(4px)}
+}
+
+/* ── LoRA selector - always dark ───────────────────────────────────────────── */
+.lora-selector-card { border-bottom:1px solid #2a2a2e!important; background:#0d0d0f!important; }
+.lora-selector-body { padding:12px 18px!important; background:#0d0d0f!important; }
+.lora-select-label {
+ font-size:11px!important; font-weight:700!important;
+ color:#555560!important; -webkit-text-fill-color:#555560!important;
+ text-transform:uppercase!important; letter-spacing:1px!important;
+ margin-bottom:8px!important; display:flex!important; align-items:center!important;
+ gap:6px!important; font-family:'Inter',sans-serif!important;
+}
+.lora-select-label::before {
+ content:''; display:inline-block; width:8px; height:8px;
+ background:#FF4500; border-radius:50%; flex-shrink:0;
+}
+.lora-native-select {
+ width:100%!important; background:#09090b!important;
+ border:1px solid #333338!important; border-radius:8px!important;
+ padding:9px 36px 9px 14px!important;
+ font-family:'Inter',sans-serif!important;
+ font-size:13px!important; font-weight:600!important;
+ color:#e8e8ec!important; -webkit-text-fill-color:#e8e8ec!important;
+ outline:none!important; appearance:none!important; -webkit-appearance:none!important;
+ 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='%23FF4500' d='M6 8L1 3h10z'/%3E%3C/svg%3E")!important;
+ background-repeat:no-repeat!important; background-position:right 12px center!important;
+ cursor:pointer!important; transition:border-color .2s!important; color-scheme:dark!important;
+}
+.lora-native-select:focus { border-color:#FF4500!important; box-shadow:0 0 0 3px rgba(255,69,0,.22)!important; }
+.lora-native-select option { background:#1a1a1d!important; color:#e8e8ec!important; }
+
+@media (prefers-color-scheme: light) {
+ .lora-selector-card { background:#0d0d0f!important; }
+ .lora-selector-body { background:#0d0d0f!important; }
+ .lora-select-label { color:#555560!important; -webkit-text-fill-color:#555560!important; }
+ .lora-native-select {
+ background:#09090b!important; color:#e8e8ec!important;
+ -webkit-text-fill-color:#e8e8ec!important;
+ border-color:#333338!important; color-scheme:dark!important;
+ }
+ .lora-native-select option { background:#1a1a1d!important; color:#e8e8ec!important; }
+}
+
+/* ── Toast ─────────────────────────────────────────────────────────────────── */
+.toast-notification {
+ position:fixed; top:24px; left:50%;
+ transform:translateX(-50%) translateY(-120%);
+ z-index:9999; padding:10px 24px; border-radius:10px;
+ font-family:'Inter',sans-serif; font-size:14px; font-weight:700;
+ display:flex; align-items:center; gap:8px;
+ box-shadow:0 8px 24px rgba(0,0,0,.5);
+ transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;
+ opacity:0; pointer-events:none;
+}
+.toast-notification.visible { transform:translateX(-50%) translateY(0); opacity:1; pointer-events:auto; }
+.toast-notification.error { background:linear-gradient(135deg,#dc2626,#b91c1c); color:#fff; border:1px solid rgba(255,255,255,.15); }
+.toast-notification.warning { background:linear-gradient(135deg,#FF4500,#CC3700); color:#fff; border:1px solid rgba(255,255,255,.15); }
+.toast-notification.info { background:linear-gradient(135deg,#2563eb,#1d4ed8); color:#fff; border:1px solid rgba(255,255,255,.15); }
+
+/* ── Run button ────────────────────────────────────────────────────────────── */
+.btn-run {
+ display:flex; align-items:center; justify-content:center; gap:8px; width:100%;
+ background:linear-gradient(135deg,#FF4500,#CC3700); border:none; border-radius:10px;
+ padding:13px 24px; cursor:pointer; font-size:15px; font-weight:800;
+ font-family:'Inter',sans-serif;
+ color:#fff!important; -webkit-text-fill-color:#fff!important;
+ transition:all .2s ease; letter-spacing:.2px;
+ box-shadow:0 4px 20px rgba(255,69,0,.28),inset 0 1px 0 rgba(255,255,255,.12);
+}
+.btn-run:hover {
+ background:linear-gradient(135deg,#FF6A33,#FF4500); transform:translateY(-1px);
+ box-shadow:0 8px 28px rgba(255,69,0,.45),inset 0 1px 0 rgba(255,255,255,.15);
+}
+.btn-run:active { transform:translateY(0); box-shadow:0 2px 10px rgba(255,69,0,.28); }
+.btn-run svg { width:18px; height:18px; fill:#fff!important; }
+#custom-run-btn, #custom-run-btn *, #run-btn-label, .btn-run, .btn-run * {
+ color:#fff!important; -webkit-text-fill-color:#fff!important; fill:#fff!important;
+}
+
+/* ── Output ────────────────────────────────────────────────────────────────── */
+.output-frame { border-bottom:1px solid #2a2a2e; display:flex; flex-direction:column; position:relative; }
+.out-title {
+ padding:10px 20px; font-size:11px; font-weight:700;
+ color:#fff!important; -webkit-text-fill-color:#fff!important;
+ text-transform:uppercase; letter-spacing:1px;
+ border-bottom:1px solid rgba(42,42,46,.6);
+ display:flex; align-items:center; justify-content:space-between;
+ background:#141416!important; font-family:'Inter',sans-serif;
+}
+.out-body {
+ flex:1; background:#09090b!important;
+ display:flex; align-items:center; justify-content:center;
+ overflow:hidden; min-height:240px; position:relative;
+}
+.out-body img { max-width:100%; max-height:460px; image-rendering:auto; }
+.out-placeholder { color:#555560; font-size:13px; text-align:center; padding:20px; font-weight:500; font-family:'Inter',sans-serif; }
+.out-download-btn {
+ display:none; align-items:center; justify-content:center;
+ background:rgba(255,69,0,.12); border:1px solid rgba(255,69,0,.28); border-radius:6px;
+ cursor:pointer; padding:3px 10px; font-size:11px; font-weight:700;
+ color:#FF6A33!important; -webkit-text-fill-color:#FF6A33!important;
+ gap:4px; height:24px; transition:all .15s; font-family:'Inter',sans-serif;
+}
+.out-download-btn:hover {
+ background:#FF4500; border-color:#FF4500;
+ color:#fff!important; -webkit-text-fill-color:#fff!important;
+}
+.out-download-btn.visible { display:inline-flex; }
+.out-download-btn svg { width:12px; height:12px; fill:#FF6A33; }
+.out-download-btn:hover svg { fill:#fff; }
+
+/* ── Loader ────────────────────────────────────────────────────────────────── */
+.modern-loader {
+ display:none; position:absolute; top:0; left:0; right:0; bottom:0;
+ background:rgba(9,9,11,.93); z-index:15;
+ flex-direction:column; align-items:center; justify-content:center;
+ gap:16px; backdrop-filter:blur(4px);
+}
+.modern-loader.active { display:flex; }
+.modern-loader .loader-spinner {
+ width:36px; height:36px; border:3px solid #2a2a2e;
+ border-top-color:#FF4500; border-radius:50%; animation:spin .8s linear infinite;
+}
+@keyframes spin { to{transform:rotate(360deg)} }
+.modern-loader .loader-text { font-size:13px; color:#9898a8; font-weight:600; font-family:'Inter',sans-serif; }
+.loader-bar-track { width:200px; height:4px; background:#2a2a2e; border-radius:2px; overflow:hidden; }
+.loader-bar-fill {
+ height:100%; background:linear-gradient(90deg,#FF4500,#FF6A33,#FF4500);
+ background-size:200% 100%; animation:shimmer 1.5s ease-in-out infinite; border-radius:2px;
+}
+@keyframes shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
+
+/* ── Settings ──────────────────────────────────────────────────────────────── */
+.settings-group { border:1px solid #2a2a2e; border-radius:10px; margin:12px 16px; padding:0; overflow:hidden; background:#141416!important; }
+.settings-group-title {
+ font-size:11px; font-weight:700; color:#555560; text-transform:uppercase; letter-spacing:1px;
+ padding:9px 16px; border-bottom:1px solid #2a2a2e;
+ background:rgba(26,26,29,.5)!important; font-family:'Inter',sans-serif;
+}
+.settings-group-body { padding:14px 16px; display:flex; flex-direction:column; gap:12px; background:#141416!important; }
+.slider-row { display:flex; align-items:center; gap:10px; min-height:28px; }
+.slider-row label { font-size:13px; font-weight:600; color:#9898a8; min-width:72px; flex-shrink:0; font-family:'Inter',sans-serif; }
+.slider-row input[type="range"] {
+ flex:1; -webkit-appearance:none; appearance:none;
+ height:5px; background:#333338; border-radius:3px; outline:none; min-width:0;
+}
+.slider-row input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance:none; width:16px; height:16px;
+ background:linear-gradient(135deg,#FF4500,#CC3700);
+ border-radius:50%; cursor:pointer;
+ box-shadow:0 2px 6px rgba(255,69,0,.28); transition:transform .15s;
+}
+.slider-row input[type="range"]::-webkit-slider-thumb:hover { transform:scale(1.2); }
+.slider-row input[type="range"]::-moz-range-thumb {
+ width:16px; height:16px; background:linear-gradient(135deg,#FF4500,#CC3700);
+ border-radius:50%; cursor:pointer; border:none; box-shadow:0 2px 6px rgba(255,69,0,.28);
+}
+.slider-row .slider-val {
+ min-width:52px; text-align:right; font-family:var(--mono); font-size:12px;
+ font-weight:500; padding:3px 8px; background:#09090b; border:1px solid #2a2a2e;
+ border-radius:6px; color:#9898a8; flex-shrink:0;
+}
+.checkbox-row { display:flex; align-items:center; gap:8px; font-size:13px; color:#9898a8; }
+.checkbox-row input[type="checkbox"] { accent-color:#FF4500; width:16px; height:16px; cursor:pointer; }
+.checkbox-row label { color:#9898a8; font-size:13px; cursor:pointer; font-weight:500; font-family:'Inter',sans-serif; }
+
+/* ── Status bar ────────────────────────────────────────────────────────────── */
+.app-statusbar {
+ background:#141416!important; border-top:1px solid #2a2a2e;
+ padding:6px 20px; display:flex; gap:12px; height:34px; align-items:center; font-size:12px;
+}
+.app-statusbar .sb-section {
+ padding:0 12px; flex:1; display:flex; align-items:center;
+ font-family:var(--mono); font-size:12px; color:#555560;
+ overflow:hidden; white-space:nowrap;
+}
+.app-statusbar .sb-section.sb-fixed {
+ flex:0 0 auto; min-width:90px; text-align:center; justify-content:center;
+ padding:3px 12px; background:rgba(255,69,0,.1); border-radius:6px;
+ color:#FF6A33; font-weight:700; border:1px solid rgba(255,69,0,.2);
+}
+
+/* ── Footer ────────────────────────────────────────────────────────────────── */
+.exp-note {
+ padding:10px 20px; font-size:12px; color:#555560;
+ border-top:1px solid #2a2a2e; text-align:center; font-weight:500;
+ background:#141416!important; font-family:'Inter',sans-serif;
+}
+.exp-note a { color:#FF6A33!important; -webkit-text-fill-color:#FF6A33!important; text-decoration:none; }
+.exp-note a:hover { text-decoration:underline; color:#fff!important; -webkit-text-fill-color:#fff!important; }
+
+/* ── Scrollbars ────────────────────────────────────────────────────────────── */
+::-webkit-scrollbar { width:7px; height:7px; }
+::-webkit-scrollbar-track { background:#09090b; }
+::-webkit-scrollbar-thumb { background:#333338; border-radius:4px; }
+::-webkit-scrollbar-thumb:hover { background:#CC3700; }
+
+/* ── Responsive ────────────────────────────────────────────────────────────── */
+@media(max-width:860px){
+ .app-main-row { flex-direction:column; }
+ .app-main-right { width:100%; }
+ .app-main-left { border-right:none; border-bottom:1px solid #2a2a2e; }
+}
+"""
+
+gallery_js = r"""
+() => {
+function init() {
+ if (window.__qwenInitDone) return;
+
+ const galleryGrid = document.getElementById('image-gallery-grid');
+ const dropZone = document.getElementById('gallery-drop-zone');
+ const uploadPrompt = document.getElementById('upload-prompt');
+ const uploadClick = document.getElementById('upload-click-area');
+ const fileInput = document.getElementById('custom-file-input');
+ const btnUpload = document.getElementById('tb-upload');
+ const btnRemove = document.getElementById('tb-remove');
+ const btnClear = document.getElementById('tb-clear');
+ const promptInput = document.getElementById('custom-prompt-input');
+ const loraSelect = document.getElementById('custom-lora-select');
+ const runBtnEl = document.getElementById('custom-run-btn');
+ const imgCountTb = document.getElementById('tb-image-count');
+ const imgCountSb = document.getElementById('sb-image-count');
+
+ if (!galleryGrid || !fileInput || !dropZone) { setTimeout(init, 250); return; }
+ window.__qwenInitDone = true;
+
+ let images = [];
+ window.__uploadedImages = images;
+ let selectedIdx = -1;
+ let toastTimer = null;
+
+ /* ── Force dark styles on elements that browsers may override ── */
+ function enforceDarkStyles() {
+ /* LoRA card */
+ const loraCard = document.querySelector('.lora-selector-card');
+ const loraBody = document.querySelector('.lora-selector-body');
+ const loraLabel = document.querySelector('.lora-select-label');
+ const loraEl = document.getElementById('custom-lora-select');
+ if (loraCard) { loraCard.style.setProperty('background','#0d0d0f','important'); }
+ if (loraBody) { loraBody.style.setProperty('background','#0d0d0f','important'); }
+ if (loraLabel) {
+ loraLabel.style.setProperty('color','#555560','important');
+ loraLabel.style.setProperty('-webkit-text-fill-color','#555560','important');
+ }
+ if (loraEl) {
+ loraEl.style.setProperty('background-color','#09090b','important');
+ loraEl.style.setProperty('color','#e8e8ec','important');
+ loraEl.style.setProperty('-webkit-text-fill-color','#e8e8ec','important');
+ loraEl.style.setProperty('border-color','#333338','important');
+ }
+
+ /* GitHub button */
+ const ghBtn = document.querySelector('.gh-btn');
+ if (ghBtn) {
+ ghBtn.style.setProperty('background','#FF4500','important');
+ ghBtn.style.setProperty('color','#ffffff','important');
+ ghBtn.style.setProperty('-webkit-text-fill-color','#ffffff','important');
+ ghBtn.style.setProperty('border-color','rgba(255,255,255,.15)','important');
+ ghBtn.style.setProperty('box-shadow','0 2px 8px rgba(255,69,0,.4)','important');
+ const svg = ghBtn.querySelector('svg');
+ if (svg) svg.style.setProperty('fill','#ffffff','important');
+ const span = ghBtn.querySelector('span');
+ if (span) {
+ span.style.setProperty('color','#ffffff','important');
+ span.style.setProperty('-webkit-text-fill-color','#ffffff','important');
+ }
+ }
+
+ /* Shell sections */
+ const shell = document.querySelector('.app-shell');
+ const header = document.querySelector('.app-header');
+ const toolbar = document.querySelector('.app-toolbar');
+ if (shell) shell.style.setProperty('background','#141416','important');
+ if (header) header.style.setProperty('background','#1a1a1d','important');
+ if (toolbar) toolbar.style.setProperty('background','#141416','important');
+ }
+
+ enforceDarkStyles();
+ setInterval(enforceDarkStyles, 800);
+
+ /* GitHub button hover interaction */
+ const ghBtn = document.querySelector('.gh-btn');
+ if (ghBtn) {
+ ghBtn.addEventListener('mouseenter', () => {
+ ghBtn.style.setProperty('background','#FF6A33','important');
+ ghBtn.style.setProperty('color','#ffffff','important');
+ ghBtn.style.setProperty('-webkit-text-fill-color','#ffffff','important');
+ ghBtn.style.setProperty('transform','translateY(-1px)','important');
+ ghBtn.style.setProperty('box-shadow','0 4px 16px rgba(255,69,0,.55)','important');
+ });
+ ghBtn.addEventListener('mouseleave', () => {
+ ghBtn.style.setProperty('background','#FF4500','important');
+ ghBtn.style.setProperty('color','#ffffff','important');
+ ghBtn.style.setProperty('-webkit-text-fill-color','#ffffff','important');
+ ghBtn.style.setProperty('transform','translateY(0)','important');
+ ghBtn.style.setProperty('box-shadow','0 2px 8px rgba(255,69,0,.4)','important');
+ });
+ ghBtn.addEventListener('mousedown', () => {
+ ghBtn.style.setProperty('background','#CC3700','important');
+ ghBtn.style.setProperty('transform','translateY(0)','important');
+ });
+ ghBtn.addEventListener('mouseup', () => {
+ ghBtn.style.setProperty('background','#FF6A33','important');
+ });
+ }
+
+ function showToast(message, type) {
+ let toast = document.getElementById('app-toast');
+ if (!toast) {
+ toast = document.createElement('div');
+ toast.id = 'app-toast';
+ toast.className = 'toast-notification';
+ toast.innerHTML = '';
+ document.body.appendChild(toast);
+ }
+ const icon = toast.querySelector('.toast-icon');
+ const text = toast.querySelector('.toast-text');
+ toast.className = 'toast-notification ' + (type || 'error');
+ icon.textContent = type === 'warning' ? '\u26A0' : type === 'info' ? '\u2139' : '\u2717';
+ text.textContent = message;
+ if (toastTimer) clearTimeout(toastTimer);
+ void toast.offsetWidth;
+ toast.classList.add('visible');
+ toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500);
+ }
+ window.__showToast = showToast;
+
+ function flashPromptError() {
+ if (!promptInput) return;
+ promptInput.classList.add('error-flash');
+ promptInput.focus();
+ setTimeout(() => promptInput.classList.remove('error-flash'), 800);
+ }
+
+ function setGradioValue(containerId, value) {
+ const container = document.getElementById(containerId);
+ if (!container) return;
+ container.querySelectorAll('input, textarea').forEach(el => {
+ if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return;
+ const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
+ const ns = Object.getOwnPropertyDescriptor(proto, 'value');
+ if (ns && ns.set) {
+ ns.set.call(el, value);
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
+ }
+ });
+ }
+ window.__setGradioValue = setGradioValue;
+
+ function syncImagesToGradio() {
+ window.__uploadedImages = images;
+ const b64Array = images.map(img => img.b64);
+ setGradioValue('hidden-images-b64', JSON.stringify(b64Array));
+ updateCounts();
+ }
+ function syncPromptToGradio() {
+ if (promptInput) setGradioValue('prompt-gradio-input', promptInput.value);
+ }
+ function syncLoraToGradio() {
+ if (!loraSelect) return;
+ const container = document.getElementById('gradio-lora');
+ if (!container) return;
+ container.querySelectorAll('input').forEach(el => {
+ const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
+ if (ns && ns.set) {
+ ns.set.call(el, loraSelect.value);
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
+ }
+ });
+ }
+
+ function updateCounts() {
+ const n = images.length;
+ const txt = n > 0 ? n + ' image' + (n > 1 ? 's' : '') : 'No images';
+ if (imgCountTb) imgCountTb.textContent = txt;
+ if (imgCountSb) imgCountSb.textContent = n > 0 ? txt + ' uploaded' : 'No images uploaded';
+ }
+
+ function addImage(b64, name) {
+ images.push({id: Date.now() + Math.random(), b64, name});
+ renderGallery(); syncImagesToGradio();
+ }
+ window.__addImage = addImage;
+
+ function removeImage(idx) {
+ images.splice(idx, 1);
+ if (selectedIdx === idx) selectedIdx = -1;
+ else if (selectedIdx > idx) selectedIdx--;
+ renderGallery(); syncImagesToGradio();
+ }
+
+ function clearAll() {
+ images = []; window.__uploadedImages = images; selectedIdx = -1;
+ renderGallery(); syncImagesToGradio();
+ }
+ window.__clearAll = clearAll;
+
+ function renderGallery() {
+ if (images.length === 0) {
+ galleryGrid.innerHTML = ''; galleryGrid.style.display = 'none';
+ if (uploadPrompt) uploadPrompt.style.display = '';
+ return;
+ }
+ if (uploadPrompt) uploadPrompt.style.display = 'none';
+ galleryGrid.style.display = 'grid';
+ let html = '';
+ images.forEach((img, i) => {
+ const sel = i === selectedIdx ? ' selected' : '';
+ html += ''
+ + '

'
+ + '
#' + (i+1) + ''
+ + '
'
+ + '
';
+ });
+ html += '+Add
';
+ galleryGrid.innerHTML = html;
+ galleryGrid.querySelectorAll('.gallery-thumb').forEach(thumb => {
+ thumb.addEventListener('click', (e) => {
+ if (e.target.closest('.thumb-remove')) return;
+ selectedIdx = (selectedIdx === parseInt(thumb.dataset.idx)) ? -1 : parseInt(thumb.dataset.idx);
+ renderGallery();
+ });
+ });
+ galleryGrid.querySelectorAll('.thumb-remove').forEach(btn => {
+ btn.addEventListener('click', (e) => { e.stopPropagation(); removeImage(parseInt(btn.dataset.remove)); });
+ });
+ const addCard = document.getElementById('gallery-add-card');
+ if (addCard) addCard.addEventListener('click', () => fileInput.click());
+ }
+
+ function processFiles(files) {
+ Array.from(files).forEach(file => {
+ if (!file.type.startsWith('image/')) return;
+ const reader = new FileReader();
+ reader.onload = (e) => addImage(e.target.result, file.name);
+ reader.readAsDataURL(file);
+ });
+ }
+
+ fileInput.addEventListener('change', (e) => { processFiles(e.target.files); e.target.value = ''; });
+ if (uploadClick) uploadClick.addEventListener('click', () => fileInput.click());
+ if (btnUpload) btnUpload.addEventListener('click', () => fileInput.click());
+ if (btnRemove) btnRemove.addEventListener('click', () => { if (selectedIdx >= 0) removeImage(selectedIdx); });
+ if (btnClear) btnClear.addEventListener('click', clearAll);
+
+ dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
+ dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); });
+ dropZone.addEventListener('drop', (e) => {
+ e.preventDefault(); dropZone.classList.remove('drag-over');
+ if (e.dataTransfer.files.length) processFiles(e.dataTransfer.files);
+ });
+
+ if (promptInput) promptInput.addEventListener('input', syncPromptToGradio);
+ if (loraSelect) loraSelect.addEventListener('change', syncLoraToGradio);
+
+ window.__setPrompt = function(text) { if (promptInput) { promptInput.value = text; syncPromptToGradio(); } };
+ window.__setLora = function(lora) {
+ if (loraSelect) {
+ loraSelect.value = lora;
+ loraSelect.dispatchEvent(new Event('change', {bubbles:true}));
+ syncLoraToGradio();
+ }
+ };
+
+ document.querySelectorAll('.example-card[data-idx]').forEach(card => {
+ card.addEventListener('click', () => {
+ const idx = card.getAttribute('data-idx');
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
+ card.classList.add('loading');
+ showToast('Loading example\u2026', 'info');
+ setGradioValue('example-result-data', '');
+ setGradioValue('example-idx-input', idx);
+ setTimeout(() => {
+ const btn = document.getElementById('example-load-btn');
+ if (btn) { const b = btn.querySelector('button'); if (b) b.click(); else btn.click(); }
+ }, 150);
+ setTimeout(() => card.classList.remove('loading'), 12000);
+ });
+ });
+
+ function syncSlider(customId, gradioId) {
+ const slider = document.getElementById(customId);
+ const valSpan = document.getElementById(customId + '-val');
+ if (!slider) return;
+ slider.addEventListener('input', () => {
+ if (valSpan) valSpan.textContent = slider.value;
+ const container = document.getElementById(gradioId);
+ if (!container) return;
+ container.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => {
+ const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
+ if (ns && ns.set) {
+ ns.set.call(el, slider.value);
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
+ }
+ });
+ });
+ }
+ syncSlider('custom-seed', 'gradio-seed');
+ syncSlider('custom-guidance', 'gradio-guidance');
+ syncSlider('custom-steps', 'gradio-steps');
+
+ const randCheck = document.getElementById('custom-randomize');
+ if (randCheck) {
+ randCheck.addEventListener('change', () => {
+ const container = document.getElementById('gradio-randomize');
+ if (!container) return;
+ const cb = container.querySelector('input[type="checkbox"]');
+ if (cb && cb.checked !== randCheck.checked) cb.click();
+ });
+ }
+
+ function showLoader() {
+ const l = document.getElementById('output-loader');
+ if (l) l.classList.add('active');
+ const sb = document.querySelector('.sb-fixed');
+ if (sb) sb.textContent = 'Processing\u2026';
+ }
+ function hideLoader() {
+ const l = document.getElementById('output-loader');
+ if (l) l.classList.remove('active');
+ const sb = document.querySelector('.sb-fixed');
+ if (sb) sb.textContent = 'Done';
+ }
+ window.__showLoader = showLoader;
+ window.__hideLoader = hideLoader;
+
+ function validateBeforeRun() {
+ const promptVal = promptInput ? promptInput.value.trim() : '';
+ const hasImages = images.length > 0;
+ if (!hasImages && !promptVal) { showToast('Please upload an image and enter a prompt', 'error'); flashPromptError(); return false; }
+ if (!hasImages) { showToast('Please upload at least one image', 'error'); return false; }
+ if (!promptVal) { showToast('Please enter an edit prompt', 'warning'); flashPromptError(); return false; }
+ return true;
+ }
+
+ window.__clickGradioRunBtn = function() {
+ if (!validateBeforeRun()) return;
+ syncPromptToGradio(); syncImagesToGradio(); syncLoraToGradio(); showLoader();
+ setTimeout(() => {
+ const gradioBtn = document.getElementById('gradio-run-btn');
+ if (!gradioBtn) return;
+ const btn = gradioBtn.querySelector('button');
+ if (btn) btn.click(); else gradioBtn.click();
+ }, 200);
+ };
+
+ if (runBtnEl) runBtnEl.addEventListener('click', () => window.__clickGradioRunBtn());
+
+ renderGallery();
+ updateCounts();
+}
+init();
+}
+"""
+
+wire_outputs_js = r"""
+() => {
+function watchOutputs() {
+ const resultContainer = document.getElementById('gradio-result');
+ const outBody = document.getElementById('output-image-container');
+ const outPh = document.getElementById('output-placeholder');
+ const dlBtn = document.getElementById('dl-btn-output');
+
+ if (!resultContainer || !outBody) { setTimeout(watchOutputs, 500); return; }
+
+ if (dlBtn) {
+ dlBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const img = outBody.querySelector('img.modern-out-img');
+ if (img && img.src) {
+ const a = document.createElement('a');
+ a.href = img.src; a.download = 'qwen_edit_output.png';
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
+ }
+ });
+ }
+
+ function syncImage() {
+ const resultImg = resultContainer.querySelector('img');
+ if (resultImg && resultImg.src) {
+ if (outPh) outPh.style.display = 'none';
+ let existing = outBody.querySelector('img.modern-out-img');
+ if (!existing) {
+ existing = document.createElement('img');
+ existing.className = 'modern-out-img';
+ outBody.appendChild(existing);
+ }
+ if (existing.src !== resultImg.src) {
+ existing.src = resultImg.src;
+ if (dlBtn) dlBtn.classList.add('visible');
+ if (window.__hideLoader) window.__hideLoader();
+ }
+ }
+ }
+ const observer = new MutationObserver(syncImage);
+ observer.observe(resultContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']});
+ setInterval(syncImage, 800);
+}
+watchOutputs();
+
+function watchSeed() {
+ const seedContainer = document.getElementById('gradio-seed');
+ const seedSlider = document.getElementById('custom-seed');
+ const seedVal = document.getElementById('custom-seed-val');
+ if (!seedContainer || !seedSlider) { setTimeout(watchSeed, 500); return; }
+ function sync() {
+ const el = seedContainer.querySelector('input[type="range"],input[type="number"]');
+ if (el && el.value) { seedSlider.value = el.value; if (seedVal) seedVal.textContent = el.value; }
+ }
+ const obs = new MutationObserver(sync);
+ obs.observe(seedContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']});
+ setInterval(sync, 1000);
+}
+watchSeed();
+
+function watchExampleResults() {
+ const container = document.getElementById('example-result-data');
+ if (!container) { setTimeout(watchExampleResults, 500); return; }
+ let lastProcessed = '';
+
+ function checkResult() {
+ const el = container.querySelector('textarea') || container.querySelector('input');
+ if (!el) return;
+ const val = el.value;
+ if (!val || val === lastProcessed || val.length < 20) return;
+ try {
+ const data = JSON.parse(val);
+ if (data.status === 'ok' && data.images && data.images.length > 0) {
+ lastProcessed = val;
+ if (window.__clearAll) window.__clearAll();
+ if (window.__setPrompt && data.prompt) window.__setPrompt(data.prompt);
+ if (window.__setLora && data.lora) window.__setLora(data.lora);
+ data.images.forEach((b64, i) => {
+ if (b64 && window.__addImage) {
+ const name = (data.names && data.names[i]) ? data.names[i] : ('example_'+(i+1)+'.jpg');
+ window.__addImage(b64, name);
+ }
+ });
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
+ if (window.__showToast) window.__showToast('Example loaded \u2014 ' + data.images.length + ' image(s)', 'info');
+ } else if (data.status === 'error') {
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
+ if (window.__showToast) window.__showToast('Could not load example images', 'error');
+ }
+ } catch(e) { console.error('Example parse error:', e); }
+ }
+
+ const obs = new MutationObserver(checkResult);
+ obs.observe(container, {childList:true, subtree:true, characterData:true, attributes:true});
+ setInterval(checkResult, 500);
+}
+watchExampleResults();
+}
+"""
+
+ROCKET_LOGO_SVG = ''''''
+
+UPLOAD_SVG = ''
+REMOVE_SVG = ''
+CLEAR_SVG = ''
+DOWNLOAD_SVG = ''
+GITHUB_SVG = ''
+
+LORA_OPTIONS_HTML = "\n".join(
+ f''
+ for name in ADAPTER_NAMES
+)
+
+with gr.Blocks() as demo:
+
+ hidden_images_b64 = gr.Textbox(value="[]", elem_id="hidden-images-b64", elem_classes="hidden-input", container=False)
+ prompt = gr.Textbox(value="", elem_id="prompt-gradio-input", elem_classes="hidden-input", container=False)
+ lora_adapter = gr.Dropdown(choices=ADAPTER_NAMES, value="Photo-to-Anime", elem_id="gradio-lora", elem_classes="hidden-input", container=False)
+ seed = gr.Slider(minimum=0, maximum=MAX_SEED, step=1, value=0, elem_id="gradio-seed", elem_classes="hidden-input", container=False)
+ randomize_seed = gr.Checkbox(value=True, elem_id="gradio-randomize", elem_classes="hidden-input", container=False)
+ guidance_scale = gr.Slider(minimum=1.0, maximum=10.0, step=0.1, value=1.0, elem_id="gradio-guidance", elem_classes="hidden-input", container=False)
+ lora_scale_in = gr.Number(value=1.0, elem_id="gradio-lorascale", elem_classes="hidden-input", container=False)
+ steps = gr.Slider(minimum=1, maximum=50, step=1, value=4, elem_id="gradio-steps", elem_classes="hidden-input", container=False)
+ result = gr.Image(elem_id="gradio-result", elem_classes="hidden-input", container=False, format="png")
+
+ example_idx = gr.Textbox(value="", elem_id="example-idx-input", elem_classes="hidden-input", container=False)
+ example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False)
+ example_load_btn = gr.Button("Load Example", elem_id="example-load-btn")
+
+ gr.HTML(f"""
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Click or drag images here
+
Supports multiple images for reference-based editing and guided manipulation
+
+
+
+
+
+
+
+ Upload: Click or drag images ·
+ Multi-image: Upload multiple for reference editing ·
+ Remove deletes selected ·
+ Clear All removes everything
+
+
+
+
Quick Prompts
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Quick Examples — click to load
+
+ {EXAMPLE_CARDS_HTML}
+
+
+
+
+
+
+
+
+
+
Edit Instruction
+
+
+
+
+
+
+
+
+
Editing Style / LoRA
+
+
+
+
+
+
+
+
+
+
+ Output
+
+ {DOWNLOAD_SVG} Save
+
+
+
+
+
+
Processing image…
+
+
+
Result will appear here
+
+
+
+
+
+
+
+
+
+
+
+
No images uploaded
+
Ready
+
+
+
+ """)
+
+ run_btn = gr.Button("Run", elem_id="gradio-run-btn")
+
+ demo.load(fn=None, js=gallery_js)
+ demo.load(fn=None, js=wire_outputs_js)
+
+ run_btn.click(
+ fn=infer,
+ inputs=[hidden_images_b64, prompt, lora_adapter, seed, randomize_seed, guidance_scale, steps, lora_scale_in],
+ outputs=[result, seed],
+ js=r"""(imgs, p, la, s, rs, gs, st) => {
+ const images = window.__uploadedImages || [];
+ const b64Array = images.map(img => img.b64);
+ const imgsJson = JSON.stringify(b64Array);
+ const promptEl = document.getElementById('custom-prompt-input');
+ const loraEl = document.getElementById('custom-lora-select');
+ const promptVal = promptEl ? promptEl.value : p;
+ const loraVal = loraEl ? loraEl.value : la;
+ return [imgsJson, promptVal, loraVal, s, rs, gs, st];
+ }""",
+ )
+
+ example_load_btn.click(
+ fn=load_example_data,
+ inputs=[example_idx],
+ outputs=[example_result],
+ queue=False,
+ )
+
+if __name__ == "__main__":
+ demo.queue(max_size=50).launch(
+ css=css,
+ mcp_server=True,
+ ssr_mode=False,
+ show_error=True,
+ allowed_paths=["examples"],
+ )
\ No newline at end of file