import os import sys import logging import spaces import torch import gradio as gr import tempfile import numpy as np from datetime import datetime from pathlib import Path from PIL import Image from typing import List, Tuple from math import floor, ceil import json import base64 import io import time import random import gc import math from huggingface_hub import InferenceClient from diffusers import QwenImageEditPlusPipeline, FlowMatchEulerDiscreteScheduler from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3 from config_image_edit import TITLE, LOGO_HTML, CUSTOM_HEAD, HEADLINE_MD, SUBHEADLINE_MD, THEME, DEFAULT_PROMPT, PROMPT_PLACEHOLDER, DEFAULT_PROMPT_NEGATIVE, SYSTEM_PROMPT_EDIT, INFO_TAB_TEXT log = logging.getLogger(__name__) # MARK: GLOBAL CONSTANTS: # Define paths using pathlib.Path for consistency BASE_DIR = Path(__file__).resolve().parent RES = BASE_DIR / "_res" ASSETS = RES / "assets" EXAMPLES = BASE_DIR / "examples" IMAGE_CACHE = BASE_DIR / "image_cache" # Ensure the image cache directory exists IMAGE_CACHE.mkdir(exist_ok=True) # Set static paths for Gradio # gr.set_static_paths([str(IMAGE_CACHE), str(EXAMPLES), str(ASSETS), str(RES)]) gr.set_static_paths(paths=[IMAGE_CACHE, RES, EXAMPLES]) MAX_SEED = np.iinfo(np.int32).max MIN_IMAGE_SIZE = 256 MAX_IMAGE_SIZE = 2048 # ------------------ # MARK: Pipeline load # ------------------ 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, } scheduler = FlowMatchEulerDiscreteScheduler.from_config(scheduler_config) dtype = torch.bfloat16 device = "cuda" if torch.cuda.is_available() else "cpu" # Load the model pipeline pipe = QwenImageEditPlusPipeline.from_pretrained("Qwen/Qwen-Image-Edit-2511", scheduler=scheduler, torch_dtype=torch.bfloat16).to(device) pipe.set_progress_bar_config(disable=None) pipe.load_lora_weights("lightx2v/Qwen-Image-Edit-2511-Lightning", weight_name="Qwen-Image-Edit-2511-Lightning-8steps-V1.0-bf16.safetensors",adapter_name="lightning-8steps") pipe.load_lora_weights("lightx2v/Qwen-Image-Edit-2511-Lightning", weight_name="Qwen-Image-Edit-2511-Lightning-4steps-V1.0-bf16.safetensors",adapter_name="lightning-4steps") # Qwen-Image-Edit-Lightning-4steps-V1.0-bf16.safetensors # pipe.load_lora_weights("dx8152/Qwen-Edit-2509-Multiple-angles", weight_name="镜头转换.safetensors", adapter_name="multiple-angles") pipe.load_lora_weights("prithivMLmods/Qwen-Image-Edit-2511-Ultra-Realistic-Portrait", weight_name="URP_20.safetensors", adapter_name="ultra-realistic-portrait") # pipe.load_lora_weights("dx8152/Qwen-Image-Edit-2509-Relight", weight_name="Qwen-Edit-Relight.safetensors", adapter_name="relight") pipe.load_lora_weights("fal/Qwen-Image-Edit-2511-Multiple-Angles-LoRA", weight_name="qwen-image-edit-2511-multiple-angles-lora.safetensors", adapter_name="multiple-angles") # pipe.load_lora_weights("tlennon-ie/qwen-edit-skin", weight_name="qwen-edit-skin_1.1_000002750.safetensors", adapter_name="edit-skin") # pipe.load_lora_weights("lovis93/next-scene-qwen-image-lora-2509", weight_name="next-scene_lora-v2-3000.safetensors", adapter_name="next-scene") pipe.load_lora_weights("valiantcat/Qwen-Image-Edit-2511-Upscale2K", weight_name="qwen_image_edit_2511_upscale.safetensors", adapter_name="upscale-image") pipe.load_lora_weights("lilylilith/AnyPose", weight_name="2511-AnyPose-base-000006250.safetensors", adapter_name="anypose-base") pipe.load_lora_weights("lilylilith/AnyPose", weight_name="2511-AnyPose-helper-00006000.safetensors", adapter_name="anypose-helper") pipe.load_lora_weights("Alissonerdx/BFS-Best-Face-Swap", weight_name="bfs_head_v5_2511_original.safetensors", adapter_name="face-swap") pipe.load_lora_weights("wiikoo/Qwen-lora-nsfw", weight_name="loras/qwen_image_edit_remove-clothing_v1.0.safetensors", adapter_name="remove-clothing") pipe.load_lora_weights("wiikoo/Qwen-lora-nsfw", weight_name="loras/Qwen-NSFW-Beta5.safetensors", adapter_name="qwen-nsfw") # "Qwen_NSFW_Beta5" "loras/Qwen-NSFW-Beta5.safetensors", # pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3()) # pipe.fuse_lora() def normalize_images(image_data) -> List[Image.Image]: """ Erwartet eine Liste von Einträgen, die entweder - ein PIL‑Bild, - ein Tupel (PIL‑Bild, Beschreibung) oder - ein Tupel (Pfad, Beschreibung) sein können. Gibt eine Liste von PIL‑Bildern zurück, die alle auf die gleiche Größe zugeschnitten sind. """ MAX_HEIGHT = 2560 MAX_WIDTH = 1704 # 1. Alle Eingaben in PIL‑Bilder umwandeln images = [] for item in image_data: if isinstance(item, tuple): img_candidate = item[0] # erstes Element ist Bild oder Pfad else: img_candidate = item if isinstance(img_candidate, str): img = Image.open(img_candidate) elif hasattr(img_candidate, 'save'): # PIL‑Bild img = img_candidate else: continue # oder raise Exception images.append({ 'image': img, 'width': img.width, 'height': img.height, 'aspect_ratio': img.width / img.height }) if not images: return [] # 2. Referenzgröße bestimmen (Bild mit minimaler Höhe, wie ursprünglich) min_aspect_img = min(images, key=lambda x: x['height']) target_width = min_aspect_img['width'] target_width = floor(target_width / 2) * 2 target_height = min_aspect_img['height'] target_height = floor(target_height / 2) * 2 target_aspect = target_width / target_height # 3. Begrenzung der Zielgröße (wie im Original) if target_width > target_height: target_height = min(target_height, 1704) target_width = floor((int(target_height * target_aspect) / 2)) * 2 else: target_height = min(target_height, 1248) target_width = floor((int(target_height * target_aspect) / 2)) * 2 # 4. Alle Bilder skalieren und zuschneiden output_images = [] for img_data in images: img = img_data['image'] img_aspect = img.width / img.height if img_aspect > target_aspect: # Bild ist breiter → nach Höhe skalieren new_height = min(target_height, MAX_HEIGHT) new_width = int(new_height * img_aspect) else: # Bild ist höher → nach Breite skalieren new_width = min(target_width, MAX_WIDTH) new_height = int(new_width / img_aspect) img_resized = img.resize((new_width, new_height), Image.LANCZOS) # Zentrierten Ausschnitt berechnen left = (new_width - target_width) // 2 top = (new_height - target_height) // 2 right = left + target_width bottom = top + target_height img_cropped = img_resized.crop((left, top, right, bottom)) output_images.append(img_cropped.convert("RGB")) return output_images def polish_prompt_hf(original_prompt, img_list): # 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 original_prompt prompt = f"{SYSTEM_PROMPT_EDIT}\n\nUser Input: {original_prompt}\n\nRewritten Prompt:" system_prompt = "you are a helpful assistant, you should provide useful answers to users." try: # Initialize the client client = InferenceClient( provider="nebius", api_key=api_key, ) # Convert list of images to base64 data URLs image_urls = [] if img_list is not None: # Ensure img_list is actually a list if not isinstance(img_list, list): img_list = [img_list] for img in img_list: image_url = None # If img is a PIL Image if hasattr(img, 'save'): # Check if it's a PIL Image buffered = io.BytesIO() img.save(buffered, format="PNG") img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8') image_url = f"data:image/png;base64,{img_base64}" # If img is already a file path (string) elif isinstance(img, str): with open(img, "rb") as image_file: img_base64 = base64.b64encode(image_file.read()).decode('utf-8') image_url = f"data:image/png;base64,{img_base64}" else: print(f"Warning: Unexpected image type: {type(img)}, skipping...") continue if image_url: image_urls.append(image_url) # Build the content array with text first, then all images content = [ { "type": "text", "text": prompt } ] # Add all images to the content for image_url in image_urls: content.append({ "type": "image_url", "image_url": { "url": image_url } }) # Format the messages for the chat completions API messages = [ {"role": "system", "content": system_prompt}, { "role": "user", "content": content } ] # Call the API completion = client.chat.completions.create( model="Qwen/Qwen2.5-VL-72B-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 original_prompt def use_history_as_input(evt: gr.SelectData, history): if history and evt.index < len(history): im = history[evt.index][0] image_path = f"image_cache/{history[evt.index][1]}" im.save(image_path, format="PNG") return image_path # gr.update(value=history[evt.index][0]) return # gr.update() @spaces.GPU(duration=60) def infer( images, prompt, lora_adapter="Lightning-8steps", negative_prompt=" ", num_inference_steps=30, true_guidance_scale=2.0, seed=42, randomize_seed=False, rewrite_prompt=False, gallery=None, height=None, width=None, num_images_per_prompt=1, progress=gr.Progress(track_tqdm=True), ): if true_guidance_scale <= 2: negative_prompt = " " if randomize_seed: seed = random.randint(0, MAX_SEED) if lora_adapter == "Multiple-Angles": pipe.set_adapters(["lightning-8steps", "multiple-angles"], adapter_weights=[1.0, 1.0]) elif lora_adapter == "Ultra-Realistic-Portrait": pipe.set_adapters(["ultra-realistic-portrait", "lightning-8steps"], adapter_weights=[1.0, 1.0]) # elif lora_adapter == "Relight": pipe.set_adapters(["relight"], adapter_weights=[1.0]) # elif lora_adapter == "Multi-Angle-Lighting": pipe.set_adapters(["multi-angle-lighting"], adapter_weights=[1.0]) # elif lora_adapter == "Edit-Skin": pipe.set_adapters(["edit-skin"], adapter_weights=[1.0]) # elif lora_adapter == "Next-Scene": pipe.set_adapters(["next-scene"], adapter_weights=[1.0]) elif lora_adapter == "Upscale-Image": pipe.set_adapters(["lightning-8steps", "upscale-image"], adapter_weights=[1.0, 1.0]) elif lora_adapter == "Any-Pose": pipe.set_adapters(["lightning-8steps", "anypose-base", "anypose-helper"], adapter_weights=[1.0, 0.7, 0.7]) elif lora_adapter == "Face-Swap": pipe.set_adapters(["lightning-8steps", "face-swap"], adapter_weights=[1.0, 0.9]) elif lora_adapter == "Face-Swap-4-steps": pipe.set_adapters(["lightning-4steps", "face-swap"], adapter_weights=[1.0, 0.9]) elif lora_adapter == "Undress": pipe.set_adapters(["lightning-8steps", "remove-clothing", "qwen-nsfw"], adapter_weights=[1.0, 0.9, 0.9]) elif lora_adapter == "Undress-4steps": pipe.set_adapters(["lightning-4steps", "remove-clothing", "qwen-nsfw"], adapter_weights=[1.0, 0.9, 0.9]) elif lora_adapter == "Lightning-4steps": pipe.set_adapters(["lightning-4steps"], adapter_weights=[1.0]) elif lora_adapter == "Lightning-8steps": pipe.set_adapters(["lightning-8steps"], adapter_weights=[1.0]) generator = torch.Generator(device=device).manual_seed(seed) # Load input images into PIL Images _norm_images = normalize_images(images) pil_images = _norm_images # if images is not None: # for item in _norm_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: # continue if height==256 and width==256: height, width = None, None print(f"Calling pipeline with prompt: '{prompt}'") print(f"Negative Prompt: '{negative_prompt}'") print(f"Seed: {seed}, Steps: {num_inference_steps}, Guidance: {true_guidance_scale}, Size: {width}x{height}") # if rewrite_prompt and len(pil_images) > 0: # prompt = polish_prompt_hf(prompt, pil_images) # print(f"Rewritten Prompt: {prompt}") # 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[0] timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") image_path = IMAGE_CACHE / f"{timestamp}.png" try: image.save(image_path, format="PNG") except Exception as exc: raise gr.Error(f"Fehler beim Speichern des Bildes: {exc}") if gallery is None: gallery = [] gallery.insert(0, (image, str(f"{timestamp}.png"))) return str(image_path), seed, gallery # ------------------ # MARK: GRADIO UI # ------------------ with gr.Blocks(title=TITLE) as demo: _PROPMT_STATE = gr.State(value={'PREPROMPT': "", 'PROMPT': "", 'LORA': ""}) with gr.Row(elem_classes="row-logo"): gr.HTML( f"""{LOGO_HTML}""", elem_classes="logo-html", ) with gr.Tab(" Start"): with gr.Row(elem_classes="row-header"): gr.HTML( f"""

