import gradio as gr from diffusers import StableDiffusionPipeline, StableDiffusionImg2ImgPipeline from diffusers import StableDiffusionInpaintPipeline, AutoencoderKL from diffusers import DPMSolverMultistepScheduler, PNDMScheduler from controlnet_module import controlnet_processor import torch from PIL import Image, ImageDraw import time import os import tempfile import random import re # === OPTIMIERTE EINSTELLUNGEN === device = "cuda" if torch.cuda.is_available() else "cpu" torch_dtype = torch.float16 if device == "cuda" else torch.float32 IMG_SIZE = 512 MAX_IMAGE_SIZE = 4096 # Maximale Bildgröße für Verarbeitung print(f"Running on: {device}") # === MODELLKONFIGURATION (NUR 2 MODELLE) === MODEL_CONFIGS = { "runwayml/stable-diffusion-v1-5": { "name": "🏠 Stable Diffusion 1.5 (Universal)", "description": "Universal model, good all-rounder, reliable results", "requires_vae": False, "vae_model": "stabilityai/sd-vae-ft-mse", "recommended_steps": 35, "recommended_cfg": 7.5, "supports_fp16": True }, "SG161222/Realistic_Vision_V6.0_B1_noVAE": { "name": "👤 Realistic Vision V6.0 (Portraits)", "description": "Best for photorealistic faces, skin details, human portraits", "requires_vae": True, "vae_model": "stabilityai/sd-vae-ft-mse", "recommended_steps": 40, "recommended_cfg": 7.0, "supports_fp16": False } } # === SAFETENSORS KONFIGURATION === SAFETENSORS_MODELS = ["runwayml/stable-diffusion-v1-5"] # Aktuell ausgewähltes Modell (wird vom User gesetzt) current_model_id = "runwayml/stable-diffusion-v1-5" # === AUTOMATISCHE NEGATIVE PROMPT GENERIERUNG === def auto_negative_prompt(positive_prompt): """Generiert automatisch negative Prompts basierend auf dem positiven Prompt""" p = positive_prompt.lower() negatives = [] # Personen / Portraits if any(w in p for w in [ "person", "man", "woman", "face", "portrait", "team", "employee", "people", "crowd", "character", "figure", "human", "child", "baby", "girl", "boy", "lady", "gentleman", "fairy", "elf", "dwarf", "santa claus", "mermaid", "angel", "demon", "witch", "wizard", "creature", "being", "model", "actor", "actress", "celebrity", "avatar", "group"]): negatives.append( "blurry face, lowres face, deformed pupils, bad anatomy, malformed hands, extra fingers, uneven eyes, distorted face, " "unrealistic skin, mutated, ugly, disfigured, poorly drawn face, " "missing limbs, extra limbs, fused fingers, too many fingers, bad teeth, " "mutated hands, long neck, extra wings, multiple wings,grainy face, noisy face, " "compression artifacts, rendering artifacts, digital artifacts, overprocessed face, oversmoothed face " ) # Business / Corporate if any(w in p for w in ["office", "business", "team", "meeting", "corporate", "company", "workplace"]): negatives.append( "overexposed, oversaturated, harsh lighting, watermark, text, logo, brand" ) # Produkt / CGI if any(w in p for w in ["product", "packshot", "mockup", "render", "3d", "cgi", "packaging"]): negatives.append( "plastic texture, noisy, overly reflective surfaces, watermark, text, low poly" ) # Landschaft / Umgebung if any(w in p for w in ["landscape", "nature", "mountain", "forest", "outdoor", "beach", "sky"]): negatives.append( "blurry, oversaturated, unnatural colors, distorted horizon, floating objects" ) # Logos / Symbole if any(w in p for w in ["logo", "symbol", "icon", "typography", "badge", "emblem"]): negatives.append( "watermark, signature, username, text, writing, scribble, messy" ) # Architektur / Gebäude if any(w in p for w in ["building", "architecture", "house", "interior", "room", "facade"]): negatives.append( "deformed, distorted perspective, floating objects, collapsing structure" ) # Basis negative Prompts für alle Fälle base_negatives = "low quality, worst quality, blurry, jpeg artifacts, ugly, deformed" if negatives: return base_negatives + ", " + ", ".join(negatives) else: return base_negatives # === HILFSFUNKTION: KOORDINATEN SORTIEREN === def sort_coordinates(x1, y1, x2, y2): """Sortiert Koordinaten, so dass x1 <= x2 und y1 <= y2""" sorted_x1 = min(x1, x2) sorted_x2 = max(x1, x2) sorted_y1 = min(y1, y2) sorted_y2 = max(y1, y2) return sorted_x1, sorted_y1, sorted_x2, sorted_y2 # === GESICHTSMASKEN-FUNKTIONEN (ERWEITERT FÜR 3 MODI) === def create_face_mask(image, bbox_coords, mode): """ ERWEITERTE FUNKTION: Erzeugt Maske basierend auf 3 Modi Weiße Bereiche werden VERÄNDERT, Schwarze bleiben ERHALTEN Parameter: - image: PIL Image - bbox_coords: [x1, y1, x2, y2] - mode: "environment_change", "focus_change", "face_only_change" Returns: - PIL Image (L-Modus, 0=schwarz=erhalten, 255=weiß=verändern) """ mask = Image.new("L", image.size, 0) # Start mit komplett schwarzer Maske (alles geschützt) if bbox_coords and all(coord is not None for coord in bbox_coords): # Sortiere Koordinaten x1, y1, x2, y2 = sort_coordinates(*bbox_coords) # Stelle sicher, dass Koordinaten innerhalb des Bildes liegen x1 = max(0, min(x1, image.width-1)) y1 = max(0, min(y1, image.height-1)) x2 = max(0, min(x2, image.width-1)) y2 = max(0, min(y2, image.height-1)) draw = ImageDraw.Draw(mask) if mode == "environment_change": # MODUS 1: Umgebung ändern (Depth + Canny) # Maske: Alles weiß AUSSER Bereich (schwarz) draw.rectangle([0, 0, image.size[0], image.size[1]], fill=255) # Alles weiß = verändern draw.rectangle([x1, y1, x2, y2], fill=0) # Bereich schwarz = geschützt (rechteckig) print(f"🎯 MODUS: Umgebung ändern - Alles außer BBox wird verändert (BBox: {x1},{y1},{x2},{y2})") elif mode == "focus_change": # MODUS 2: Focus verändern (OpenPose + Canny) # Maske: Nur innerhalb der Box weiß (Rest schwarz) draw.rectangle([x1, y1, x2, y2], fill=255) # Nur Box weiß = verändern print(f"🎯 MODUS: Focus verändern - Nur innerhalb der BBox wird verändert (BBox: {x1},{y1},{x2},{y2})") elif mode == "face_only_change": # MODUS 3: Ausschließlich Gesicht (Depth + Canny) # Maske: Nur innerhalb der Box weiß (Rest schwarz) - wie focus_change draw.rectangle([x1, y1, x2, y2], fill=255) # Nur Box weiß = verändern print(f"🎯 MODUS: Ausschließlich Gesicht - Nur innerhalb der BBox wird verändert (BBox: {x1},{y1},{x2},{y2})") return mask # === KORREKTE GEMEINSAME PROPORTIONALE SKALIERUNG MIT PADDING === def scale_image_and_mask_together(image, mask, target_size=512): """ SKALIERT BILD UND MASKE GEMEINSAM MIT GLEICHEN PROPORTIONEN (MIT PADDING) Behält das Seitenverhältnis bei und fügt ggf. Padding hinzu Parameter: - image: PIL Image (RGB) - mask: PIL Image (L-Modus, Maske) - target_size: Zielgröße (Standard 512) Returns: - padded_image: skaliertes Bild mit Padding (RGB) - padded_mask: skalierte Maske mit Padding (L) - padding_info: Dictionary mit Skalierungsinfo für späteres Compositing """ if image is None or mask is None: raise ValueError("Bild oder Maske ist None") if image.size != mask.size: raise ValueError(f"Bild und Maske haben unterschiedliche Größen: {image.size} vs {mask.size}") #Stoppt Programm sofort mit Fehlermeldung! #Variablen für Bildmaße original_width, original_height = image.size # Bestimme Skalierungsfaktor (längere Seite auf target_size) scale = target_size / max(original_width, original_height) new_width = int(original_width * scale) new_height = int(original_height * scale) print(f"📐 Gemeinsame Skalierung: {original_width}x{original_height} → {new_width}x{new_height} (Skalierung: {scale:.4f})") # Skaliere Bild und Maske getrennt voneinander aber proportional scaled_image = image.resize((new_width, new_height), Image.Resampling.LANCZOS) scaled_mask = mask.resize((new_width, new_height), Image.Resampling.NEAREST) # Auf Zielgröße padden (zentriert) #Image.new("RGB", (target_size, target_size), (0, 0, 0)) erstellt ein neues, leeres, schwarzes Bild in der Ziel-Verarbeitungsgröße deines Modells (512×512 für SD 1.5 oder 1024×1024 für SDXL) padded_image = Image.new("RGB", (target_size, target_size), (0, 0, 0)) padded_mask = Image.new("L", (target_size, target_size), 0) # Zentrierte Position berechnen #um zu wissen, wo genau das herunterskalierte Originalbild auf das leere schwarze Bild legt werden muss, damit es perfekt mittig sitzt. Ohne diese Berechnung würde das Bild falsch (z.B. in der Ecke) platziert werden. x_offset = (target_size - new_width) // 2 y_offset = (target_size - new_height) // 2 padded_image.paste(scaled_image, (x_offset, y_offset)) padded_mask.paste(scaled_mask, (x_offset, y_offset)) # WICHTIG: Speichere alle Informationen für späteres Compositing padding_info = { 'x_offset': x_offset, 'y_offset': y_offset, 'scaled_width': new_width, 'scaled_height': new_height, 'original_width': original_width, 'original_height': original_height, 'scale_factor': scale, 'target_size': target_size } print(f"📦 Padding hinzugefügt: Offsets ({x_offset}, {y_offset})") return padded_image, padded_mask, padding_info # === KORREKTE COMPOSITING-FUNKTION === def composite_edited_region(original_image, inpaint_result, original_mask, padding_info): """ KORREKTER COMPOSITING-WORKFLOW: - Schneidet den bearbeiteten Bereich aus dem Inpaint-Ergebnis - Fügt ihn nahtlos in das Originalbild ein Parameter: - original_image: Originalbild in Originalgröße - inpaint_result: 512x512 Inpaint-Ergebnis - original_mask: Originalmaske (vor Skalierung) - padding_info: Skalierungsinformationen von scale_image_and_mask_together() Returns: - composited_image: Finales Bild mit bearbeiteter Region """ print(f"🎨 Starte korrektes Compositing...") # 1. Extrahiere den bearbeiteten Bereich (die Veränderung) aus dem Inpaint-Ergebnis (ohne Padding) x_offset = padding_info['x_offset'] y_offset = padding_info['y_offset'] scaled_width = padding_info['scaled_width'] scaled_height = padding_info['scaled_height'] # Bereich im 512x512 Inpaint-Ergebnis, der dem originalen Bild entspricht unpainted_region = inpaint_result.crop( (x_offset, y_offset, x_offset + scaled_width, y_offset + scaled_height) ) print(f" Bearbeiteter Bereich extrahiert: {unpainted_region.size}") # 2. Skaliere den bearbeiteten Bereich zurück auf Originalgröße .resize() ist eine Python Funktion, aus der Pillow-Bibliothek die das skaliert. original_size = (padding_info['original_width'], padding_info['original_height']) edited_region_fullsize = unpainted_region.resize(original_size, Image.Resampling.LANCZOS) print(f" Auf Originalgröße skaliert: {edited_region_fullsize.size}") # 3. Erstelle eine weiche Maske für nahtloses Einfügen # Erweitere die Originalmaske leicht für weiche Übergänge # Dieser gesamte Codeblock erzeugt weiche Übergänge, damit der bearbeitete Bereich nicht wie eine harte, ausgeschnittene "Collage" im Originalbild aussieht. Die Kombination aus MaxFilter (erweitert die Maske) und GaussianBlur (verwischt die Kante) erzeugt eine weiche Alpha-Maske für sanftes Alpha Blending. soft_mask = original_mask.copy() # Für weiche Kanten: Erweitere die Maske um 5 Pixel from PIL import ImageFilter soft_mask = soft_mask.filter(ImageFilter.MaxFilter(5)) soft_mask = soft_mask.filter(ImageFilter.GaussianBlur(3)) # 4. Compositing: Kombiniere Originalbild und bearbeiteten Bereich final_image = original_image.copy() # Konvertiere für alpha blending edited_rgba = edited_region_fullsize.convert("RGBA") soft_mask_rgba = soft_mask.convert("L") # Erstelle ein temporäres Bild mit Alpha-Kanal temp_image = Image.new("RGBA", original_size, (0, 0, 0, 0)) temp_image.paste(edited_rgba, (0, 0), soft_mask_rgba) # Kombiniere mit Original final_image = Image.alpha_composite(final_image.convert("RGBA"), temp_image) final_image = final_image.convert("RGB") print(f"✅ Compositing abgeschlossen. Finale Größe: {final_image.size}") return final_image def auto_detect_face_area(image): """Optimierten Vorschlag für Gesichtsbereich ohne externe Bibliotheken""" width, height = image.size face_size = min(width, height) * 0.4 x1 = (width - face_size) / 2 y1 = (height - face_size) / 4 x2 = x1 + face_size y2 = y1 + face_size * 1.2 # Sortiere Koordinaten und stelle sicher, dass sie innerhalb des Bildes liegen x1 = max(0, int(min(x1, x2))) y1 = max(0, int(min(y1, y2))) x2 = min(width, int(max(x1, x2))) y2 = min(height, int(max(y1, y2))) print(f"Geschätzte Gesichtskoordinaten: [{x1}, {y1}, {x2}, {y2}] (Bild: {width}x{height})") return [x1, y1, x2, y2] # === PIPELINES === pipe_txt2img = None current_pipe_model_id = None pipe_img2img = None #Das Laden des Modells bedeutet, die trainierten Gewichte (Parameter) von der Festplatte zu lesen und #im Arbeitsspeicher (RAM) und idealerweise im Grafikspeicher (VRAM) zu halten, damit sie für Berechnungen schnell verfügbar sind. def load_txt2img(model_id): """Lädt das Text-to-Image Modell basierend auf der Auswahl""" global pipe_txt2img, current_pipe_model_id if pipe_txt2img is not None and current_pipe_model_id == model_id: print(f"✅ Modell {model_id} bereits geladen") return pipe_txt2img print(f"🔄 Lade Modell: {model_id}") config = MODEL_CONFIGS.get(model_id, MODEL_CONFIGS["runwayml/stable-diffusion-v1-5"]) print(f"📋 Modell-Konfiguration: {config['name']}") print(f"📝 Beschreibung: {config['description']}") try: # VAE-Handling basierend auf Modellkonfiguration (Realistic Vision hat kein VAE-der Autoencoder ist ein CNN) vae = None if config.get("requires_vae", False): print(f"🔧 Lade externe VAE: {config['vae_model']}") try: vae = AutoencoderKL.from_pretrained( config["vae_model"], torch_dtype=torch_dtype ).to(device) print("✅ VAE erfolgreich geladen") except Exception as vae_error: print(f"⚠️ Fehler beim Laden der VAE: {vae_error}") print("ℹ️ Versuche ohne VAE weiter...") vae = None model_params = { "torch_dtype": torch_dtype, "safety_checker": None, "requires_safety_checker": False, "add_watermarker": False, "allow_pickle": True, } # Jetzt wird nicht mehr erzwungen wo nach Gewichten gesucht werden soll sondern gezielt mitgeteilt welche Gewichte gewählt wurden. if model_id in SAFETENSORS_MODELS: model_params["use_safetensors"] = True print(f"ℹ️ Verwende safetensors für {model_id}") else: model_params["use_safetensors"] = False print(f"ℹ️ Verwende .bin weights für {model_id}") if config.get("supports_fp16", False) and torch_dtype == torch.float16: model_params["variant"] = "fp16" print("ℹ️ Verwende FP16 Variante") else: print("ℹ️ Verwende Standard Variante (kein FP16)") if vae is not None: model_params["vae"] = vae print(f"📥 Lade Hauptmodell von Hugging Face...") pipe_txt2img = StableDiffusionPipeline.from_pretrained( model_id, **model_params ).to(device) # Der Scheduler (z.B. DPM-Solver++ oder PNDM) ist der Algorithmus, der den Zeitplan für das schrittweise Entrauschen (Denoising) # festlegt - er bestimmt, wie viele und welche Rauschschritte in welcher Reihenfolge abgearbeitet werden. print("⚙️ Konfiguriere Scheduler...") if pipe_txt2img.scheduler is None: print("⚠️ Scheduler ist None, setze Standard-Scheduler") pipe_txt2img.scheduler = PNDMScheduler.from_pretrained( model_id, subfolder="scheduler" ) try: if hasattr(pipe_txt2img.scheduler, 'config'): scheduler_config = pipe_txt2img.scheduler.config else: scheduler_config = { "beta_start": 0.00085, "beta_end": 0.012, "beta_schedule": "scaled_linear", "num_train_timesteps": 1000, "prediction_type": "epsilon", "steps_offset": 1 } print("⚠️ Keine Scheduler-Konfig gefunden, verwende Standard") pipe_txt2img.scheduler = DPMSolverMultistepScheduler.from_config( scheduler_config, use_karras_sigmas=True, algorithm_type="sde-dpmsolver++" ) print("✅ DPM-Solver Multistep Scheduler konfiguriert") except Exception as scheduler_error: print(f"⚠️ Konnte DPM-Scheduler nicht setzen: {scheduler_error}") print("ℹ️ Verwende Standard-Scheduler weiter") pipe_txt2img.enable_attention_slicing() print("✅ Attention Slicing aktiviert") # Attention Slicing ist Aufteilung der Attention-Matrix auf die Heads -> späteres concat if hasattr(pipe_txt2img, 'vae') and pipe_txt2img.vae is not None: try: pipe_txt2img.enable_vae_slicing() if hasattr(pipe_txt2img.vae, 'enable_slicing'): pipe_txt2img.vae.enable_slicing() print("✅ VAE Slicing aktiviert") except Exception as vae_slice_error: print(f"⚠️ VAE Slicing nicht möglich: {vae_slice_error}") current_pipe_model_id = model_id print(f"✅ {config['name']} erfolgreich geladen") print(f"📊 Modell-Dtype: {pipe_txt2img.dtype}") print(f"📊 Scheduler: {type(pipe_txt2img.scheduler).__name__}") print(f"⚙️ Empfohlene Einstellungen: Steps={config['recommended_steps']}, CFG={config['recommended_cfg']}") return pipe_txt2img except Exception as e: print(f"❌ Fehler beim Laden von {model_id}: {str(e)[:200]}...") import traceback traceback.print_exc() print("🔄 Fallback auf SD 1.5...") try: pipe_txt2img = StableDiffusionPipeline.from_pretrained( "runwayml/stable-diffusion-v1-5", torch_dtype=torch_dtype, use_safetensors=True, ).to(device) pipe_txt2img.enable_attention_slicing() current_pipe_model_id = "runwayml/stable-diffusion-v1-5" print("✅ Fallback auf SD 1.5 erfolgreich") return pipe_txt2img except Exception as fallback_error: print(f"❌ Auch Fallback fehlgeschlagen: {fallback_error}") raise def load_img2img(): """Lädt das Inpainting-Modell mit DPMSolver++ Scheduler""" global pipe_img2img if pipe_img2img is None: print("🔄 Lade Inpainting-Modell...") try: pipe_img2img = StableDiffusionInpaintPipeline.from_pretrained( "runwayml/stable-diffusion-inpainting", torch_dtype=torch_dtype, allow_pickle=False, safety_checker=None, ).to(device) pipe_img2img.scheduler = DPMSolverMultistepScheduler.from_config( pipe_img2img.scheduler.config, algorithm_type="sde-dpmsolver++", use_karras_sigmas=True, timestep_spacing="trailing" ) print("✅ DPMSolver++ Multistep Scheduler für Inpainting konfiguriert") except Exception as e: print(f"❌ Fehler beim Laden des Inpainting-Modells: {e}") raise pipe_img2img.enable_attention_slicing() pipe_img2img.enable_vae_tiling() if hasattr(pipe_img2img, 'vae_slicing'): pipe_img2img.vae_slicing = True print("✅ Inpainting-Modell geladen und optimiert") return pipe_img2img #Die Callback-Funktion wird von der Pipeline nach jedem Verarbeitungsschritt aufgerufen und erhält Informationen #wie den aktuellen step und timestep. Diese nutzt der Progressbalken-Callback, um den Fortschritt zu berechnen und anzuzeigen. # === NEUE CALLBACK-FUNKTIONEN FÜR FORTSCHRITT === class TextToImageProgressCallback: def __init__(self, progress, total_steps): self.progress = progress self.total_steps = total_steps self.current_step = 0 def __call__(self, pipe, step, timestep, callback_kwargs): self.current_step = step + 1 progress_percent = (step / self.total_steps) * 100 self.progress(progress_percent / 100, desc="Generierung läuft...") return callback_kwargs class ImageToImageProgressCallback: def __init__(self, progress, total_steps, strength): self.progress = progress self.total_steps = total_steps self.current_step = 0 self.strength = strength self.actual_total_steps = None def __call__(self, pipe, step, timestep, callback_kwargs): self.current_step = step + 1 if self.actual_total_steps is None: if self.strength < 1.0: self.actual_total_steps = int(self.total_steps * self.strength) else: self.actual_total_steps = self.total_steps print(f"🎯 INTERNE STEP-AUSGABE: Strength {self.strength} → {self.actual_total_steps} tatsächliche Denoising-Schritte") progress_percent = (step / self.actual_total_steps) * 100 self.progress(progress_percent / 100, desc="Generierung läuft...") return callback_kwargs # === NEUE FUNKTIONEN FÜR DIE FEATURES (ANGEPASST FÜR 3 MODI) === def create_preview_image(image, bbox_coords, mode): """ NEUE FUNKTION: Erstellt Vorschau basierend auf 3 Modi mit farbigen Rahmen Parameter: - image: PIL Image - bbox_coords: [x1, y1, x2, y2] - mode: "environment_change", "focus_change", "face_only_change" Returns: - PIL Image mit farbigem Rahmen und Text """ if image is None: return None preview = image.copy() draw = ImageDraw.Draw(preview) # Farben basierend auf Modus if mode == "environment_change": border_color = (0, 255, 0, 180) # Grün für Umgebung mode_text = "UMGEBUNG ÄNDERN (Bereich geschützt)" box_color = (255, 255, 0, 200) # Gelb für geschützten Bereich text_bg_color = (0, 128, 0, 160) # Dunkelgrün elif mode == "focus_change": border_color = (255, 165, 0, 180) # Orange für Focus mode_text = "FOCUS VERÄNDERN (Bereich+Körper)" box_color = (255, 0, 0, 200) # Rot für Veränderungsbereich text_bg_color = (255, 140, 0, 160) # Dunkelorange elif mode == "face_only_change": border_color = (255, 0, 0, 180) # Rot für nur Gesicht mode_text = "NUR BEREICH VERÄNDERN" box_color = (255, 0, 0, 200) # Rot für Veränderungsbereich text_bg_color = (128, 0, 0, 160) # Dunkelrot else: # Fallback border_color = (128, 128, 128, 180) mode_text = "UNBEKANNTER MODUS" box_color = (128, 128, 128, 200) text_bg_color = (64, 64, 64, 160) # Skaliere Rahmendicke basierend auf Bildgröße (sonst bei großen Bildern ganz dünne Rahmen!) border_width = max(8, image.width // 200) # Mindestens 8px, bei großen Bildern dicker draw.rectangle([0, 0, preview.width-1, preview.height-1], outline=border_color, width=border_width) if bbox_coords and all(coord is not None for coord in bbox_coords): # Sortiere Koordinaten x1, y1, x2, y2 = sort_coordinates(*bbox_coords) # Stelle sicher, dass die Koordinaten innerhalb des Bildes liegen x1 = max(0, min(x1, preview.width-1)) y1 = max(0, min(y1, preview.height-1)) x2 = max(0, min(x2, preview.width-1)) y2 = max(0, min(y2, preview.height-1)) # Nur zeichnen, wenn die Bounding Box gültig ist if x2 > x1 and y2 > y1: # Skaliere Box-Rahmen basierend auf Bildgröße box_width = max(3, image.width // 400) draw.rectangle([x1, y1, x2, y2], outline=box_color, width=box_width) text_color = (255, 255, 255) # Text über der Bounding Box platzieren text_y = max(0, y1 - 25) text_bbox = draw.textbbox((x1, text_y), mode_text) draw.rectangle([text_bbox[0]-5, text_bbox[1]-2, text_bbox[2]+5, text_bbox[3]+2], fill=text_bg_color) draw.text((x1, text_y), mode_text, fill=text_color) return preview def update_live_preview(image, bbox_x1, bbox_y1, bbox_x2, bbox_y2, mode): """ Aktualisiert die Live-Vorschau bei Koordinaten-Änderungen NEU: Verwendet 3 Modi statt Boolean """ if image is None: return None # Sortiere die Koordinaten (Slider zeigen Originalkoordinaten) bbox_coords = sort_coordinates(bbox_x1, bbox_y1, bbox_x2, bbox_y2) return create_preview_image(image, bbox_coords, mode) def process_image_upload(image): """Verarbeitet Bild-Upload und gibt Bild + Koordinaten zurück""" if image is None: return None, None, None, None, None width, height = image.size # Berechne Bounding-Box basierend auf der tatsächlichen Bildgröße bbox = auto_detect_face_area(image) # Sortiere die Koordinaten bbox_x1, bbox_y1, bbox_x2, bbox_y2 = sort_coordinates(*bbox) # Für die Vorschau verwende die Originalkoordinaten preview = create_preview_image(image, [bbox_x1, bbox_y1, bbox_x2, bbox_y2], "environment_change") # Slider-Werte SIND JETZT ORIGINALKOORDINATEN (keine 512-Skalierung!) print(f"Bild {width}x{height} -> Slider-Originalwerte: [{bbox_x1}, {bbox_y1}, {bbox_x2}, {bbox_y2}]") return preview, bbox_x1, bbox_y1, bbox_x2, bbox_y2 # === FUNKTION FÜR SLIDER-UPDATE === def update_slider_for_image(image): """Aktualisiert Slider-Maxima basierend auf Bildgröße bis 4096x4096""" if image is None: return ( gr.update(maximum=MAX_IMAGE_SIZE), gr.update(maximum=MAX_IMAGE_SIZE), gr.update(maximum=MAX_IMAGE_SIZE), gr.update(maximum=MAX_IMAGE_SIZE) ) width, height = image.size # Setze Slider-Maxima auf Bildgröße (begrenzt auf MAX_IMAGE_SIZE für Stabilität) max_width = min(width, MAX_IMAGE_SIZE) max_height = min(height, MAX_IMAGE_SIZE) print(f"Slider-Maxima gesetzt auf: {max_width}x{max_height}") return ( gr.update(maximum=max_width), gr.update(maximum=max_height), gr.update(maximum=max_width), gr.update(maximum=max_height) ) def text_to_image(prompt, model_id, steps, guidance_scale, progress=gr.Progress()): try: if not prompt or not prompt.strip(): return None, "Bitte einen Prompt eingeben" print(f"🚀 Starte Generierung mit Modell: {model_id}") print(f"📝 Prompt: {prompt}") # Automatische negative Prompts generieren auto_negatives = auto_negative_prompt(prompt) print(f"🤖 Automatisch generierte Negative Prompts: {auto_negatives}") start_time = time.time() # Liste von Qualitätswörtern/Gewichten, die auf Benutzereingaben prüfen quality_keywords = ['masterpiece', 'best quality', 'high quality', 'highly detailed', 'exquisite', 'ultra detailed', 'professional', 'perfect', 'excellent', 'amazing', 'stunning', 'beautiful'] # Prüfe, ob der Benutzer bereits Qualitätswörter/Gewichte verwendet hat user_has_quality_words = False # Konvertiere Prompt zu Kleinbuchstaben für die Prüfung prompt_lower = prompt.lower() # Prüfe auf einfache Qualitätswörter for keyword in quality_keywords: if keyword in prompt_lower: user_has_quality_words = True print(f"✓ Benutzer verwendet bereits Qualitätswort: {keyword}") break # Prüfe auf Gewichte (z.B. (word:1.5), [word], etc.) weight_patterns = [r'\([^)]+:\d+(\.\d+)?\)', r'\[[^\]]+\]'] for pattern in weight_patterns: if re.search(pattern, prompt): user_has_quality_words = True print("✓ Benutzer verwendet bereits Gewichte im Prompt") break # Prompt basierend auf Prüfung anpassen if not user_has_quality_words: enhanced_prompt = f"masterpiece, best quality, {prompt}" print(f"🔄 Verbesserter Prompt: {enhanced_prompt}") else: enhanced_prompt = prompt print("✓ Benutzerprompt wird unverändert verwendet") print(f"Finaler Prompt für Generation: {enhanced_prompt}") progress(0, desc="Lade Modell...") pipe = load_txt2img(model_id) seed = random.randint(0, 2**32 - 1) generator = torch.Generator(device=device).manual_seed(seed) print(f"🌱 Seed: {seed}") callback = TextToImageProgressCallback(progress, steps) print(f"⚙️ Einstellungen: Steps={steps}, CFG={guidance_scale}") image = pipe( prompt=enhanced_prompt, negative_prompt=auto_negatives, height=512, width=512, num_inference_steps=int(steps), guidance_scale=guidance_scale, generator=generator, callback_on_step_end=callback, callback_on_step_end_tensor_inputs=[], ).images[0] end_time = time.time() duration = end_time - start_time print(f"✅ Bild generiert in {duration:.2f} Sekunden") config = MODEL_CONFIGS.get(model_id, MODEL_CONFIGS["runwayml/stable-diffusion-v1-5"]) status_msg = f"✅ Generiert mit {config['name']} in {duration:.1f}s" return image, status_msg except Exception as e: error_msg = f"❌ Fehler: {str(e)}" print(f"❌ Fehler in text_to_image: {e}") import traceback traceback.print_exc() return None, error_msg # === KORRIGIERTE HAUPTFUNKTION MIT RICHTIGEM COMPOSITING === def img_to_image(image, prompt, neg_prompt, strength, steps, guidance_scale, mode, bbox_x1, bbox_y1, bbox_x2, bbox_y2, progress=gr.Progress()): """ KORRIGIERTE HAUPTFUNKTION FÜR BILD-zu-BILD MIT RICHTIGEM COMPOSITING WICHTIG: Verwendet den korrekten Compositing-Workflow: 1. Skaliert Bild und Maske gemeinsam 2. Führt Inpainting auf 512×512 durch 3. Kompositiert nur den bearbeiteten Bereich zurück ins Originalbild """ try: if image is None: return None import time, random start_time = time.time() print(f"🚀 Img2Img Start → Modus: {mode}") print(f"📊 Einstellungen: Strength: {strength}, Steps: {steps}, Guidance: {guidance_scale}") print(f"📝 Prompt: {prompt}") print(f"🚫 Negativ-Prompt: {neg_prompt}") # ===== AUTOMATISCHEN NEGATIV-PROMPT GENERIEREN ===== auto_negatives = auto_negative_prompt(prompt) print(f"🤖 Automatisch generierter Negativ-Prompt: {auto_negatives}") # ===== KOMBINIERE MANUELLEN UND AUTOMATISCHEN PROMPT ===== combined_negative_prompt = "" if neg_prompt and neg_prompt.strip(): user_neg = neg_prompt.strip() print(f"👤 Benutzer Negativ-Prompt: {user_neg}") user_words = [word.strip().lower() for word in user_neg.split(",")] auto_words = [word.strip().lower() for word in auto_negatives.split(",")] combined_words = user_words.copy() for auto_word in auto_words: if auto_word and auto_word not in user_words: combined_words.append(auto_word) unique_words = [] seen_words = set() for word in combined_words: if word and word not in seen_words: unique_words.append(word) seen_words.add(word) combined_negative_prompt = ", ".join(unique_words) else: combined_negative_prompt = auto_negatives print(f"ℹ️ Kein manueller Negativ-Prompt, verwende nur automatischen: {combined_negative_prompt}") print(f"✅ Finaler kombinierter Negativ-Prompt: {combined_negative_prompt}") # ===== GESICHTS-SPEZIFISCHE BOOSTER FÜR NUR-GESICHT MODUS ===== if mode == "face_only_change": face_boosters = "(perfect face:1.2), (symmetrical face:1.1), realistic shaded perfect face, " if not any(keyword in prompt.lower() for keyword in ["perfect face", "symmetrical", "realistic face", "shaded face"]): enhanced_prompt = face_boosters + prompt print(f"👤 Gesichts-Booster hinzugefügt: {face_boosters}") else: enhanced_prompt = prompt print(f"👤 Benutzer hat bereits Gesichts-Booster im Prompt") else: enhanced_prompt = prompt # ===== HINTERGRUND-BOOSTER FÜR UMGEBUNGS-ÄNDERUNG ===== if mode == "environment_change": background_boosters = "complete scene, full background, entire environment, " if not any(keyword in enhanced_prompt.lower() for keyword in ["complete scene", "full background", "entire environment", "whole setting"]): enhanced_prompt = background_boosters + enhanced_prompt print(f"🌳 Hintergrund-Booster hinzugefügt: {background_boosters}") print(f"🎯 Finaler Prompt für {mode}: {enhanced_prompt}") #Zur Überbrückung bis von der Pipelines Infos kommen! progress(0, desc="Starte Generierung mit ControlNet...") # ===== MODUS-SPEZIFISCHE EINSTELLUNGEN ===== adj_strength = min(0.85, strength * 1.25) if mode == "focus_change": keep_environment = False controlnet_strength = adj_strength * 0.7 print(f"🎯 MODUS: Focus verändern → OpenPose+Canny (keep_environment=False)") elif mode == "environment_change": keep_environment = True controlnet_strength = adj_strength * 0.3 print(f"🎯 MODUS: Umgebung ändern → Depth+Canny (keep_environment=True)") else: # face_only_change keep_environment = True controlnet_strength = adj_strength * 0.5 print(f"🎯 MODUS: Ausschließlich Gesicht → Depth+Canny (keep_environment=True)") controlnet_steps = min(25, int(steps * 0.8)) print(f"⚙️ ControlNet Settings: Strength={controlnet_strength:.3f}, Steps={controlnet_steps}") progress(0.03, desc="ControlNet läuft...") # ===== WICHTIG: VARIABLEN FÜR KOMPLETTEN WORKFLOW ===== original_mask = None padding_info = None controlnet_input = image.convert("RGB") # Standard: Originalbild if bbox_x1 is not None and bbox_y1 is not None and bbox_x2 is not None and bbox_y2 is not None: print(f"🎯 BBox Koordinaten erhalten: [{bbox_x1}, {bbox_y1}, {bbox_x2}, {bbox_y2}]") # 1. MASKE AUF ORIGINAL-BILD ERSTELLEN (speichern für späteres Compositing) original_mask = create_face_mask(image, (bbox_x1, bbox_y1, bbox_x2, bbox_y2), mode) # 2. BILD UND MASKE GEMEINSAM SKALIEREN (mit Padding) scaled_image, scaled_mask, padding_info = scale_image_and_mask_together( image.convert("RGB"), # Originalbild original_mask, # Originalmaske target_size=IMG_SIZE ) #ControlNet ist ein paralleles Modell (CNN), das unveränderte Control-Maps (z. B. Tiefenkarten) #verarbeitet und konditionierende Signale an das frozen UNet weiterleitet, um die Gesamtgeneration zu steuern, #ohne pixelgenaue Manipulationen vorzunehmen. Es beeinflusst den Diffusionsprozess global/lokal durch Addition zu den Features. #ControlNet-Bildgröße und Inpaint-Bildgröße müssen übereinstimmen! controlnet_input = scaled_image # Verwende das skalierte Bild für ControlNet print(f"✅ Gemeinsame Skalierung abgeschlossen") print(f" Original: {image.size} → Skaliert: {scaled_image.size}") else: # Keine BBox: Normales Img2Img (ohne Maske) print(f"ℹ️ Keine BBox angegeben → normales Img2Img (ohne Maske)") controlnet_input = image.convert("RGB").resize((IMG_SIZE, IMG_SIZE), Image.Resampling.LANCZOS) # ===== CONTROLNET AUFRUF ===== print(f"📊 ControlNet Input Größe: {controlnet_input.size}") controlnet_output, inpaint_input = controlnet_processor.generate_with_controlnet( image=controlnet_input, prompt=enhanced_prompt, negative_prompt=combined_negative_prompt, steps=controlnet_steps, guidance_scale=guidance_scale, controlnet_strength=controlnet_strength, progress=None, keep_environment=keep_environment ) print(f"✅ ControlNet Output erhalten") print(f"✅ Inpaint Input Größe: {inpaint_input.size}") progress(0.3, desc="ControlNet abgeschlossen – starte Inpaint...") # ===== INPAINTING PIPELINE ===== pipe = load_img2img() # Bild für Inpainting vorbereiten if inpaint_input.size != (IMG_SIZE, IMG_SIZE): print(f"⚠️ Inpaint Input hat unerwartete Größe {inpaint_input.size}, skaliere auf {IMG_SIZE}x{IMG_SIZE}") img_resized = inpaint_input.convert("RGB").resize((IMG_SIZE, IMG_SIZE), Image.Resampling.LANCZOS) else: img_resized = inpaint_input.convert("RGB") print(f"✅ Inpaint Input ist bereits {IMG_SIZE}x{IMG_SIZE}") # ===== SEED UND GENERATOR ===== adj_guidance = min(guidance_scale, 12.0) seed = random.randint(0, 2**32 - 1) generator = torch.Generator(device=device).manual_seed(seed) print(f"🌱 Inpaint Seed: {seed}") # ===== MASKE FÜR INPAINTING VORBEREITEN ===== inpaint_mask = None if original_mask is not None and padding_info is not None: # Verwende die skalierte Maske für Inpainting _, scaled_mask, _ = scale_image_and_mask_together( image.convert("RGB"), original_mask, target_size=IMG_SIZE ) inpaint_mask = scaled_mask print(f"✅ Maske für Inpainting vorbereitet: {inpaint_mask.size}") # ===== FORTSCHRITTS-CALLBACK ===== callback = ImageToImageProgressCallback(progress, int(steps), adj_strength) # ===== INPAINT DURCHFÜHREN ===== result = pipe( prompt=enhanced_prompt, negative_prompt=combined_negative_prompt, image=img_resized, mask_image=inpaint_mask, strength=adj_strength, num_inference_steps=int(steps), guidance_scale=adj_guidance, generator=generator, callback_on_step_end=callback, callback_on_step_end_tensor_inputs=[], ) # ===== KORREKTES COMPOSITING ===== generated_image = result.images[0] if original_mask is not None and padding_info is not None: # KORREKTER WORKFLOW: Nur bearbeiteten Bereich in Originalbild einfügen final_image = composite_edited_region( original_image=image.convert("RGB"), inpaint_result=generated_image, original_mask=original_mask, padding_info=padding_info ) print(f"✅ Korrektes Compositing durchgeführt") else: # Keine Maske: Einfach das generierte Bild zurückgeben (bereits 512×512) final_image = generated_image print(f"ℹ️ Keine Maske → Direkte Rückgabe des 512×512 Bildes") end_time = time.time() duration = end_time - start_time print(f"✅ Transformation abgeschlossen in {duration:.2f} Sekunden") print(f"🎯 Verwendeter Modus: {mode}") print(f"⚙️ ControlNet: {'Depth+Canny' if keep_environment else 'OpenPose+Canny'}") print(f"📊 Finale Bildgröße: {final_image.size}") return final_image except Exception as e: print(f"❌ Fehler in img_to_image: {e}") import traceback traceback.print_exc() return None def update_bbox_from_image(image): """Aktualisiert die Bounding-Box-Koordinaten wenn ein Bild hochgeladen wird""" if image is None: return None, None, None, None bbox = auto_detect_face_area(image) return bbox[0], bbox[1], bbox[2], bbox[3] def update_model_settings(model_id): """Aktualisiert die empfohlenen Einstellungen basierend auf Modellauswahl""" config = MODEL_CONFIGS.get(model_id, MODEL_CONFIGS["runwayml/stable-diffusion-v1-5"]) return ( config["recommended_steps"], # steps config["recommended_cfg"], # guidance_scale f"📊 Empfohlene Einstellungen: {config['recommended_steps']} Steps, CFG {config['recommended_cfg']}" ) def main_ui(): """ HAUPT-UI (ANGEPASST FÜR 3 MODI) """ with gr.Blocks( title="AI Image Generator", theme=gr.themes.Base(), css=""" .info-box { background-color: #f8f4f0; padding: 15px; border-radius: 8px; border-left: 4px solid #8B7355; margin: 20px 0; } .clickable-file { color: #1976d2; cursor: pointer; text-decoration: none; font-family: 'Monaco', 'Consolas', monospace; background: #e3f2fd; padding: 2px 6px; border-radius: 4px; border: 1px solid #bbdefb; } .clickable-file:hover { background: #bbdefb; text-decoration: underline; } .model-info-box { background: #e8f4fd; padding: 12px; border-radius: 6px; margin: 10px 0; border-left: 4px solid #2196f3; font-size: 14px; } #generate-button { background-color: #0080FF !important; border: none !important; margin: 20px auto !important; display: block !important; font-weight: 600; width: 280px; } #generate-button:hover { background-color: #0066CC !important; } .hint-box { margin-top: 20px; } .custom-text { font-size: 25px !important; } .image-upload .svelte-1p4f8co { display: block !important; } .preview-box { border: 2px dashed #ccc; padding: 10px; border-radius: 8px; margin: 10px 0; } .mode-red { border: 3px solid #ff4444 !important; } .mode-green { border: 3px solid #44ff44 !important; } .coordinate-sliders { background: #f8f9fa; padding: 15px; border-radius: 8px; margin: 10px 0; } .gr-checkbox .wrap .text-gray { font-size: 14px !important; font-weight: 600 !important; line-height: 1.4 !important; } .status-message { padding: 10px; border-radius: 5px; margin: 10px 0; text-align: center; font-weight: 500; } .status-success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .status-error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .radio-group { background: #f8f9fa; padding: 15px; border-radius: 8px; margin: 10px 0; border: 2px solid #e9ecef; } .radio-item { padding: 8px 12px; margin: 5px 0; border-radius: 4px; transition: background 0.3s; } .radio-item:hover { background: #e9ecef; } .radio-label { font-weight: 600; font-size: 14px; } .radio-description { font-size: 12px; color: #6c757d; margin-left: 24px; } """ ) as demo: with gr.Column(visible=True) as content_area: with gr.Tab("Text zu Bild"): gr.Markdown("## 🎨 Text zu Bild Generator") with gr.Row(): with gr.Column(scale=2): # Modellauswahl Dropdown (NUR 2 MODELLE) model_dropdown = gr.Dropdown( choices=[ (config["name"], model_id) for model_id, config in MODEL_CONFIGS.items() ], value="runwayml/stable-diffusion-v1-5", label="📁 Modellauswahl", info="🏠 Universal vs 👤 Portraits" ) # Modellinformationen Box model_info_box = gr.Markdown( value="
" "**🏠 Stable Diffusion 1.5 (Universal)**
" "Universal model, good all-rounder, reliable results
" "Empfohlene Einstellungen: 35 Steps, CFG 7.5" "
", label="Modellinformationen" ) with gr.Column(scale=3): txt_input = gr.Textbox( placeholder="z.B. ultra realistic mountain landscape at sunrise, soft mist over the valley, detailed foliage, crisp textures, depth of field, sunlight rays through clouds, shot on medium format camera, 8k, HDR, hyper-detailed, natural lighting, masterpiece", lines=3, label="🎯 Prompt (Englisch)", info="Beschreibe detailliert, was du sehen möchtest. Negative Prompts werden automatisch generiert." ) with gr.Row(): with gr.Column(): txt_steps = gr.Slider( minimum=10, maximum=100, value=35, step=1, label="⚙️ Inferenz-Schritte", info="Mehr Schritte = bessere Qualität, aber langsamer (20-50 empfohlen)" ) with gr.Column(): txt_guidance = gr.Slider( minimum=1.0, maximum=20.0, value=7.5, step=0.5, label="🎛️ Prompt-Stärke (CFG Scale)", info="Wie stark der Prompt befolgt wird (7-12 für gute Balance)" ) # Status-Nachricht status_output = gr.Markdown( value="", elem_classes="status-message" ) generate_btn = gr.Button("🚀 Bild generieren", variant="primary", elem_id="generate-button") with gr.Row(): txt_output = gr.Image( label="🖼️ Generiertes Bild", show_download_button=True, type="pil", height=400 ) # Event-Handler für Modelländerung def update_model_info(model_id): config = MODEL_CONFIGS.get(model_id, MODEL_CONFIGS["runwayml/stable-diffusion-v1-5"]) info_html = f"""
{config['name']}
{config['description']}
Empfohlene Einstellungen: {config['recommended_steps']} Steps, CFG {config['recommended_cfg']}
""" return info_html, config["recommended_steps"], config["recommended_cfg"] model_dropdown.change( fn=update_model_info, inputs=[model_dropdown], outputs=[model_info_box, txt_steps, txt_guidance] ) generate_btn.click( fn=text_to_image, inputs=[txt_input, model_dropdown, txt_steps, txt_guidance], outputs=[txt_output, status_output], concurrency_limit=1 ) with gr.Tab("Bild zu Bild"): gr.Markdown("## 🖼️ Bild zu Bild Transformation (3 MODI)") with gr.Row(): with gr.Column(): img_input = gr.Image( type="pil", label="📤 Eingabebild", height=300, sources=["upload"], elem_id="image-upload" ) with gr.Column(): preview_output = gr.Image( label="🎯 Live-Vorschau mit Maske", height=300, interactive=False, show_download_button=False ) # ===== NEUE RADIO-BUTTONS STATT CHECKBOX ===== with gr.Row(): with gr.Column(): gr.Markdown("### 🎛️ Transformations-Modus") # NEU: 3 Radio-Buttons statt 1 Checkbox mode_radio = gr.Radio( choices=[ ("🌳 Umgebung ändern", "environment_change"), ("🎯 Focus verändern", "focus_change"), ("👤 Ausschließlich Gesicht", "face_only_change") ], value="environment_change", # Standardmodus label="Wähle den Transformationsmodus:", info="Steuert, welcher Teil des Bildes verändert wird", elem_classes="radio-group" ) # Detailierte Erklärungen gr.Markdown("""
Modus-Erklärungen:
🌳 Umgebung ändern: Ändert alles AUSSER dem markierten Bereich (Depth+Canny)
🎯 Focus verändern: Ändert markierten Bereich+Körper (OpenPose+Canny)
👤 Ausschließlich Gesicht: Ändert NUR den markierten Bereich (Depth+Canny)
""") with gr.Row(): gr.Markdown("### 📐 Bildelementbereich anpassen") # SLIDER MIT DYNAMISCHEM MAXIMUM (4096 für große Bilder) with gr.Row(): with gr.Column(): bbox_x1 = gr.Slider( label="← Links (x1)", minimum=0, maximum=MAX_IMAGE_SIZE, value=100, step=1, info="Linke Kante des Bildelementbereichs" ) with gr.Column(): bbox_y1 = gr.Slider( label="↑ Oben (y1)", minimum=0, maximum=MAX_IMAGE_SIZE, value=100, step=1, info="Obere Kante des Bildelementbereichs" ) with gr.Row(): with gr.Column(): bbox_x2 = gr.Slider( label="→ Rechts (x2)", minimum=0, maximum=MAX_IMAGE_SIZE, value=300, step=1, info="Rechte Kante des Bildelementbereichs" ) with gr.Column(): bbox_y2 = gr.Slider( label="↓ Unten (y2)", minimum=0, maximum=MAX_IMAGE_SIZE, value=300, step=1, info="Untere Kante des Bildelementbereichs" ) with gr.Row(): with gr.Column(): img_prompt = gr.Textbox( placeholder="change background to beach with palm trees, keep person unchanged, sunny day", lines=2, label="🎯 Transformations-Prompt (Englisch)", info="Was soll verändert werden? Sei spezifisch." ) with gr.Column(): img_neg_prompt = gr.Textbox( placeholder="blurry, deformed, ugly, bad anatomy, extra limbs, poorly drawn hands", lines=2, label="🚫 Negativ-Prompt (Englisch)", info="Was soll vermieden werden? Unerwünschte Elemente auflisten." ) with gr.Row(): with gr.Column(): strength_slider = gr.Slider( minimum=0.1, maximum=0.9, value=0.4, step=0.05, label="💪 Veränderungs-Stärke", info="0.1-0.3: Leichte Anpassungen, 0.4-0.6: Mittlere Veränderungen, 0.7-0.9: Starke Umgestaltung" ) with gr.Column(): img_steps = gr.Slider( minimum=10, maximum=45, value=35, step=1, label="⚙️ Inferenz-Schritte", info="Anzahl der Verarbeitungsschritte (25-45 für gute Ergebnisse)" ) with gr.Column(): img_guidance = gr.Slider( minimum=1.0, maximum=15.0, value=7.5, step=0.5, label="🎛️ Prompt-Stärke", info="Einfluss des Prompts auf das Ergebnis (6-10 für natürliche Ergebnisse)" ) with gr.Row(): gr.Markdown( "### 📋 Hinweise:\n" "• **🆕 3 Transformations-Modi** für präzise Kontrolle\n" "• **🆕 Unterstützt Bilder bis 4096×4096 Pixel**\n" "• **🆕 Automatische Bildelementerkennung** setzt Koordinaten beim Upload\n" "• **🆕 Live-Vorschau** zeigt farbige Rahmen je nach Modus\n" "• **🆕 Dynamische Koordinaten-Schieberegler** passen sich an Bildgröße an\n" "• **ControlNet-Technologie** für konsistente Ergebnisse\n" "• **Automatische Negative Prompts** für bessere Qualität\n" "• **KORREKTER COMPOSITING-WORKFLOW** – nur bearbeiteter Bereich wird eingefügt\n" "• **Ausgabe: 512×512 Pixel** für beste Qualität (kein Hochskalieren!)" ) transform_btn = gr.Button("🔄 Bild transformieren", variant="primary") with gr.Row(): img_output = gr.Image( label="✨ Transformiertes Bild (512×512 - SD-Technologie-Limit)", show_download_button=True, type="pil", height=400 ) # EVENT-HANDLER FÜR DYNAMISCHE BILDGRÖßEN img_input.change( fn=process_image_upload, inputs=[img_input], outputs=[preview_output, bbox_x1, bbox_y1, bbox_x2, bbox_y2] ).then( fn=update_slider_for_image, inputs=[img_input], outputs=[bbox_x1, bbox_y1, bbox_x2, bbox_y2] ) # NEUE Input-Liste mit mode_radio statt face_preserve coordinate_inputs = [img_input, bbox_x1, bbox_y1, bbox_x2, bbox_y2, mode_radio] # Live-Vorschau Updates für alle Steuerelemente for slider in [bbox_x1, bbox_y1, bbox_x2, bbox_y2]: slider.change( fn=update_live_preview, inputs=coordinate_inputs, outputs=preview_output ) # NEU: Mode-Radio-Button ändert auch Live-Vorschau mode_radio.change( fn=update_live_preview, inputs=coordinate_inputs, outputs=preview_output ) # NEU: Transform-Button mit mode_radio statt face_preserve transform_btn.click( fn=img_to_image, inputs=[ img_input, img_prompt, img_neg_prompt, strength_slider, img_steps, img_guidance, mode_radio, bbox_x1, bbox_y1, bbox_x2, bbox_y2 ], outputs=img_output, concurrency_limit=1 ) return demo if __name__ == "__main__": demo = main_ui() demo.queue(max_size=3) demo.launch( server_name="0.0.0.0", server_port=7860, max_file_size="15MB", show_error=True, share=False, ssr_mode=False )