clopeux's picture
Upload app.py with huggingface_hub
c12430b verified
Raw
History Blame Contribute Delete
43.1 kB
#!/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
@spaces.GPU(duration=60)
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()