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 += ''; + }); + html += ''; + 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""" +
+ + +
+
+ + Qwen-Image-Edit + 2511 + 4-Step Fast +
+ + + + {GITHUB_SVG} + GitHub + +
+ + +
+ + + +
+ No images +
+ + +
+ + +
+ + + +
+ 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
+
+
+ +
+
Advanced Settings
+
+
+ + + 0 +
+
+ + +
+
+ + + 1.0 +
+
+ + + 4 +
+
+
+ +
+
+ +
+ Experimental Space for + Qwen-Image-Edit-2511 + · LoRAs loaded lazily on first use +
+ +
+
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