|
|
import gradio as gr
|
|
|
import torch
|
|
|
import gc
|
|
|
from PIL import Image
|
|
|
import numpy as np
|
|
|
import logging
|
|
|
import io
|
|
|
import os
|
|
|
import requests
|
|
|
from spandrel import ModelLoader
|
|
|
from abc import ABC, abstractmethod
|
|
|
from typing import Optional, Tuple, Dict
|
|
|
import psutil
|
|
|
import time
|
|
|
import traceback
|
|
|
|
|
|
|
|
|
class Config:
|
|
|
"""Configuration settings for the application."""
|
|
|
MODEL_DIR = "weights"
|
|
|
REALESRGAN_URL = "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth"
|
|
|
REALESRGAN_FILENAME = "RealESRGAN_x2plus.pth"
|
|
|
|
|
|
|
|
|
SPAN_URL = "https://huggingface.co/Phips/2xNomosUni_span_multijpg/resolve/main/2xNomosUni_span_multijpg.safetensors"
|
|
|
SPAN_FILENAME = "2xNomosUni_span_multijpg.safetensors"
|
|
|
HATS_URL = "https://huggingface.co/Phips/4xNomos8kSCHAT-S/resolve/main/4xNomos8kSCHAT-S.safetensors"
|
|
|
HATS_FILENAME = "4xNomos8kSCHAT-S.safetensors"
|
|
|
|
|
|
DEVICE = "cpu"
|
|
|
|
|
|
@staticmethod
|
|
|
def ensure_model_dir():
|
|
|
if not os.path.exists(Config.MODEL_DIR):
|
|
|
os.makedirs(Config.MODEL_DIR)
|
|
|
|
|
|
|
|
|
class LogCapture(io.StringIO):
|
|
|
"""Custom StringIO to capture logs."""
|
|
|
pass
|
|
|
|
|
|
log_capture_string = LogCapture()
|
|
|
ch = logging.StreamHandler(log_capture_string)
|
|
|
ch.setLevel(logging.INFO)
|
|
|
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
|
ch.setFormatter(formatter)
|
|
|
|
|
|
logger = logging.getLogger("UpscalerApp")
|
|
|
logger.setLevel(logging.INFO)
|
|
|
logger.addHandler(ch)
|
|
|
|
|
|
def get_logs() -> str:
|
|
|
"""Retrieve captured logs."""
|
|
|
return log_capture_string.getvalue()
|
|
|
|
|
|
|
|
|
def get_system_usage() -> str:
|
|
|
"""Returns current CPU and RAM usage."""
|
|
|
cpu_percent = psutil.cpu_percent()
|
|
|
ram_percent = psutil.virtual_memory().percent
|
|
|
ram_used_gb = psutil.virtual_memory().used / (1024 ** 3)
|
|
|
return f"CPU: {cpu_percent}% | RAM: {ram_percent}% ({ram_used_gb:.1f} GB used)"
|
|
|
|
|
|
|
|
|
class UpscalerStrategy(ABC):
|
|
|
"""Abstract base class for upscaling strategies."""
|
|
|
|
|
|
def __init__(self):
|
|
|
self.model = None
|
|
|
self.name = "Unknown"
|
|
|
|
|
|
@abstractmethod
|
|
|
def load(self) -> None:
|
|
|
"""Load the model into memory."""
|
|
|
pass
|
|
|
|
|
|
@abstractmethod
|
|
|
def upscale(self, image: Image.Image, **kwargs) -> Image.Image:
|
|
|
"""Upscale the given image."""
|
|
|
pass
|
|
|
|
|
|
def unload(self) -> None:
|
|
|
"""Unload the model to free memory."""
|
|
|
if self.model is not None:
|
|
|
del self.model
|
|
|
self.model = None
|
|
|
gc.collect()
|
|
|
logger.info(f"Unloaded {self.name}")
|
|
|
|
|
|
|
|
|
def manual_tile_upscale(model, img_tensor, tile_size=256, tile_pad=10, scale=2):
|
|
|
"""
|
|
|
Low-level tiling implementation for custom models.
|
|
|
Prevents OOM by processing image in chunks.
|
|
|
"""
|
|
|
B, C, H, W = img_tensor.shape
|
|
|
|
|
|
|
|
|
tile_h = (H + tile_size - 1) // tile_size
|
|
|
tile_w = (W + tile_size - 1) // tile_size
|
|
|
|
|
|
output = torch.zeros(B, C, H * scale, W * scale,
|
|
|
device=img_tensor.device, dtype=img_tensor.dtype)
|
|
|
|
|
|
for th in range(tile_h):
|
|
|
for tw in range(tile_w):
|
|
|
|
|
|
x1 = th * tile_size
|
|
|
y1 = tw * tile_size
|
|
|
x2 = min((th + 1) * tile_size, H)
|
|
|
y2 = min((tw + 1) * tile_size, W)
|
|
|
|
|
|
|
|
|
x1_pad = max(0, x1 - tile_pad)
|
|
|
y1_pad = max(0, y1 - tile_pad)
|
|
|
x2_pad = min(H, x2 + tile_pad)
|
|
|
y2_pad = min(W, y2 + tile_pad)
|
|
|
|
|
|
|
|
|
tile = img_tensor[:, :, x1_pad:x2_pad, y1_pad:y2_pad]
|
|
|
|
|
|
|
|
|
with torch.no_grad():
|
|
|
tile_out = model(tile)
|
|
|
|
|
|
|
|
|
halo_x1 = (x1 - x1_pad) * scale
|
|
|
halo_y1 = (y1 - y1_pad) * scale
|
|
|
out_x2 = halo_x1 + (x2 - x1) * scale
|
|
|
out_y2 = halo_y1 + (y2 - y1) * scale
|
|
|
|
|
|
|
|
|
output[:, :, x1*scale:x2*scale, y1*scale:y2*scale] = \
|
|
|
tile_out[:, :, halo_x1:out_x2, halo_y1:out_y2]
|
|
|
|
|
|
return output
|
|
|
|
|
|
def select_tile_config(height, width):
|
|
|
"""
|
|
|
Dynamically select tile size based on image resolution.
|
|
|
"""
|
|
|
megapixels = (height * width) / (1024 ** 2)
|
|
|
|
|
|
if megapixels < 2:
|
|
|
return {'tile': 512, 'tile_pad': 10}
|
|
|
elif megapixels < 6:
|
|
|
return {'tile': 384, 'tile_pad': 15}
|
|
|
elif megapixels < 16:
|
|
|
return {'tile': 256, 'tile_pad': 20}
|
|
|
else:
|
|
|
return {'tile': 128, 'tile_pad': 25}
|
|
|
|
|
|
|
|
|
|
|
|
class RealESRGANStrategy(UpscalerStrategy):
|
|
|
def __init__(self):
|
|
|
super().__init__()
|
|
|
self.name = "RealESRGAN x2"
|
|
|
self.compiled = False
|
|
|
|
|
|
def load(self) -> None:
|
|
|
if self.model is None:
|
|
|
logger.info(f"Loading {self.name}...")
|
|
|
Config.ensure_model_dir()
|
|
|
model_path = os.path.join(Config.MODEL_DIR, Config.REALESRGAN_FILENAME)
|
|
|
|
|
|
if not os.path.exists(model_path):
|
|
|
logger.info(f"Downloading {Config.REALESRGAN_FILENAME}...")
|
|
|
try:
|
|
|
response = requests.get(Config.REALESRGAN_URL, stream=True)
|
|
|
response.raise_for_status()
|
|
|
with open(model_path, 'wb') as f:
|
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
|
f.write(chunk)
|
|
|
logger.info("Download complete.")
|
|
|
except Exception as e:
|
|
|
logger.error(f"Failed to download model: {e}")
|
|
|
raise
|
|
|
|
|
|
try:
|
|
|
self.model = ModelLoader().load_from_file(model_path)
|
|
|
self.model.eval()
|
|
|
self.model.to(Config.DEVICE)
|
|
|
|
|
|
|
|
|
if not self.compiled:
|
|
|
try:
|
|
|
|
|
|
if Config.DEVICE == 'cuda':
|
|
|
self.model = torch.compile(self.model, mode='reduce-overhead')
|
|
|
logger.info("[INFO] torch.compile enabled (reduce-overhead mode)")
|
|
|
elif os.name == 'nt' and Config.DEVICE == 'cpu':
|
|
|
|
|
|
|
|
|
logger.info("[INFO] Skipping torch.compile on Windows CPU to avoid MSVC requirement.")
|
|
|
elif (psutil.cpu_count(logical=False) or 0) < 4 and Config.DEVICE == 'cpu':
|
|
|
|
|
|
logger.info("[INFO] Skipping torch.compile on low-core CPU to prevent timeout.")
|
|
|
else:
|
|
|
|
|
|
self.model = torch.compile(self.model)
|
|
|
logger.info("[SUCCESS] torch.compile enabled (default mode)")
|
|
|
|
|
|
self.compiled = True
|
|
|
except Exception as e:
|
|
|
logger.warning(f"[WARNING] torch.compile not available or failed: {e}")
|
|
|
self.compiled = True
|
|
|
|
|
|
logger.info(f"{self.name} loaded successfully.")
|
|
|
except Exception as e:
|
|
|
logger.error(f"Failed to load model architecture: {e}")
|
|
|
raise
|
|
|
|
|
|
def upscale(self, image: Image.Image, **kwargs) -> Image.Image:
|
|
|
if self.model is None:
|
|
|
self.load()
|
|
|
|
|
|
logger.info(f"Starting inference with {self.name}...")
|
|
|
start_time = time.time()
|
|
|
|
|
|
img_np = np.array(image).astype(np.float32) / 255.0
|
|
|
img_tensor = torch.from_numpy(img_np).permute(2, 0, 1).unsqueeze(0).to(Config.DEVICE)
|
|
|
|
|
|
|
|
|
h, w = img_np.shape[:2]
|
|
|
tile_config = select_tile_config(h, w)
|
|
|
logger.info(f"Using tile config: {tile_config}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dtype = torch.float16 if Config.DEVICE == 'cuda' else torch.bfloat16
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
context = torch.autocast(device_type=Config.DEVICE, dtype=dtype) if Config.DEVICE != 'cpu' else torch.no_grad()
|
|
|
|
|
|
with context:
|
|
|
if tile_config['tile'] > 0:
|
|
|
output_tensor = manual_tile_upscale(
|
|
|
self.model,
|
|
|
img_tensor,
|
|
|
tile_size=tile_config['tile'],
|
|
|
tile_pad=tile_config['tile_pad'],
|
|
|
scale=2
|
|
|
)
|
|
|
else:
|
|
|
output_tensor = self.model(img_tensor)
|
|
|
except Exception as e:
|
|
|
logger.warning(f"AMP/Tiling failed, falling back to standard FP32: {e}")
|
|
|
|
|
|
output_tensor = self.model(img_tensor)
|
|
|
|
|
|
output_np = output_tensor.squeeze(0).permute(1, 2, 0).clamp(0, 1).float().cpu().numpy()
|
|
|
output_np = (output_np * 255.0).round().astype(np.uint8)
|
|
|
|
|
|
elapsed = time.time() - start_time
|
|
|
logger.info(f"Inference finished in {elapsed:.2f}s")
|
|
|
|
|
|
|
|
|
output_megapixels = (output_np.shape[0] * output_np.shape[1]) / (1024 ** 2)
|
|
|
throughput = output_megapixels / elapsed
|
|
|
logger.info(f"Speed: {throughput:.2f} MP/s")
|
|
|
|
|
|
return Image.fromarray(output_np)
|
|
|
|
|
|
class SpanStrategy(UpscalerStrategy):
|
|
|
def __init__(self):
|
|
|
super().__init__()
|
|
|
self.name = "SPAN (NomosUni) x2"
|
|
|
self.compiled = False
|
|
|
|
|
|
def load(self) -> None:
|
|
|
if self.model is None:
|
|
|
logger.info(f"Loading {self.name}...")
|
|
|
Config.ensure_model_dir()
|
|
|
model_path = os.path.join(Config.MODEL_DIR, Config.SPAN_FILENAME)
|
|
|
|
|
|
if not os.path.exists(model_path):
|
|
|
logger.info(f"Downloading {Config.SPAN_FILENAME}...")
|
|
|
try:
|
|
|
response = requests.get(Config.SPAN_URL, stream=True)
|
|
|
response.raise_for_status()
|
|
|
with open(model_path, 'wb') as f:
|
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
|
f.write(chunk)
|
|
|
logger.info("Download complete.")
|
|
|
except Exception as e:
|
|
|
logger.error(f"Failed to download model: {e}")
|
|
|
raise
|
|
|
|
|
|
try:
|
|
|
self.model = ModelLoader().load_from_file(model_path)
|
|
|
self.model.eval()
|
|
|
self.model.to(Config.DEVICE)
|
|
|
|
|
|
|
|
|
if not self.compiled:
|
|
|
try:
|
|
|
if Config.DEVICE == 'cuda':
|
|
|
self.model = torch.compile(self.model, mode='reduce-overhead')
|
|
|
logger.info("[INFO] torch.compile enabled (reduce-overhead mode)")
|
|
|
elif os.name == 'nt' and Config.DEVICE == 'cpu':
|
|
|
logger.info("[INFO] Skipping torch.compile on Windows CPU.")
|
|
|
elif (psutil.cpu_count(logical=False) or 0) < 4 and Config.DEVICE == 'cpu':
|
|
|
logger.info("[INFO] Skipping torch.compile on low-core CPU.")
|
|
|
else:
|
|
|
|
|
|
logger.info("[INFO] Skipping torch.compile for SPAN (incompatible architecture).")
|
|
|
|
|
|
self.compiled = True
|
|
|
except Exception:
|
|
|
self.compiled = True
|
|
|
|
|
|
logger.info(f"{self.name} loaded successfully.")
|
|
|
except Exception as e:
|
|
|
logger.error(f"Failed to load model architecture: {e}")
|
|
|
raise
|
|
|
|
|
|
def upscale(self, image: Image.Image, **kwargs) -> Image.Image:
|
|
|
if self.model is None:
|
|
|
self.load()
|
|
|
|
|
|
logger.info(f"Starting inference with {self.name}...")
|
|
|
start_time = time.time()
|
|
|
|
|
|
img_np = np.array(image).astype(np.float32) / 255.0
|
|
|
img_tensor = torch.from_numpy(img_np).permute(2, 0, 1).unsqueeze(0).to(Config.DEVICE)
|
|
|
|
|
|
|
|
|
h, w = img_np.shape[:2]
|
|
|
tile_config = select_tile_config(h, w)
|
|
|
|
|
|
|
|
|
|
|
|
dtype = torch.float32 if Config.DEVICE == 'cpu' else torch.float16
|
|
|
|
|
|
try:
|
|
|
|
|
|
context = torch.autocast(device_type=Config.DEVICE, dtype=dtype) if Config.DEVICE != 'cpu' else torch.no_grad()
|
|
|
|
|
|
with context:
|
|
|
if tile_config['tile'] > 0:
|
|
|
output_tensor = manual_tile_upscale(
|
|
|
self.model,
|
|
|
img_tensor,
|
|
|
tile_size=tile_config['tile'],
|
|
|
tile_pad=tile_config['tile_pad'],
|
|
|
scale=2
|
|
|
)
|
|
|
else:
|
|
|
output_tensor = self.model(img_tensor)
|
|
|
except Exception as e:
|
|
|
logger.warning(f"AMP/Tiling failed, falling back: {e}")
|
|
|
output_tensor = self.model(img_tensor)
|
|
|
|
|
|
output_np = output_tensor.squeeze(0).permute(1, 2, 0).clamp(0, 1).float().cpu().numpy()
|
|
|
output_np = (output_np * 255.0).round().astype(np.uint8)
|
|
|
|
|
|
elapsed = time.time() - start_time
|
|
|
logger.info(f"Inference finished in {elapsed:.2f}s")
|
|
|
return Image.fromarray(output_np)
|
|
|
|
|
|
class HatsStrategy(UpscalerStrategy):
|
|
|
def __init__(self):
|
|
|
super().__init__()
|
|
|
self.name = "HAT-S x4"
|
|
|
self.compiled = False
|
|
|
|
|
|
def load(self) -> None:
|
|
|
if self.model is None:
|
|
|
logger.info(f"Loading {self.name}...")
|
|
|
Config.ensure_model_dir()
|
|
|
model_path = os.path.join(Config.MODEL_DIR, Config.HATS_FILENAME)
|
|
|
|
|
|
if not os.path.exists(model_path):
|
|
|
logger.info(f"Downloading {Config.HATS_FILENAME}...")
|
|
|
try:
|
|
|
response = requests.get(Config.HATS_URL, stream=True)
|
|
|
response.raise_for_status()
|
|
|
with open(model_path, 'wb') as f:
|
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
|
f.write(chunk)
|
|
|
logger.info("Download complete.")
|
|
|
except Exception as e:
|
|
|
logger.error(f"Failed to download model: {e}")
|
|
|
raise
|
|
|
|
|
|
try:
|
|
|
self.model = ModelLoader().load_from_file(model_path)
|
|
|
self.model.eval()
|
|
|
self.model.to(Config.DEVICE)
|
|
|
|
|
|
if not self.compiled:
|
|
|
try:
|
|
|
if Config.DEVICE == 'cuda':
|
|
|
self.model = torch.compile(self.model, mode='reduce-overhead')
|
|
|
elif os.name == 'nt' and Config.DEVICE == 'cpu':
|
|
|
pass
|
|
|
elif (psutil.cpu_count(logical=False) or 0) < 4 and Config.DEVICE == 'cpu':
|
|
|
pass
|
|
|
else:
|
|
|
|
|
|
logger.info("[INFO] Skipping torch.compile for HAT-S (incompatible architecture).")
|
|
|
|
|
|
self.compiled = True
|
|
|
except Exception:
|
|
|
self.compiled = True
|
|
|
|
|
|
logger.info(f"{self.name} loaded successfully.")
|
|
|
except Exception as e:
|
|
|
logger.error(f"Failed to load model architecture: {e}")
|
|
|
raise
|
|
|
|
|
|
def upscale(self, image: Image.Image, **kwargs) -> Image.Image:
|
|
|
if self.model is None:
|
|
|
self.load()
|
|
|
|
|
|
logger.info(f"Starting inference with {self.name}...")
|
|
|
start_time = time.time()
|
|
|
|
|
|
img_np = np.array(image).astype(np.float32) / 255.0
|
|
|
img_tensor = torch.from_numpy(img_np).permute(2, 0, 1).unsqueeze(0).to(Config.DEVICE)
|
|
|
|
|
|
h, w = img_np.shape[:2]
|
|
|
tile_config = select_tile_config(h, w)
|
|
|
|
|
|
dtype = torch.float16 if Config.DEVICE == 'cuda' else torch.float32
|
|
|
|
|
|
try:
|
|
|
context = torch.autocast(device_type=Config.DEVICE, dtype=dtype) if Config.DEVICE != 'cpu' else torch.no_grad()
|
|
|
with context:
|
|
|
if tile_config['tile'] > 0:
|
|
|
output_tensor = manual_tile_upscale(
|
|
|
self.model,
|
|
|
img_tensor,
|
|
|
tile_size=tile_config['tile'],
|
|
|
tile_pad=tile_config['tile_pad'],
|
|
|
scale=4
|
|
|
)
|
|
|
else:
|
|
|
output_tensor = self.model(img_tensor)
|
|
|
except Exception as e:
|
|
|
logger.warning(f"AMP/Tiling failed, falling back: {e}")
|
|
|
output_tensor = self.model(img_tensor)
|
|
|
|
|
|
output_np = output_tensor.squeeze(0).permute(1, 2, 0).clamp(0, 1).float().cpu().numpy()
|
|
|
output_np = (output_np * 255.0).round().astype(np.uint8)
|
|
|
|
|
|
elapsed = time.time() - start_time
|
|
|
logger.info(f"Inference finished in {elapsed:.2f}s")
|
|
|
return Image.fromarray(output_np)
|
|
|
|
|
|
|
|
|
class UpscalerManager:
|
|
|
"""Manages model lifecycle and selection."""
|
|
|
def __init__(self):
|
|
|
self.strategies: Dict[str, UpscalerStrategy] = {
|
|
|
"SPAN (NomosUni) x2": SpanStrategy(),
|
|
|
"RealESRGAN x2": RealESRGANStrategy(),
|
|
|
"HAT-S x4": HatsStrategy()
|
|
|
}
|
|
|
self.current_model_name: Optional[str] = None
|
|
|
|
|
|
def get_strategy(self, name: str) -> UpscalerStrategy:
|
|
|
if name not in self.strategies:
|
|
|
raise ValueError(f"Model {name} not found.")
|
|
|
|
|
|
|
|
|
|
|
|
if self.current_model_name != name:
|
|
|
if self.current_model_name is not None:
|
|
|
logger.info(f"Switching models: Unloading {self.current_model_name}...")
|
|
|
self.strategies[self.current_model_name].unload()
|
|
|
self.current_model_name = name
|
|
|
|
|
|
return self.strategies[name]
|
|
|
|
|
|
def unload_all(self):
|
|
|
"""Unload all models to free memory."""
|
|
|
for strategy in self.strategies.values():
|
|
|
strategy.unload()
|
|
|
gc.collect()
|
|
|
logger.info("All models unloaded.")
|
|
|
|
|
|
manager = UpscalerManager()
|
|
|
|
|
|
|
|
|
def process_image(input_img: Image.Image, model_name: str, output_format: str) -> Tuple[Optional[str], str, str]:
|
|
|
if input_img is None:
|
|
|
return None, get_logs(), get_system_usage()
|
|
|
|
|
|
try:
|
|
|
strategy = manager.get_strategy(model_name)
|
|
|
|
|
|
output_img = strategy.upscale(input_img)
|
|
|
|
|
|
|
|
|
output_path = f"output.{output_format.lower()}"
|
|
|
|
|
|
|
|
|
if output_format.lower() in ['jpeg', 'jpg'] and output_img.mode == 'RGBA':
|
|
|
output_img = output_img.convert('RGB')
|
|
|
|
|
|
output_img.save(output_path, format=output_format)
|
|
|
|
|
|
|
|
|
gc.collect()
|
|
|
|
|
|
return output_path, get_logs(), get_system_usage()
|
|
|
except Exception as e:
|
|
|
error_msg = f"Critical Error: {str(e)}\n{traceback.format_exc()}"
|
|
|
logger.error(error_msg)
|
|
|
return None, get_logs() + "\n\n" + error_msg, get_system_usage()
|
|
|
|
|
|
def unload_models():
|
|
|
manager.unload_all()
|
|
|
return get_logs(), get_system_usage()
|
|
|
|
|
|
|
|
|
desc = """
|
|
|
# Universal Upscaler Pro (CPU Optimized)
|
|
|
|
|
|
This application provides state-of-the-art (SOTA) image upscaling running entirely on CPU, optimized for free-tier cloud environments.
|
|
|
|
|
|
### Available Models
|
|
|
|
|
|
| Model | Scale | Best For | License |
|
|
|
| :--- | :--- | :--- | :--- |
|
|
|
| **SPAN (NomosUni)** | x2 | **Speed & General Use**. Extremely fast, parameter-free attention network. | Apache 2.0 |
|
|
|
| **RealESRGAN** | x2 | **Robustness**. Excellent at removing JPEG artifacts and noise. | BSD 3-Clause |
|
|
|
| **HAT-S** | x4 | **Texture Detail**. Hybrid Attention Transformer for high-fidelity restoration. | MIT |
|
|
|
|
|
|
### Attributions & Credits
|
|
|
|
|
|
* **Real-ESRGAN**: [Wang et al., 2021](https://github.com/xinntao/Real-ESRGAN). *Real-ESRGAN: Training Real-World Blind Super-Resolution with Pure Synthetic Data*.
|
|
|
* **SPAN**: [Zhang et al., 2023](https://github.com/hongyuanyu/SPAN). *Swift Parameter-free Attention Network for Efficient Super-Resolution*.
|
|
|
* **HAT**: [Chen et al., 2023](https://github.com/XPixelGroup/HAT). *Activating Activation Functions for Image Restoration*.
|
|
|
* **NomosUni**: Custom SPAN training by [Phhofm](https://github.com/Phhofm).
|
|
|
"""
|
|
|
|
|
|
with gr.Blocks(title="Universal Upscaler Pro") as iface:
|
|
|
gr.Markdown(desc)
|
|
|
|
|
|
with gr.Row():
|
|
|
with gr.Column(scale=1, min_width=300):
|
|
|
input_image = gr.Image(type="pil", label="Input Image", height=400)
|
|
|
|
|
|
with gr.Row():
|
|
|
model_selector = gr.Dropdown(
|
|
|
choices=list(manager.strategies.keys()),
|
|
|
value="SPAN (NomosUni) x2",
|
|
|
label="Model Architecture",
|
|
|
scale=2
|
|
|
)
|
|
|
output_format = gr.Dropdown(
|
|
|
choices=["PNG", "JPEG", "WEBP"],
|
|
|
value="PNG",
|
|
|
label="Output Format",
|
|
|
scale=1
|
|
|
)
|
|
|
|
|
|
submit_btn = gr.Button("Upscale Image", variant="primary", size="lg")
|
|
|
|
|
|
with gr.Accordion("Advanced Settings", open=False):
|
|
|
unload_btn = gr.Button("Unload All Models (Free RAM)", variant="secondary")
|
|
|
system_info = gr.Label(value=get_system_usage(), label="System Status")
|
|
|
|
|
|
with gr.Column(scale=1, min_width=300):
|
|
|
output_image = gr.Image(type="filepath", label="Upscaled Result", height=400)
|
|
|
logs_output = gr.TextArea(label="Execution Logs", interactive=False, lines=8)
|
|
|
|
|
|
|
|
|
submit_btn.click(
|
|
|
fn=process_image,
|
|
|
inputs=[input_image, model_selector, output_format],
|
|
|
outputs=[output_image, logs_output, system_info]
|
|
|
)
|
|
|
|
|
|
unload_btn.click(
|
|
|
fn=unload_models,
|
|
|
inputs=[],
|
|
|
outputs=[logs_output, system_info]
|
|
|
)
|
|
|
|
|
|
iface.launch()
|
|
|
|