import gradio as gr import numpy as np import random import torch import spaces import requests import io from PIL import Image from diffusers import FlowMatchEulerDiscreteScheduler from optimization_simple import optimize_pipeline_ from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3 from huggingface_hub import InferenceClient import math import os import base64 import json SYSTEM_PROMPT = ''' # Edit Instruction Rewriter You are a professional edit instruction rewriter. Your task is to generate a precise, concise, and visually achievable professional-level edit instruction based on the user-provided instruction and the image to be edited. Please strictly follow the rewriting rules below: ... (Giữ nguyên phần System Prompt để tiết kiệm không gian hiển thị) ... ''' # --- Prompt Enhancement using Hugging Face InferenceClient --- def polish_prompt_hf(prompt, img_list): """ Rewrites the prompt using a Hugging Face InferenceClient. """ # Ensure HF_TOKEN is set api_key = os.environ.get("HF_TOKEN") if not api_key: print("Warning: HF_TOKEN not set. Falling back to original prompt.") return prompt try: # Initialize the client prompt = f"{SYSTEM_PROMPT}\n\nUser Input: {prompt}\n\nRewritten Prompt:" client = InferenceClient( provider="novita", api_key=api_key, ) # Format the messages for the chat completions API sys_promot = "you are a helpful assistant, you should provide useful answers to users." messages = [ {"role": "system", "content": sys_promot}, {"role": "user", "content": []}] for img in img_list: messages[1]["content"].append( {"image": f"data:image/png;base64,{encode_image(img)}"}) messages[1]["content"].append({"text": f"{prompt}"}) completion = client.chat.completions.create( model="Qwen/Qwen3-Next-80B-A3B-Instruct", messages=messages, ) # Parse the response result = completion.choices[0].message.content # Try to extract JSON if present if '{"Rewritten"' in result: try: # Clean up the response result = result.replace('```json', '').replace('```', '') result_json = json.loads(result) polished_prompt = result_json.get('Rewritten', result) except: polished_prompt = result else: polished_prompt = result polished_prompt = polished_prompt.strip().replace("\n", " ") return polished_prompt except Exception as e: print(f"Error during API call to Hugging Face: {e}") # Fallback to original prompt if enhancement fails return prompt def encode_image(pil_image): import io buffered = io.BytesIO() pil_image.save(buffered, format="PNG") return base64.b64encode(buffered.getvalue()).decode("utf-8") # --- Model Loading --- dtype = torch.bfloat16 device = "cuda" if torch.cuda.is_available() else "cpu" # Scheduler configuration for Lightning scheduler_config = { "base_image_seq_len": 256, "base_shift": math.log(3), "invert_sigmas": False, "max_image_seq_len": 8192, "max_shift": math.log(3), "num_train_timesteps": 1000, "shift": 1.0, "shift_terminal": None, "stochastic_sampling": False, "time_shift_type": "exponential", "use_beta_sigmas": False, "use_dynamic_shifting": True, "use_exponential_sigmas": False, "use_karras_sigmas": False, } # Initialize scheduler with Lightning config scheduler = FlowMatchEulerDiscreteScheduler.from_config(scheduler_config) # Load the model pipeline pipe = QwenImageEditPlusPipeline.from_pretrained("Qwen/Qwen-Image-Edit-2509", scheduler=scheduler, torch_dtype=dtype).to(device) pipe.load_lora_weights( "lightx2v/Qwen-Image-Lightning", weight_name="Qwen-Image-Edit-2509/Qwen-Image-Edit-2509-Lightning-4steps-V1.0-fp32.safetensors" ) pipe.fuse_lora() # Apply the same optimizations from the first version pipe.transformer.__class__ = QwenImageTransformer2DModel pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3()) # --- Ahead-of-time compilation --- optimize_pipeline_(pipe, image=[Image.new("RGB", (1024, 1024)), Image.new("RGB", (1024, 1024))], prompt="prompt") # --- UI Constants and Helpers --- MAX_SEED = np.iinfo(np.int32).max # --- Main Inference Function (Modified to accept URL) --- @spaces.GPU(duration=40) def infer( images, image_url, # New parameter for URL prompt, seed=42, randomize_seed=False, true_guidance_scale=1.0, num_inference_steps=4, height=None, width=None, rewrite_prompt=False, num_images_per_prompt=1, progress=gr.Progress(track_tqdm=True), ): """ Generates an image using the local Qwen-Image diffusers pipeline. Accepts input via gallery upload OR direct URL. """ # Hardcode the negative prompt as requested negative_prompt = "Vibrant colors, overexposed, static, blurry details, subtitles, style, artwork, painting, image, still, overall grayish, worst quality, low quality, JPEG compression residue, ugly, incomplete, extra fingers, poorly drawn hands, poorly drawn face, deformed, disfigured, deformed limbs, fingers fused together, static image, cluttered background, three legs, many people in the background, walking backwards. " # Check Auth Key expected_key = os.environ.get("hf_key") if expected_key and expected_key not in prompt: print("❌ Invalid key.") return None, seed if expected_key: prompt = prompt.replace(expected_key, "") if randomize_seed: seed = random.randint(0, MAX_SEED) # Set up the generator for reproducibility generator = torch.Generator(device=device).manual_seed(seed) pil_images = [] # 1. Process Input from Gallery (Uploaded files) if images: for item in images: try: if isinstance(item[0], Image.Image): pil_images.append(item[0].convert("RGB")) elif isinstance(item[0], str): pil_images.append(Image.open(item[0]).convert("RGB")) elif hasattr(item, "name"): pil_images.append(Image.open(item.name).convert("RGB")) except Exception as e: print(f"Error loading gallery image: {e}") continue # 2. Process Input from URL if image_url and image_url.strip(): try: print(f"Downloading image from: {image_url}") response = requests.get(image_url.strip(), timeout=10) response.raise_for_status() url_img = Image.open(io.BytesIO(response.content)).convert("RGB") pil_images.append(url_img) except Exception as e: print(f"❌ Failed to load image from URL: {e}") # Optional: You could raise a gr.Error here if URL was critical # 3. Fallback to Default if no images found if not pil_images: default_path = os.path.join(os.path.dirname(__file__), "1.jpg") if os.path.exists(default_path): pil_images = [Image.open(default_path).convert("RGB")] print("Loaded default image: 1.jpg") else: raise gr.Error("No input images provided (upload or URL) and '1.jpg' not found.") if height==256 and width==256: height, width = None, None # Generate the image image = 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 image, seed # --- Examples and UI Layout --- examples = [] 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) as demo: with gr.Column(elem_id="col-container"): gr.HTML("""