{HEADLINE_MD}

{SUBHEADLINE_MD}

lg Sebastian gib dem Space gerne ein

""", elem_classes="md-header", ) with gr.Row(elem_classes="row-main"): with gr.Column(elem_classes="col-input") as input_column: # INPUT DATA COLUMN: with gr.Column(elem_classes="storyboard-column"): with gr.Row(elem_classes="gradio-row storyboard-row", equal_height=True): with gr.Column(): gr.Markdown("# **** Eingabe Bilder", elem_classes="markdown-label") input_image_component = gr.Gallery(type="pil", label="Bilder", show_label=True, allow_preview=False, object_fit="cover", elem_classes="storyboard-image image-1", interactive=True) gr.HTML("""""", elem_classes="storyboard-arrow") with gr.Column(): gr.Markdown("# **** Prompt", elem_classes="markdown-label") prompt = gr.Textbox(value=DEFAULT_PROMPT, placeholder=PROMPT_PLACEHOLDER, label="Regieanweisung", show_label=False, elem_classes="regieanweisung-textbox", lines=10, max_lines=12, interactive=True, html_attributes=gr.InputHTMLAttributes(autocorrect="off", spellcheck=False)) with gr.Row(elem_classes="gradio-row"): lora_adapter = gr.Dropdown( choices=[ ("Standard Modus", "Lightning-8steps"), ("Lightning 4 Schritte", "Lightning-4steps"), ("Jede Pose", "Any-Pose"), ("Face Swap", "Face-Swap"), ("Face Swap 4 Schritte", "Face-Swap-4steps"), ("Upscale-Image", "Upscale-Image"), ("Multiple-Angles", "Multiple-Angles"), ("Realistisches Porträt", "Ultra-Realistic-Portrait"), ("Undress", "Undress"), ("Undress 4 Schritte", "Undress-4steps") ], value="Lightning-8steps", elem_classes="lora-adapter-dropdown", label="⚗️ Voreinstellungen", info="Wähle eine Voreinstellung für gezielte hochwertige Bearbeitung.", interactive=True ) steps_slider = gr.Slider( minimum=1, maximum=30, step=1, value=8, label="🚶‍➡️ Inferenzschritte", info="Anzahl der Iterationen, die das Modell zur Bildgenerierung durchläuft; mehr Schritte erhöhen Detailtreue, verlangsamen aber die Berechnung.", interactive=True, elem_classes="einstellungen-slider einstellungen-steps", ) with gr.Column(elem_classes="profi-einstellungen-column"): with gr.Accordion("⚙️ Profi-Einstellungen", elem_classes="profi-einstellungen-accordion", open=False): with gr.Row(elem_classes="profi-einstellungen-inner-row"): with gr.Column(elem_classes="profi-einstellungen-inner-column"): gr.Markdown("# **** Negative Anweisungen", elem_classes="markdown-label") negative_prompt_input = gr.Textbox(value=DEFAULT_PROMPT_NEGATIVE, label="Profi-Name", show_label=False, elem_classes="negative-prompt-textbox", lines=10, interactive=False, html_attributes=gr.InputHTMLAttributes(autocorrect="off", spellcheck=False)) with gr.Column(elem_classes="profi-einstellungen-inner-column"): with gr.Row(elem_classes="einstellungen-seed-row"): seed_input = gr.Number(value=42, minimum=0, maximum=MAX_SEED, step=1, label="🌱 Seed", info="Ein fester Ausgangswert, der die Zufallszahlen im Bildgenerierungsprozess steuert, damit das gleiche Ergebnis bei gleicher Eingabe reproduzierbar ist.", elem_classes="einstellungen-seed", elem_id="seed_input", interactive=False) randomize_seed_checkbox = gr.Checkbox(value=True, label="Zufallsgenerator", show_label=False, elem_classes="einstellungen-random-seed", interactive=True) guidance_scale_input = gr.Slider( minimum=0.0, maximum=10.0, step=0.1, value=2, label="🧐 Leitwert", info="Steuert, wie stark das Modell dem Prompt folgt; höhere Werte führen zu genauerer Bildgestaltung.", interactive=True, elem_classes="einstellungen-slider einstellungen-guidance-scale-1", ) rewrite_prompt = gr.Checkbox(label="✨ Prompt verbessern", value=False, elem_classes="einstellungen-rewrite-prompt", interactive=True) game_button = gr.Button("Bearbeiten ✨", variant="primary", elem_classes="gradio-button run-button", elem_id="gameBtn", size="lg") generate_button = gr.Button("Bearbeiten ✨", variant="primary", elem_classes="gradio-button run-button", elem_id="runBtn", size="lg") with gr.Column(elem_classes="output-column", visible=False) as output_column_1: with gr.Row(elem_classes="gradio-row", equal_height=True): with gr.Column(): gr.Markdown("# **** Ergebnis", elem_classes="markdown-label") result_image = gr.Image(label="Ergebnis", type="pil", show_label=False, buttons=["download", "fullscreen"], elem_classes="result-image", visible=True, interactive=False) with gr.Column(): gr.Markdown("# **** Verlauf", elem_classes="markdown-label") result_history = gr.Gallery([], label="Ergebnis", show_label=False, buttons=["download", "fullscreen"], object_fit="cover", elem_classes="result-gallery", interactive=False, allow_preview=False, type="pil") with gr.Tab(" Info", elem_classes="info-tab"): with gr.Column(elem_classes="info-column"): gr.Markdown(INFO_TAB_TEXT) randomize_seed_checkbox.change(fn=lambda x: gr.update(interactive=(not x)), inputs=[randomize_seed_checkbox], outputs=[seed_input], show_progress=False) guidance_scale_input.change(fn=lambda x: gr.update(interactive=(x > 2)), inputs=[guidance_scale_input], outputs=[negative_prompt_input], show_progress=False) result_history.select(fn=use_history_as_input, inputs=[result_history], outputs=[result_image], show_progress=False) ui_inputs = [input_image_component, prompt, lora_adapter, negative_prompt_input, steps_slider, guidance_scale_input, seed_input, randomize_seed_checkbox, rewrite_prompt, result_history] generate_button.click(fn=lambda: gr.Column(visible=True), outputs=[output_column_1], show_progress=False, scroll_to_output=True).then(fn=infer, inputs=ui_inputs, outputs=[result_image, seed_input, result_history], show_progress_on=[result_image, result_history], scroll_to_output=True).then( fn=None, outputs=generate_button, js="() => { document.getElementById('gameBtn').classList.remove('process-running'); }" ) def _remove(rem, string, replace=""): return re.sub(f".*{rem}\n?", replace, string, flags=re.S) def _lora_adapter_change(lora_adapter_input, prompt_input, _PROPMT_STATE_INPUT): ANY_POSE_PREPROMPT = """Make the person in image 1 do the exact same pose of the person in image 2. Changing the style and background of the image of the person in image 1 is undesirable, so don't do it. The new pose should be pixel accurate to the pose we are trying to copy. The position of the arms and head and legs should be the same as the pose we are trying to copy. Change the field of view and angle to match exactly image 2. Head tilt and eye gaze pose should match the person in image 2. Remove the background of image 2, and replace it with the background of image 1. Don't change the identity of the person in image 1, keep their appearance the same, it is undesirable to change their facical features or hair style. don't do it.\n """ ANY_POSE_PREPROMPT = ANY_POSE_PREPROMPT.replace(" ", "") FACE_SWAP_PREPROMPT = """head_swap: start with Picture 1 as the base image, keeping its lighting, environment, and background. Remove the head from Picture 1 completely and replace it with the head from Picture 2. FROM PICTURE 1 (strictly preserve): - Scene: lighting conditions, shadows, highlights, color temperature, environment, background - Head positioning: exact rotation angle, tilt, direction the head is facing - Expression: facial expression, micro-expressions, eye gaze direction, mouth position, emotion FROM PICTURE 2 (strictly preserve identity): - Facial structure: face shape, bone structure, jawline, chin - All facial features: eye color, eye shape, nose structure, lip shape and fullness, eyebrows - Hair: color, style, texture, hairline - Skin: texture, tone, complexion The replaced head must seamlessly match Picture 1's lighting and expression while maintaining the complete identity from Picture 2. High quality, photorealistic, sharp details, 4k.\n """ if _PROPMT_STATE_INPUT['PROMPT'] == "" and prompt_input != "": _PROPMT_STATE_INPUT['PROMPT'] = prompt_input if lora_adapter_input == "Any-Pose": _PROPMT_STATE_INPUT['PREPROMPT'] = ANY_POSE_PREPROMPT _PROPMT_STATE_INPUT['PROMPT'] = prompt_input return _PROPMT_STATE_INPUT if lora_adapter_input == "Face-Swap": _PROPMT_STATE_INPUT['PREPROMPT'] = FACE_SWAP_PREPROMPT _PROPMT_STATE_INPUT['PROMPT'] = prompt_input return _PROPMT_STATE_INPUT _PROPMT_STATE_INPUT['PREPROMPT'] = "" return _PROPMT_STATE_INPUT lora_adapter.change(fn=_lora_adapter_change, inputs=[lora_adapter, prompt, _PROPMT_STATE], outputs=[_PROPMT_STATE], show_progress=False) lora_adapter.change(lambda x: gr.update(value=4 if x == "Lightning-4steps" else 8), inputs=[lora_adapter], outputs=[steps_slider], show_progress=False) _PROPMT_STATE.change(lambda x: gr.update(value=f"{x['PREPROMPT']}{x['PROMPT']}"), inputs=[_PROPMT_STATE], outputs=[prompt], show_progress=False) if __name__ == "__main__": demo.launch(theme=THEME, head=CUSTOM_HEAD, css_paths=["_res/_custom_image_edit.css", "_res/miniGameButton.css"], footer_links=["gradio", "settings"])