diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d964c589..580d649bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,19 @@ ## Update for 2026-02-05 -Bugfix refresh - -- fix: add metadata restore to always-on scripts -- fix: improve wildcard weights parsing, thanks @Tillerz -- fix: `anima` model detection -- refactor: reorganize `cli` scripts -- upscalers: hqx, icbi -- ui: add CTD-NT64Light theme, thanks @resonantsky +- **UI** + - ui: add CTD-NT64Light theme, thanks @resonantsky +- **Internal** + - refactor: reorganize `cli` scripts +- **Upscalers** + - add support for [spandrel](https://github.com/chaiNNer-org/spandrel) + upscaling engine with suport for new upscaling families + - add two new ai upscalers: *RealPLKSR NomosWebPhoto* and *RealPLKSR AnimeSharpV2* + - add two new interpolation methods: *HQX* and *ICB* +- **Fixes** + - fix: add metadata restore to always-on scripts + - fix: improve wildcard weights parsing, thanks @Tillerz + - fix: `anima` model detection ## Update for 2026-02-04 diff --git a/modules/upscaler_spandrel.py b/modules/upscaler_spandrel.py new file mode 100644 index 000000000..975685efe --- /dev/null +++ b/modules/upscaler_spandrel.py @@ -0,0 +1,55 @@ +import os +import time +from PIL import Image +from modules.upscaler import Upscaler, UpscalerData +from modules import devices, paths +from modules.shared import log + + +MODELS = { + "Spandrel 4x RealPLKSR NomosWebPhoto": "https://huggingface.co/vladmandic/sdnext-upscalers/resolve/main/4xNomosWebPhoto_RealPLKSR.safetensors", + "Spandrel 2x RealPLKSR AnimeSharpV2": "https://huggingface.co/vladmandic/sdnext-upscalers/resolve/main/2x-AnimeSharpV2_RPLKSR_Sharp.pth", +} + +class UpscalerSpandrel(Upscaler): + def __init__(self, dirname=None): # pylint: disable=unused-argument + super().__init__(False) + self.name = "Spandrel" + self.model_path = os.path.join(paths.models_path, 'Spandrel') + self.user_path = os.path.join(paths.models_path, 'Spandrel') + self.selected = None + self.model = None + self.scalers = [] + for model_name, model_path in MODELS.items(): + scaler = UpscalerData(name=model_name, path=model_path, upscaler=self) + self.scalers.append(scaler) + + def process(self, img: Image.Image) -> Image.Image: + import torchvision.transforms.functional as TF + tensor = TF.to_tensor(img).unsqueeze(0).to(devices.device) + img = img.convert('RGB') + t0 = time.time() + with devices.inference_context(): + tensor = self.model(tensor) + tensor = tensor.clamp(0, 1).squeeze(0).cpu() + t1 = time.time() + upscaled = TF.to_pil_image(tensor) + log.debug(f'Upscale: name="{self.selected}" input={img.size} output={upscaled.size} time={t1 - t0:.2f}') + return upscaled + + def do_upscale(self, img: Image, selected_model=None): + from installer import install + if selected_model is None: + return img + install('spandrel') + try: + import spandrel + if (self.model is None) or (self.selected != selected_model): + self.selected = selected_model + model = self.find_model(selected_model) + self.model = spandrel.ModelLoader().load_from_file(model.local_data_path) + self.model.to(devices.device).eval() + return self.process(img) + except Exception as e: + log.error(f'Spandrel: {e}') + return img diff --git a/modules/upscaler_vae.py b/modules/upscaler_vae.py index b8887bc6a..a8c014ee4 100644 --- a/modules/upscaler_vae.py +++ b/modules/upscaler_vae.py @@ -1,3 +1,4 @@ +import time from PIL import Image from modules.upscaler import Upscaler, UpscalerData @@ -30,12 +31,15 @@ class UpscalerAsymmetricVAE(Upscaler): self.vae.eval() self.selected = selected_model shared.log.debug(f'Upscaler load: selected="{self.selected}" vae="{repo_id}"') + t0 = time.time() img = img.resize((8 * (img.width // 8), 8 * (img.height // 8)), resample=Image.Resampling.LANCZOS).convert('RGB') tensor = (F.pil_to_tensor(img).unsqueeze(0) / 255.0).to(device=devices.device, dtype=devices.dtype) self.vae = self.vae.to(device=devices.device) tensor = self.vae(tensor).sample upscaled = F.to_pil_image(tensor.squeeze().clamp(0.0, 1.0).float().cpu()) self.vae = self.vae.to(device=devices.cpu) + t1 = time.time() + shared.log.debug(f'Upscale: name="{self.selected}" input={img.size} output={upscaled.size} time={t1 - t0:.2f}') return upscaled @@ -73,6 +77,7 @@ class UpscalerWanUpscale(Upscaler): self.selected = selected_model shared.log.debug(f'Upscaler load: selected="{self.selected}" encode="{repo_encode}" decode="{repo_decode}"') + t0 = time.time() self.vae_encode = self.vae_encode.to(device=devices.device) tensor = (F.pil_to_tensor(img).unsqueeze(0).unsqueeze(2) / 255.0).to(device=devices.device, dtype=devices.dtype) tensor = self.vae_encode.encode(tensor).latent_dist.mode() @@ -84,4 +89,6 @@ class UpscalerWanUpscale(Upscaler): self.vae_decode.to(device=devices.cpu) upscaled = F.to_pil_image(tensor.squeeze().clamp(0.0, 1.0).float().cpu()) + t1 = time.time() + shared.log.debug(f'Upscale: name="{self.selected}" input={img.size} output={upscaled.size} time={t1 - t0:.2f}') return upscaled diff --git a/webui.py b/webui.py index 55e459781..de6ecf832 100644 --- a/webui.py +++ b/webui.py @@ -39,6 +39,7 @@ import modules.upscaler import modules.upscaler_simple import modules.upscaler_vae import modules.upscaler_algo +import modules.upscaler_spandrel import modules.extra_networks import modules.ui_extra_networks import modules.textual_inversion