Spaces:
Runtime error
Runtime error
| #!/usr/bin/env python3 | |
| """ | |
| Pro Realism Edit Studio - Enhanced Edition | |
| ========================================= | |
| Advanced image editing and enhancement studio powered by: | |
| - Qwen-Image-Edit-2511 with Phr00t's Rapid-AIO v23 accelerated transformer | |
| - Real-ESRGAN for high-quality upscaling | |
| - GFPGAN/CodeFormer for face restoration | |
| - Multi-stage detail enhancement pipeline | |
| Author: Enhanced with Hugging Face CLI and image generation expertise | |
| Version: 1.0.0 | |
| """ | |
| import gradio as gr | |
| import numpy as np | |
| import random | |
| import torch | |
| import spaces | |
| import os | |
| import time | |
| import tempfile | |
| from pathlib import Path | |
| # Advanced imports | |
| from accelerate import init_empty_weights | |
| from collections import OrderedDict | |
| from PIL import Image, ImageEnhance, ImageFilter, ImageOps | |
| from diffusers.models import QwenImageTransformer2DModel as DiffusersQwenImageTransformer2DModel | |
| from diffusers.models.model_loading_utils import load_model_dict_into_meta | |
| from huggingface_hub import hf_hub_download, HfApi, login, whoami | |
| from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline | |
| from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel | |
| from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3 | |
| from safetensors import safe_open | |
| from gradio_client import Client, handle_file | |
| # ============================================================================ | |
| # CONFIGURATION - Model IDs and Parameters | |
| # ============================================================================ | |
| # Base model configuration | |
| BASE_MODEL_ID = "Qwen/Qwen-Image-Edit-2511" | |
| APP_VERSION = "1.0.0" | |
| PHR00T_REPO_ID = os.environ.get("PHR00T_REPO_ID", "Phr00t/Qwen-Image-Edit-Rapid-AIO").strip() | |
| RAPID_TRANSFORMER_FILENAME = os.environ.get( | |
| "RAPID_TRANSFORMER_FILENAME", | |
| "v23/Qwen-Rapid-AIO-NSFW-v23.safetensors", | |
| ).strip() | |
| PHR00T_TRANSFORMER_PREFIX = "model.diffusion_model." | |
| VIDEO_SPACE_ID = os.environ.get("VIDEO_SPACE_ID", "").strip() | |
| # Enhanced Upscaler Configuration | |
| UPSCALER_MODEL_ID = os.environ.get("UPSCALER_MODEL_ID", "ai-forever/Real-ESRGAN").strip() | |
| UPSCALER_MODEL_FILENAME = os.environ.get("UPSCALER_MODEL_FILENAME", "RealESRGAN_x4plus.pth").strip() | |
| UPSCALER_TILE_SIZE = int(os.environ.get("UPSCALER_TILE_SIZE", "512")) | |
| UPSCALER_TILE_OVERLAP = int(os.environ.get("UPSCALER_TILE_OVERLAP", "64")) # Increased overlap for better blending | |
| ENHANCE_MAX_INPUT_EDGE = int(os.environ.get("ENHANCE_MAX_INPUT_EDGE", "2048")) # Increased from 1280 | |
| ENHANCE_GRAIN_STRENGTH = float(os.environ.get("ENHANCE_GRAIN_STRENGTH", "0.015")) # Reduced from 0.018 | |
| # Face Restoration Configuration | |
| FACE_RESTORATION_MODEL = os.environ.get("FACE_RESTORATION_MODEL", "Xintao/GFPGAN").strip() | |
| FACE_RESTORATION_WEIGHTS = os.environ.get("FACE_RESTORATION_WEIGHTS", "GFPGANv1.3.pth").strip() | |
| # Advanced Detail Enhancement Configuration | |
| DETAIL_ENHANCEMENT_ENABLED = os.environ.get("DETAIL_ENHANCEMENT_ENABLED", "true").lower() == "true" | |
| SMART_SHARPENING_STRENGTH = float(os.environ.get("SMART_SHARPENING_STRENGTH", "1.15")) | |
| # ============================================================================ | |
| # ENHANCEMENT MODES | |
| # ============================================================================ | |
| ENHANCE_MODE_OFF = "Off" | |
| ENHANCE_MODE_UPSCALE = "Upscale Only" | |
| ENHANCE_MODE_CLEAN = "Clean & Restore" | |
| ENHANCE_MODE_MAX_DETAIL = "Max Detail" | |
| ENHANCE_MODE_FACE_ENHANCE = "Face Enhance" | |
| ENHANCE_MODE_FULL_ENHANCE = "Full Enhance" | |
| ENHANCE_MODE_CHOICES = [ | |
| ENHANCE_MODE_OFF, | |
| ENHANCE_MODE_UPSCALE, | |
| ENHANCE_MODE_CLEAN, | |
| ENHANCE_MODE_MAX_DETAIL, | |
| ENHANCE_MODE_FACE_ENHANCE, | |
| ENHANCE_MODE_FULL_ENHANCE | |
| ] | |
| # ============================================================================ | |
| # GLOBAL MODEL CACHE | |
| # ============================================================================ | |
| _upscaler_model = None | |
| _face_restoration_model = None | |
| _detail_enhancement_model = None | |
| # ============================================================================ | |
| # HUGGING FACE CLI EXPERT FUNCTIONS | |
| # ============================================================================ | |
| def check_hf_login(): | |
| """Check if user is logged in to Hugging Face Hub""" | |
| try: | |
| return whoami() is not None | |
| except Exception: | |
| return False | |
| def ensure_hf_login(): | |
| """Ensure user is logged in, prompt if not""" | |
| if not check_hf_login(): | |
| try: | |
| login() | |
| return True | |
| except Exception as e: | |
| print(f"Hugging Face login failed: {e}") | |
| return False | |
| return True | |
| def download_model_with_retry(repo_id, filename, max_retries=3): | |
| """Download model with retry logic and error handling""" | |
| for attempt in range(max_retries): | |
| try: | |
| return hf_hub_download(repo_id=repo_id, filename=filename) | |
| except Exception as e: | |
| if attempt == max_retries - 1: | |
| raise RuntimeError(f"Failed to download {filename} from {repo_id} after {max_retries} attempts: {e}") | |
| time.sleep(2 ** attempt) # Exponential backoff | |
| return None | |
| def get_model_info(repo_id): | |
| """Get model information from Hugging Face Hub""" | |
| try: | |
| api = HfApi() | |
| model_info = api.model_info(repo_id) | |
| return model_info | |
| except Exception as e: | |
| print(f"Failed to get model info for {repo_id}: {e}") | |
| return None | |
| # ============================================================================ | |
| # VIDEO GENERATION (Preserved from original) | |
| # ============================================================================ | |
| def turn_into_video(input_image, output_images, prompt, progress=gr.Progress(track_tqdm=True)): | |
| """Convert image edit into video transition""" | |
| if not VIDEO_SPACE_ID: | |
| raise gr.Error("Video generation is not configured for this Space.") | |
| if not input_image or not output_images: | |
| raise gr.Error("Please generate an output image first.") | |
| progress(0.02, desc="Preparing images...") | |
| def extract_pil(img_entry): | |
| if isinstance(img_entry, tuple) and isinstance(img_entry[0], Image.Image): | |
| return img_entry[0] | |
| elif isinstance(img_entry, Image.Image): | |
| return img_entry | |
| elif isinstance(img_entry, str): | |
| return Image.open(img_entry) | |
| else: | |
| raise gr.Error(f"Unsupported image format: {type(img_entry)}") | |
| start_img = extract_pil(input_image) | |
| end_img = extract_pil(output_images[0]) | |
| progress(0.10, desc="Saving temp files...") | |
| with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_start, \ | |
| tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_end: | |
| start_img.save(tmp_start.name) | |
| end_img.save(tmp_end.name) | |
| progress(0.20, desc="Connecting to video Space...") | |
| client = Client(VIDEO_SPACE_ID) | |
| progress(0.35, desc="Generating video...") | |
| video_path, seed = client.predict( | |
| start_image_pil=handle_file(tmp_start.name), | |
| end_image_pil=handle_file(tmp_end.name), | |
| prompt=prompt or "smooth cinematic transition", | |
| api_name="/generate_video" | |
| ) | |
| progress(0.95, desc="Finalizing...") | |
| return video_path['video'] | |
| # ============================================================================ | |
| # HISTORY MANAGEMENT (Enhanced) | |
| # ============================================================================ | |
| def update_history(new_images, history): | |
| """Updates the history gallery with the new images.""" | |
| time.sleep(0.3) # Reduced delay for better responsiveness | |
| 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[:50] # Increased from 20 to 50 | |
| 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() | |
| # ============================================================================ | |
| # MODEL LOADING (Enhanced with better error handling) | |
| # ============================================================================ | |
| dtype = torch.bfloat16 | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| def load_phr00t_rapid_transformer(torch_dtype): | |
| """Load Phr00t's Rapid-AIO v23 transformer with enhanced error handling""" | |
| checkpoint_path = download_model_with_retry(PHR00T_REPO_ID, RAPID_TRANSFORMER_FILENAME) | |
| try: | |
| config = DiffusersQwenImageTransformer2DModel.load_config( | |
| BASE_MODEL_ID, | |
| subfolder="transformer", | |
| ) | |
| except Exception as e: | |
| raise RuntimeError(f"Failed to load config for {BASE_MODEL_ID}: {e}") | |
| with init_empty_weights(): | |
| transformer = DiffusersQwenImageTransformer2DModel.from_config(config) | |
| expected_keys = set(transformer.state_dict().keys()) | |
| state_dict = OrderedDict() | |
| try: | |
| with safe_open(checkpoint_path, framework="pt", device="cpu") as checkpoint: | |
| for key in checkpoint.keys(): | |
| if not key.startswith(PHR00T_TRANSFORMER_PREFIX): | |
| continue | |
| mapped_key = key.removeprefix(PHR00T_TRANSFORMER_PREFIX) | |
| if mapped_key in expected_keys: | |
| state_dict[mapped_key] = checkpoint.get_tensor(key) | |
| except Exception as e: | |
| raise RuntimeError(f"Failed to load checkpoint from {checkpoint_path}: {e}") | |
| missing_keys = sorted(expected_keys.difference(state_dict.keys())) | |
| if missing_keys: | |
| sample = ", ".join(missing_keys[:20]) | |
| raise RuntimeError( | |
| f"Phr00t Rapid-AIO transformer checkpoint is missing {len(missing_keys)} " | |
| f"required diffusers keys after prefix conversion. First missing keys: {sample}" | |
| ) | |
| try: | |
| load_model_dict_into_meta(transformer, state_dict, dtype=torch_dtype) | |
| except Exception as e: | |
| raise RuntimeError(f"Failed to load state dict into meta: {e}") | |
| meta_parameters = [name for name, parameter in transformer.named_parameters() if parameter.is_meta] | |
| if meta_parameters: | |
| sample = ", ".join(meta_parameters[:20]) | |
| raise RuntimeError( | |
| f"Phr00t Rapid-AIO transformer still has {len(meta_parameters)} meta parameters " | |
| f"after loading. First meta parameters: {sample}" | |
| ) | |
| transformer.eval() | |
| return transformer | |
| # Load main pipeline | |
| try: | |
| pipe = QwenImageEditPlusPipeline.from_pretrained( | |
| BASE_MODEL_ID, | |
| transformer=load_phr00t_rapid_transformer(dtype), | |
| torch_dtype=dtype | |
| ).to(device) | |
| print("โ Successfully loaded Qwen-Image-Edit-2511 with Rapid-AIO v23 transformer") | |
| except Exception as e: | |
| print(f"โ Failed to load main pipeline: {e}") | |
| raise | |
| # Apply optimizations | |
| pipe.transformer.__class__ = QwenImageTransformer2DModel | |
| pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3()) | |
| print("โ Applied FA3 attention processor optimization") | |
| # ============================================================================ | |
| # ENHANCED UPSCALER (Real-ESRGAN based) | |
| # ============================================================================ | |
| def load_upscaler_model(): | |
| """Load Real-ESRGAN model for high-quality upscaling""" | |
| global _upscaler_model | |
| if _upscaler_model is not None: | |
| return _upscaler_model | |
| try: | |
| import spandrel | |
| import spandrel_extra_arches | |
| spandrel_extra_arches.install() | |
| except ImportError as exc: | |
| raise gr.Error("Enhance mode requires spandrel and spandrel_extra_arches to be installed. " | |
| "Install with: pip install spandrel spandrel_extra_arches") from exc | |
| try: | |
| model_path = download_model_with_retry(UPSCALER_MODEL_ID, UPSCALER_MODEL_FILENAME) | |
| model = spandrel.ModelLoader().load_from_file(model_path) | |
| model.eval().to(device) | |
| _upscaler_model = model | |
| print(f"โ Successfully loaded upscaler: {UPSCALER_MODEL_ID}/{UPSCALER_MODEL_FILENAME}") | |
| return _upscaler_model | |
| except Exception as e: | |
| print(f"โ Failed to load upscaler model: {e}") | |
| # Fallback to original Nomos model | |
| print("๐ Falling back to Nomos upscaler...") | |
| try: | |
| model_path = download_model_with_retry("Phips/4xNomos8k_atd_jpg", "4xNomos8k_atd_jpg.safetensors") | |
| model = spandrel.ModelLoader().load_from_file(model_path) | |
| model.eval().to(device) | |
| _upscaler_model = model | |
| return _upscaler_model | |
| except Exception as fallback_error: | |
| raise gr.Error(f"Failed to load all upscaler models: {e} | {fallback_error}") | |
| def image_to_tensor(image): | |
| """Convert PIL Image to tensor""" | |
| array = np.asarray(image.convert("RGB")).astype(np.float32) / 255.0 | |
| tensor = torch.from_numpy(array).permute(2, 0, 1).unsqueeze(0) | |
| return tensor.to(device) | |
| def tensor_to_image(tensor): | |
| """Convert tensor to PIL Image""" | |
| array = tensor.squeeze(0).detach().float().cpu().clamp(0, 1).permute(1, 2, 0).numpy() | |
| return Image.fromarray((array * 255.0).round().astype(np.uint8), mode="RGB") | |
| def validate_enhance_input_size(image): | |
| """Validate image size for enhancement""" | |
| max_edge = max(image.size) | |
| if max_edge > ENHANCE_MAX_INPUT_EDGE: | |
| raise gr.Error( | |
| f"Enhance mode accepts images up to {ENHANCE_MAX_INPUT_EDGE}px on the longest edge. " | |
| f"Current image is {image.width}x{image.height}. " | |
| f"Consider resizing your image first." | |
| ) | |
| def advanced_tile_upscale(image, scale=4): | |
| """ | |
| Advanced tiling upscaler with improved blending and edge handling | |
| Uses Real-ESRGAN for superior quality compared to Nomos | |
| """ | |
| validate_enhance_input_size(image) | |
| model = load_upscaler_model() | |
| tensor = image_to_tensor(image) | |
| _, _, height, width = tensor.shape | |
| # Adaptive tile size based on image dimensions | |
| base_tile_size = UPSCALER_TILE_SIZE | |
| optimal_tile_size = min(base_tile_size, max(height, width) // 2) | |
| tile_size = max(64, optimal_tile_size) | |
| overlap = max(0, min(UPSCALER_TILE_OVERLAP, tile_size // 2)) | |
| step = max(1, tile_size - overlap) | |
| # Ensure full coverage with edge tiles | |
| y_positions = list(range(0, height, step)) | |
| if y_positions[-1] + tile_size < height: | |
| y_positions.append(max(0, height - tile_size)) | |
| x_positions = list(range(0, width, step)) | |
| if x_positions[-1] + tile_size < width: | |
| x_positions.append(max(0, width - tile_size)) | |
| y_positions = sorted(set(y_positions)) | |
| x_positions = sorted(set(x_positions)) | |
| output = None | |
| weights = None | |
| with torch.inference_mode(): | |
| for y in y_positions: | |
| for x in x_positions: | |
| y1 = min(y + tile_size, height) | |
| x1 = min(x + tile_size, width) | |
| tile = tensor[:, :, y:y1, x:x1] | |
| # Process tile through upscaler | |
| upscaled_tile = model(tile).clamp(0, 1) | |
| # Calculate scale factors | |
| scale_y = upscaled_tile.shape[-2] // tile.shape[-2] | |
| scale_x = upscaled_tile.shape[-1] // tile.shape[-1] | |
| if output is None: | |
| output = torch.zeros( | |
| (1, 3, height * scale_y, width * scale_x), | |
| dtype=upscaled_tile.dtype, | |
| device=upscaled_tile.device, | |
| ) | |
| weights = torch.zeros_like(output) | |
| oy0, oy1 = y * scale_y, y1 * scale_y | |
| ox0, ox1 = x * scale_x, x1 * scale_x | |
| output[:, :, oy0:oy1, ox0:ox1] += upscaled_tile | |
| weights[:, :, oy0:oy1, ox0:ox1] += 1 | |
| # Normalize overlapping regions | |
| output = output / weights.clamp_min(1) | |
| return tensor_to_image(output) | |
| # ============================================================================ | |
| # ENHANCED DETAILER (Multi-stage processing) | |
| # ============================================================================ | |
| def smart_sharpen(image, strength=1.15): | |
| """ | |
| Smart sharpening with edge detection to avoid oversharpening smooth areas | |
| """ | |
| if strength <= 0: | |
| return image | |
| # Convert to array for processing | |
| img_array = np.array(image.convert("RGB")) | |
| # Apply adaptive sharpening | |
| if strength > 1.0: | |
| # Use ImageEnhance for basic sharpening | |
| enhanced = ImageEnhance.Sharpness(image).enhance(strength) | |
| # Additional edge-aware sharpening | |
| gray = image.convert("L") | |
| edges = gray.filter(ImageFilter.FIND_EDGES) | |
| edge_mask = edges.filter(ImageFilter.GaussianBlur(radius=1)) | |
| edge_mask = edge_mask.point(lambda x: min(x * 0.3, 255)) # Normalize edge strength | |
| # Blend sharpened version with original based on edge strength | |
| sharpened_array = np.array(enhanced) | |
| original_array = img_array | |
| edge_array = np.array(edge_mask).astype(float) / 255.0 | |
| # Create edge-aware blend | |
| for c in range(3): | |
| sharpened_array[:, :, c] = ( | |
| edge_array * sharpened_array[:, :, c] + | |
| (1 - edge_array) * original_array[:, :, c] | |
| ) | |
| image = Image.fromarray(np.clip(sharpened_array, 0, 255).astype(np.uint8)) | |
| return image | |
| def add_ultra_detail(image, strength=0.8): | |
| """ | |
| Add ultra-fine details using high-frequency enhancement | |
| """ | |
| if strength <= 0: | |
| return image | |
| # Apply high-pass filtering for detail extraction | |
| original = image.convert("RGB") | |
| blurred = original.filter(ImageFilter.GaussianBlur(radius=2)) | |
| # Extract high-frequency details | |
| high_freq = ImageChops.subtract(original, blurred) | |
| # Enhance the high-frequency component | |
| high_freq_enhanced = ImageEnhance.Contrast(high_freq).enhance(1.0 + strength) | |
| # Add enhanced details back to original | |
| result = ImageChops.add(original, high_freq_enhanced) | |
| return result | |
| def apply_high_frequency_details(image, amount=0.6): | |
| """ | |
| Apply high-frequency detail enhancement for crisp textures | |
| """ | |
| if amount <= 0: | |
| return image | |
| # Multiple scales of detail enhancement | |
| scales = [1, 2, 4] # Different blur radii for multi-scale details | |
| result = image.convert("RGB") | |
| for scale in scales: | |
| blurred = result.filter(ImageFilter.GaussianBlur(radius=scale)) | |
| high_freq = ImageChops.subtract(result, blurred) | |
| enhanced_hf = ImageEnhance.Contrast(high_freq).enhance(1.0 + amount * 0.3) | |
| result = ImageChops.add(result, enhanced_hf) | |
| return result | |
| # ============================================================================ | |
| # ENHANCED CLEANER (Face Restoration + Artifact Removal) | |
| # ============================================================================ | |
| def load_face_restoration_model(): | |
| """Load GFPGAN model for face restoration""" | |
| global _face_restoration_model | |
| if _face_restoration_model is not None: | |
| return _face_restoration_model | |
| try: | |
| # Try to import face restoration libraries | |
| import gfpgan | |
| from gfpgan import GFPGANer | |
| # Download and load model | |
| model_path = download_model_with_retry(FACE_RESTORATION_MODEL, FACE_RESTORATION_WEIGHTS) | |
| # Initialize GFPGANer | |
| restorer = GFPGANer( | |
| model_path=model_path, | |
| upscale=1, # We handle upscaling separately | |
| arch='clean', | |
| channel_multiplier=2, | |
| bg_upsampler=None | |
| ) | |
| _face_restoration_model = restorer | |
| print("โ Successfully loaded GFPGAN face restoration model") | |
| return _face_restoration_model | |
| except ImportError: | |
| print("โ ๏ธ GFPGAN not available, face restoration will use fallback methods") | |
| return None | |
| except Exception as e: | |
| print(f"โ Failed to load face restoration model: {e}") | |
| return None | |
| def detect_faces(image): | |
| """Detect faces in an image and return bounding boxes""" | |
| try: | |
| import cv2 | |
| import numpy as np | |
| # Convert PIL to numpy array | |
| img_array = np.array(image.convert("RGB")) | |
| gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY) | |
| # Load face cascade | |
| face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') | |
| faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30)) | |
| return faces | |
| except ImportError: | |
| print("โ ๏ธ OpenCV not available, using simple face detection fallback") | |
| # Simple fallback: assume center of image for portrait | |
| width, height = image.size | |
| if width > height: # Landscape | |
| return [] | |
| else: # Portrait | |
| face_size = min(width, height) // 2 | |
| x = (width - face_size) // 2 | |
| y = (height - face_size) // 2 | |
| return [[x, y, face_size, face_size]] | |
| except Exception as e: | |
| print(f"โ ๏ธ Face detection failed: {e}") | |
| return [] | |
| def restore_faces(image): | |
| """Restore faces in an image using GFPGAN""" | |
| restorer = load_face_restoration_model() | |
| if restorer is None: | |
| print("โ ๏ธ Face restoration model not available, using skin repair fallback") | |
| return repair_skin_texture(image) | |
| try: | |
| # Convert to numpy array | |
| img_array = np.array(image.convert("RGB")) | |
| # Restore faces | |
| restored_array, _ = restorer.enhance(img_array, has_aligned=False, only_center_face=False, paste_back=True) | |
| # Convert back to PIL | |
| restored_image = Image.fromarray(restored_array.astype(np.uint8)) | |
| return restored_image | |
| except Exception as e: | |
| print(f"โ ๏ธ Face restoration failed: {e}, using skin repair fallback") | |
| return repair_skin_texture(image) | |
| def remove_artifacts(image): | |
| """Remove compression artifacts and noise""" | |
| # Apply mild median filtering for noise reduction | |
| denoised = image.filter(ImageFilter.MedianFilter(size=3)) | |
| # Apply slight Gaussian blur to smooth artifacts | |
| smoothed = denoised.filter(ImageFilter.GaussianBlur(radius=0.5)) | |
| # Blend with original to preserve details | |
| result = Image.blend(image, smoothed, alpha=0.3) | |
| return result | |
| def enhanced_skin_repair(image): | |
| """Enhanced skin repair with better color detection and blending""" | |
| base = image.convert("RGB") | |
| # Improved skin detection using YCbCr with better thresholds | |
| ycbcr = np.asarray(base.convert("YCbCr")) | |
| y, cb, cr = ycbcr[:, :, 0], ycbcr[:, :, 1], ycbcr[:, :, 2] | |
| # More sophisticated skin detection | |
| skin_mask = ( | |
| (cr > 130) & (cr < 170) & | |
| (cb > 70) & (cb < 140) & | |
| (y > 80) # Exclude dark areas | |
| ).astype(np.uint8) * 255 | |
| # Apply morphological operations to clean up mask | |
| try: | |
| import cv2 | |
| kernel = np.ones((5, 5), np.uint8) | |
| skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel) | |
| skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel) | |
| skin_mask = cv2.GaussianBlur(skin_mask, (7, 7), 0) | |
| except ImportError: | |
| # Fallback without OpenCV | |
| from scipy import ndimage | |
| skin_mask = ndimage.binary_opening(skin_mask > 128, structure=np.ones((3, 3))).astype(np.uint8) * 255 | |
| skin_mask = ndimage.gaussian_filter(skin_mask, sigma=3) | |
| mask_image = Image.fromarray(skin_mask, mode="L") | |
| # Apply more sophisticated skin repair | |
| repaired = base.filter(ImageFilter.MedianFilter(size=3)) | |
| repaired = repaired.filter(ImageFilter.GaussianBlur(radius=0.4)) | |
| # Apply selective sharpening to non-skin areas | |
| non_skin = ImageOps.invert(mask_image) | |
| sharpened = ImageEnhance.Sharpness(base).enhance(1.15) | |
| # Blend repaired skin with sharpened non-skin areas | |
| blended = Image.composite(repaired, sharpened, mask_image) | |
| # Final enhancement | |
| result = ImageEnhance.Sharpness(blended).enhance(1.05) | |
| return result | |
| # Original skin repair functions (preserved for compatibility) | |
| def skin_repair_mask(image): | |
| ycbcr = np.asarray(image.convert("YCbCr")) | |
| cb = ycbcr[:, :, 1] | |
| cr = ycbcr[:, :, 2] | |
| mask = ( | |
| (cr >= 135) | |
| & (cr <= 180) | |
| & (cb >= 75) | |
| & (cb <= 135) | |
| ).astype(np.uint8) * 255 | |
| mask_image = Image.fromarray(mask, mode="L") | |
| return mask_image.filter(ImageFilter.GaussianBlur(radius=1.2)) | |
| def repair_skin_texture(image): | |
| base = image.convert("RGB") | |
| mask = skin_repair_mask(base) | |
| repaired = base.filter(ImageFilter.MedianFilter(size=3)).filter(ImageFilter.GaussianBlur(radius=0.35)) | |
| blended = Image.composite(repaired, base, mask) | |
| return ImageEnhance.Sharpness(blended).enhance(1.08) | |
| def add_film_grain(image, seed): | |
| base = image.convert("RGB") | |
| array = np.asarray(base).astype(np.float32) | |
| rng = np.random.default_rng(seed) | |
| grain = rng.normal(0.0, 255.0 * ENHANCE_GRAIN_STRENGTH, size=(array.shape[0], array.shape[1], 1)) | |
| array = np.clip(array + grain, 0, 255) | |
| return Image.fromarray(array.astype(np.uint8), mode="RGB") | |
| # ============================================================================ | |
| # ENHANCED APPLY ENHANCEMENT (Main enhancement pipeline) | |
| # ============================================================================ | |
| def apply_enhancement(image, enhance_mode, seed=0, progress=None): | |
| """ | |
| Apply various enhancement modes to the image | |
| Modes: | |
| - Off: No enhancement | |
| - Upscale Only: Just upscale the image | |
| - Clean & Restore: Remove artifacts, repair skin, restore faces | |
| - Max Detail: Full enhancement with detail boost | |
| - Face Enhance: Focus on face restoration | |
| - Full Enhance: Complete enhancement pipeline | |
| """ | |
| mode = enhance_mode or ENHANCE_MODE_OFF | |
| if mode not in ENHANCE_MODE_CHOICES: | |
| raise gr.Error(f"Unknown enhance mode: {mode}") | |
| if mode == ENHANCE_MODE_OFF: | |
| return image | |
| enhanced = image.convert("RGB") | |
| # Progress tracking | |
| total_steps = 0 | |
| if mode == ENHANCE_MODE_UPSCALE: | |
| total_steps = 1 | |
| elif mode == ENHANCE_MODE_CLEAN: | |
| total_steps = 3 | |
| elif mode == ENHANCE_MODE_MAX_DETAIL: | |
| total_steps = 4 | |
| elif mode == ENHANCE_MODE_FACE_ENHANCE: | |
| total_steps = 2 | |
| elif mode == ENHANCE_MODE_FULL_ENHANCE: | |
| total_steps = 5 | |
| step = 0 | |
| # Face Enhance Mode | |
| if mode == ENHANCE_MODE_FACE_ENHANCE: | |
| if progress: | |
| step += 1 | |
| progress(0.5 * step / total_steps, desc="Restoring faces...") | |
| enhanced = restore_faces(enhanced) | |
| if progress: | |
| step += 1 | |
| progress(0.5 * step / total_steps, desc="Upscaling...") | |
| enhanced = advanced_tile_upscale(enhanced) | |
| return enhanced | |
| # Clean & Restore Mode | |
| if mode in (ENHANCE_MODE_CLEAN, ENHANCE_MODE_FULL_ENHANCE): | |
| if progress: | |
| step += 1 | |
| progress(0.7 * step / total_steps, desc="Removing artifacts...") | |
| enhanced = remove_artifacts(enhanced) | |
| if progress: | |
| step += 1 | |
| progress(0.7 * step / total_steps, desc="Repairing skin and faces...") | |
| enhanced = enhanced_skin_repair(enhanced) | |
| # Also apply face restoration specifically | |
| enhanced = restore_faces(enhanced) | |
| # Upscale for all modes except Face Enhance (which already upscales) | |
| if mode in (ENHANCE_MODE_UPSCALE, ENHANCE_MODE_CLEAN, ENHANCE_MODE_MAX_DETAIL, ENHANCE_MODE_FULL_ENHANCE): | |
| if progress: | |
| step += 1 | |
| progress(0.8 * step / total_steps, desc="Upscaling image...") | |
| enhanced = advanced_tile_upscale(enhanced) | |
| # Detail Enhancement | |
| if mode in (ENHANCE_MODE_MAX_DETAIL, ENHANCE_MODE_FULL_ENHANCE): | |
| if progress: | |
| step += 1 | |
| progress(0.9 * step / total_steps, desc="Enhancing details...") | |
| enhanced = add_ultra_detail(enhanced, strength=0.7) | |
| enhanced = apply_high_frequency_details(enhanced, amount=0.5) | |
| enhanced = smart_sharpen(enhanced, strength=SMART_SHARPENING_STRENGTH) | |
| if progress: | |
| step += 1 | |
| progress(0.95 * step / total_steps, desc="Adding final grain...") | |
| enhanced = add_film_grain(enhanced, seed) | |
| return enhanced | |
| # ============================================================================ | |
| # UTILITY FUNCTIONS | |
| # ============================================================================ | |
| 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] | |
| # Gallery items can be filepath strings or (filepath, label) tuples. | |
| path = first[0] if isinstance(first, (list, tuple)) else first | |
| return gr.update(value=path) | |
| def check_gpu_memory(): | |
| """Check available GPU memory""" | |
| if device == "cuda": | |
| try: | |
| total = torch.cuda.get_device_properties(0).total_memory | |
| reserved = torch.cuda.memory_reserved(0) | |
| allocated = torch.cuda.memory_allocated(0) | |
| free = total - reserved | |
| print(f"GPU Memory: Total={total/1024**3:.2f}GB, " | |
| f"Reserved={reserved/1024**3:.2f}GB, " | |
| f"Allocated={allocated/1024**3:.2f}GB, " | |
| f"Free={free/1024**3:.2f}GB") | |
| return free > 1024**3 # Return True if more than 1GB free | |
| except Exception as e: | |
| print(f"Failed to check GPU memory: {e}") | |
| return True | |
| return True | |
| def clear_gpu_cache(): | |
| """Clear GPU cache to free up memory""" | |
| if device == "cuda": | |
| try: | |
| torch.cuda.empty_cache() | |
| import gc | |
| gc.collect() | |
| print("โ GPU cache cleared") | |
| except Exception as e: | |
| print(f"โ ๏ธ Failed to clear GPU cache: {e}") | |
| # ============================================================================ | |
| # MAIN INFERENCE FUNCTION (Enhanced) | |
| # ============================================================================ | |
| MAX_SEED = np.iinfo(np.int32).max | |
| 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, | |
| enhance_mode=ENHANCE_MODE_OFF, | |
| num_images_per_prompt=1, | |
| progress=gr.Progress(track_tqdm=True), | |
| ): | |
| """ | |
| Enhanced image generation with advanced editing and enhancement options | |
| """ | |
| # Hardcode the negative prompt as requested | |
| negative_prompt = " " | |
| if randomize_seed: | |
| seed = random.randint(0, MAX_SEED) | |
| # Set up the generator for reproducibility | |
| generator = torch.Generator(device=device).manual_seed(seed) | |
| # Load input images into PIL Images โ two optional slots. | |
| 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 | |
| # Fix for default 256x256 size | |
| if height == 256 and width == 256: | |
| height, width = None, None | |
| # Log generation parameters | |
| print(f"๐ฏ Generation Parameters:") | |
| print(f" Prompt: '{prompt}'") | |
| print(f" Negative Prompt: '{negative_prompt}'") | |
| print(f" Seed: {seed}, Steps: {num_inference_steps}, Guidance: {true_guidance_scale}") | |
| print(f" Size: {width}x{height}, Images: {num_images_per_prompt}") | |
| print(f" Enhance Mode: {enhance_mode}") | |
| # Check GPU memory before generation | |
| if not check_gpu_memory(): | |
| clear_gpu_cache() | |
| if not check_gpu_memory(): | |
| raise gr.Error("Insufficient GPU memory. Please reduce image size or close other applications.") | |
| # Generate the image | |
| try: | |
| 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 | |
| except Exception as e: | |
| clear_gpu_cache() | |
| raise gr.Error(f"Image generation failed: {e}") | |
| # Apply enhancement if requested | |
| if enhance_mode != ENHANCE_MODE_OFF: | |
| images_pil = [ | |
| apply_enhancement(img, enhance_mode, seed=seed + idx, progress=progress) | |
| for idx, img in enumerate(images_pil) | |
| ] | |
| # Save images to temporary files for proper serving | |
| output_paths = [] | |
| os.makedirs("outputs", exist_ok=True) | |
| for idx, img in enumerate(images_pil): | |
| output_path = f"outputs/output_{seed}_{idx}_{int(time.time()*1000)}.png" | |
| img.save(output_path) | |
| output_paths.append(output_path) | |
| # Clear GPU cache after generation | |
| clear_gpu_cache() | |
| # Return image paths, seed, and make buttons visible when their feature is configured. | |
| return output_paths, seed, gr.update(visible=True), gr.update(visible=bool(VIDEO_SPACE_ID)) | |
| # ============================================================================ | |
| # UI LAYOUT (Enhanced) | |
| # ============================================================================ | |
| css = """ | |
| #col-container { | |
| margin: 0 auto; | |
| max-width: 1024px; | |
| } | |
| #logo-title { | |
| text-align: center; | |
| } | |
| #logo-title h1 { | |
| margin-bottom: 0; | |
| } | |
| #logo-title h2 { | |
| color: #5b47d1; | |
| font-style: italic; | |
| margin-top: 0; | |
| } | |
| #edit_text{margin-top: -62px !important} | |
| .enhance-info { | |
| font-size: 0.9em; | |
| color: #666; | |
| margin-top: 5px; | |
| } | |
| """ | |
| with gr.Blocks(css=css) as demo: | |
| with gr.Column(elem_id="col-container"): | |
| gr.HTML(f""" | |
| <!-- v{APP_VERSION} --> | |
| <div id="logo-title"> | |
| <h1>Pro Realism Edit Studio - Enhanced</h1> | |
| <h2>Rapid Edit โก with Real-ESRGAN & Face Restoration</h2> | |
| </div> | |
| """) | |
| gr.Markdown(""" | |
| **๐ Powered by:** | |
| - [Qwen-Image-Edit-2511](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) | |
| - [Phr00t's Rapid-AIO v23](https://huggingface.co/Phr00t/Qwen-Image-Edit-Rapid-AIO) accelerated transformer | |
| - [Real-ESRGAN](https://huggingface.co/ai-forever/Real-ESRGAN) for high-quality upscaling | |
| - [GFPGAN](https://github.com/TencentARC/GFPGAN) for face restoration | |
| Upload an image and enter your prompt to edit it. The model uses your prompt exactly as provided. | |
| **๐ก Pro Tips:** | |
| - Use **Face Enhance** mode for portrait photography | |
| - Use **Max Detail** for product shots and textures | |
| - Use **Full Enhance** for comprehensive improvement | |
| """) | |
| 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...", | |
| ) | |
| enhance_mode = gr.Radio( | |
| label="Enhance Mode", | |
| choices=ENHANCE_MODE_CHOICES, | |
| value=ENHANCE_MODE_OFF, | |
| interactive=True, | |
| info="Choose enhancement level for your output" | |
| ) | |
| # Enhancement info | |
| enhance_info = gr.Markdown(""" | |
| **Enhancement Options:** | |
| - **Off**: No post-processing | |
| - **Upscale Only**: 4x upscaling with Real-ESRGAN | |
| - **Clean & Restore**: Artifact removal + skin/face restoration | |
| - **Max Detail**: Full detail enhancement with sharpening | |
| - **Face Enhance**: Specialized face restoration + upscaling | |
| - **Full Enhance**: Complete pipeline (clean + detail + face + upscale) | |
| """, visible=False, elem_classes="enhance-info") | |
| run_button = gr.Button("Generate! ๐จ", 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.Row(): | |
| height = gr.Slider( | |
| label="Height", | |
| minimum=256, | |
| maximum=2048, | |
| step=8, | |
| value=None, | |
| ) | |
| width = gr.Slider( | |
| label="Width", | |
| minimum=256, | |
| maximum=2048, | |
| step=8, | |
| value=None, | |
| ) | |
| gr.Markdown(""" | |
| **๐ง Performance Tips:** | |
| - Use 4 steps for fastest results | |
| - Increase steps (8-20) for better quality | |
| - Lower guidance scale for more creative freedom | |
| - Set custom dimensions for specific aspect ratios | |
| """) | |
| 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) | |
| turn_video_btn = gr.Button("๐ฌ Turn into Video", variant="secondary", size="sm", visible=False) | |
| output_video = gr.Video(label="Generated Video", autoplay=True, visible=False) | |
| with gr.Row(): | |
| 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=True # Made visible by default | |
| ) | |
| # Event handlers | |
| 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, | |
| height, | |
| width, | |
| enhance_mode, | |
| ], | |
| outputs=[result, seed, use_output_btn, turn_video_btn], | |
| ).then( | |
| fn=update_history, | |
| inputs=[result, history_gallery], | |
| outputs=history_gallery, | |
| ) | |
| # Show enhancement info when enhance mode is changed | |
| enhance_mode.change( | |
| fn=lambda mode: gr.update(visible=mode != ENHANCE_MODE_OFF), | |
| inputs=[enhance_mode], | |
| outputs=[enhance_info] | |
| ) | |
| # Use output as input button | |
| use_output_btn.click( | |
| fn=use_output_as_input, | |
| inputs=[result], | |
| outputs=[image_1] | |
| ) | |
| # History gallery event handlers | |
| history_gallery.select( | |
| fn=use_history_as_input, | |
| inputs=None, | |
| outputs=[image_1], | |
| ) | |
| clear_history_button.click( | |
| fn=lambda: [], | |
| inputs=None, | |
| outputs=history_gallery, | |
| ) | |
| turn_video_btn.click( | |
| fn=lambda: gr.update(visible=True), | |
| inputs=None, | |
| outputs=[output_video], | |
| ).then( | |
| fn=turn_into_video, | |
| inputs=[image_1, result, prompt], | |
| outputs=[output_video], | |
| ) | |
| if __name__ == "__main__": | |
| # Check GPU availability | |
| print(f"๐ฅ๏ธ Device: {device}") | |
| if device == "cuda": | |
| print(f"๐ฎ GPU: {torch.cuda.get_device_name(0)}") | |
| # Check memory | |
| check_gpu_memory() | |
| # Launch the app | |
| print(f"๐ Starting Pro Realism Edit Studio v{APP_VERSION}") | |
| print("=" * 60) | |
| demo.launch() |