| import gradio as gr |
| import numpy as np |
| import random |
| import torch |
| import spaces |
|
|
| from PIL import Image |
| from torchao.quantization import Float8DynamicActivationFloat8WeightConfig |
| from torchao.quantization import quantize_ |
| from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline |
| from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel |
| from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3 |
|
|
| import time |
|
|
|
|
| def update_history(new_images, history): |
| """Updates the history gallery with the new images.""" |
| time.sleep(0.5) |
| if history is None: |
| history = [] |
| if new_images is not None and len(new_images) > 0: |
| if not isinstance(history, list): |
| history = list(history) if history else [] |
| for img in new_images: |
| history.insert(0, img) |
| history = history[:20] |
| return history |
|
|
| def use_history_as_input(evt: gr.SelectData): |
| """Sets the selected history image into the Image 1 slot.""" |
| if evt.value is not None: |
| |
| return gr.update(value=evt.value) |
| return gr.update() |
|
|
| |
| dtype = torch.bfloat16 |
| device = "cuda" if torch.cuda.is_available() else "cpu" |
| BASE_MODEL_ID = "Qwen/Qwen-Image-Edit-2511" |
| BASE_MODEL_REVISION = "6f3ccc0b56e431dc6a0c2b2039706d7d26f22cb9" |
| ACCELERATED_TRANSFORMER_ID = "SeedOfEvil/Qwen-Rapid-AIO-v18-NSFW-diffusers" |
| ACCELERATED_TRANSFORMER_REVISION = "072c4e226778329021c469d0ed7281ffba55e494" |
|
|
| |
| |
| |
| try: |
| transformer = QwenImageTransformer2DModel.from_pretrained( |
| ACCELERATED_TRANSFORMER_ID, |
| subfolder="transformer", |
| revision=ACCELERATED_TRANSFORMER_REVISION, |
| torch_dtype=dtype, |
| device_map="cuda" if torch.cuda.is_available() else None, |
| ) |
| except Exception as exc: |
| raise RuntimeError( |
| "The pinned four-step accelerated transformer could not be loaded. " |
| "Generation is disabled rather than silently using an incompatible fallback." |
| ) from exc |
|
|
| pipe = QwenImageEditPlusPipeline.from_pretrained( |
| BASE_MODEL_ID, |
| revision=BASE_MODEL_REVISION, |
| transformer=transformer, |
| torch_dtype=dtype, |
| ) |
|
|
| del transformer |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
|
|
| |
| |
| |
| fp8_config = Float8DynamicActivationFloat8WeightConfig() |
| quantize_(pipe.transformer, fp8_config) |
| try: |
| quantize_(pipe.text_encoder, fp8_config) |
| except Exception as exc: |
| print( |
| f"Text-encoder FP8 quantization unavailable ({type(exc).__name__}); " |
| "continuing with the pinned text encoder.", |
| flush=True, |
| ) |
|
|
| pipe = pipe.to(device) |
|
|
| |
| |
| try: |
| pipe.transformer.__class__ = QwenImageTransformer2DModel |
| pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3()) |
| except Exception as exc: |
| print(f"FA3 attention processor unavailable; continuing with default attention: {exc}") |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| |
| |
| |
| |
| |
| |
|
|
| |
| MAX_SEED = np.iinfo(np.int32).max |
| MIN_INPUT_SIDE = 256 |
|
|
| def use_output_as_input(output_images): |
| """Move the first output image into the Image 1 slot.""" |
| if not output_images: |
| return gr.update() |
| first = output_images[0] |
| |
| path = first[0] if isinstance(first, (list, tuple)) else first |
| return gr.update(value=path) |
|
|
|
|
| |
| @spaces.GPU(duration=120) |
| def infer( |
| image_1, |
| image_2, |
| prompt, |
| seed=42, |
| randomize_seed=False, |
| true_guidance_scale=1.0, |
| num_inference_steps=4, |
| height=None, |
| width=None, |
| num_images_per_prompt=1, |
| progress=gr.Progress(track_tqdm=True), |
| ): |
| """ |
| Generates an image using the local Qwen-Image diffusers pipeline. |
| """ |
| |
| negative_prompt = " " |
|
|
| if randomize_seed: |
| seed = random.randint(0, MAX_SEED) |
|
|
| |
| generator = torch.Generator(device=device).manual_seed(seed) |
|
|
| |
| pil_images = [] |
| for img in (image_1, image_2): |
| if img is None: |
| continue |
| try: |
| if isinstance(img, str): |
| pil_images.append(Image.open(img).convert("RGB")) |
| elif isinstance(img, Image.Image): |
| pil_images.append(img.convert("RGB")) |
| elif hasattr(img, "name"): |
| pil_images.append(Image.open(img.name).convert("RGB")) |
| except Exception: |
| continue |
|
|
| if not pil_images: |
| raise gr.Error("Upload at least one valid input image before editing.") |
|
|
| for img in pil_images: |
| if min(img.size) < MIN_INPUT_SIDE: |
| raise gr.Error( |
| f"Input images must be at least {MIN_INPUT_SIDE}px on each side. " |
| "Very small source images can produce tiled or grid-like artifacts." |
| ) |
|
|
| print( |
| f"Starting generation: seed={seed}, steps={num_inference_steps}, " |
| f"guidance={true_guidance_scale}, size={width}x{height}, inputs={len(pil_images)}", |
| flush=True, |
| ) |
|
|
| |
| images_pil = pipe( |
| image=pil_images if len(pil_images) > 0 else None, |
| prompt=prompt, |
| height=height, |
| width=width, |
| negative_prompt=negative_prompt, |
| num_inference_steps=num_inference_steps, |
| generator=generator, |
| true_cfg_scale=true_guidance_scale, |
| num_images_per_prompt=num_images_per_prompt, |
| ).images |
|
|
| |
| return images_pil, seed, gr.update(visible=True) |
|
|
|
|
| |
| css = """ |
| #col-container { |
| margin: 0 auto; |
| max-width: 1024px; |
| } |
| #logo-title { |
| text-align: center; |
| } |
| #logo-title img { |
| width: 400px; |
| } |
| #edit_text{margin-top: -62px !important} |
| """ |
|
|
| with gr.Blocks(css=css, delete_cache=(3600, 86400)) as demo: |
| with gr.Column(elem_id="col-container"): |
| gr.HTML(""" |
| <div id="logo-title"> |
| <h1>Pro Realism Edit Studio 🎨</h1> |
| <h2 style="font-style: italic;color: #5b47d1">Rapid Edit ⚡</h2> |
| </div> |
| """) |
| gr.Markdown(""" |
| This demo uses [Qwen-Image-Edit-2511](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) with [Phr00t's Rapid-AIO v18](https://huggingface.co/Phr00t/Qwen-Image-Edit-Rapid-AIO) accelerated transformer + [AoT compilation & FA3](https://huggingface.co/blog/zerogpu-aoti) for fast 4-step inference. |
| |
| Upload an image and enter your prompt to edit it. The model will use your prompt exactly as provided. |
| """) |
| with gr.Row(): |
| with gr.Column(): |
| with gr.Row(): |
| image_1 = gr.Image(label="Image 1", type="filepath", interactive=True) |
| image_2 = gr.Image(label="Image 2 (optional)", type="filepath", interactive=True) |
|
|
| prompt = gr.Text( |
| label="Prompt 🪄", |
| show_label=True, |
| placeholder="Enter your prompt here...", |
| ) |
| run_button = gr.Button("Edit!", variant="primary") |
| |
| with gr.Accordion("Advanced Settings", open=False): |
| |
| |
| seed = gr.Slider( |
| label="Seed", |
| minimum=0, |
| maximum=MAX_SEED, |
| step=1, |
| value=0, |
| ) |
| |
| randomize_seed = gr.Checkbox(label="Randomize seed", value=True) |
| |
| with gr.Row(): |
| |
| true_guidance_scale = gr.Slider( |
| label="True guidance scale", |
| minimum=1.0, |
| maximum=10.0, |
| step=0.1, |
| value=1.0 |
| ) |
|
|
| num_inference_steps = gr.Slider( |
| label="Number of inference steps", |
| minimum=1, |
| maximum=40, |
| step=1, |
| value=4, |
| ) |
| |
| with gr.Column(): |
| result = gr.Gallery(label="Result", show_label=False, type="filepath") |
| with gr.Row(): |
| use_output_btn = gr.Button("↗️ Use as input", variant="secondary", size="sm", visible=False) |
|
|
| with gr.Row(visible=False): |
| gr.Markdown("### 📜 History") |
| clear_history_button = gr.Button("🗑️ Clear History", size="sm", variant="stop") |
| |
| history_gallery = gr.Gallery( |
| label="Click any image to use as input", |
| interactive=False, |
| show_label=True, |
| visible=False |
| ) |
|
|
|
|
|
|
| |
|
|
| gr.on( |
| triggers=[run_button.click, prompt.submit], |
| fn=infer, |
| inputs=[ |
| image_1, |
| image_2, |
| prompt, |
| seed, |
| randomize_seed, |
| true_guidance_scale, |
| num_inference_steps, |
| ], |
| outputs=[result, seed, use_output_btn], |
|
|
| ).then( |
| fn=update_history, |
| inputs=[result, history_gallery], |
| outputs=history_gallery, |
|
|
| ) |
|
|
| |
| use_output_btn.click( |
| fn=use_output_as_input, |
| inputs=[result], |
| outputs=[image_1] |
| ) |
|
|
| |
| history_gallery.select( |
| fn=use_history_as_input, |
| inputs=None, |
| outputs=[image_1], |
|
|
| ) |
| |
| clear_history_button.click( |
| fn=lambda: [], |
| inputs=None, |
| outputs=history_gallery, |
|
|
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|