""" SDXL + ControlNet-Tile engine — the lighter generative fallback. Runs when the Space is deployed on hardware where SUPIR is too heavy (e.g. a 16 GB T4). This is still a *generative* path: the image is bicubically enlarged to the target size, then an SDXL img2img pass conditioned by a tile ControlNet regenerates plausible fine detail at the new resolution (the community-standard "ControlNet Tile upscale" recipe) — not an ESRGAN sharpener. Models (both public): * base: stabilityai/stable-diffusion-xl-base-1.0 (fp16 variant) * control: xinsir/controlnet-tile-sdxl-1.0 (SDXL tile ControlNet) VRAM: roughly 12–16 GB in fp16 for a 2048x2048 output. """ from __future__ import annotations import logging import threading import torch from PIL import Image from . import UpscaleEngine, env_float, env_int, gpu_context log = logging.getLogger("upscale.sdxl_tile") BASE_REPO = "stabilityai/stable-diffusion-xl-base-1.0" CONTROL_REPO = "xinsir/controlnet-tile-sdxl-1.0" MAX_EDGE = env_int("SDXL_TILE_MAX_EDGE", 2048) # cap output long edge STRENGTH = env_float("SDXL_TILE_STRENGTH", 0.4) # img2img denoise amount CONTROL_SCALE = env_float("SDXL_TILE_CONTROL_SCALE", 0.6) STEPS = env_int("SDXL_TILE_STEPS", 25) GUIDANCE = env_float("SDXL_TILE_GUIDANCE", 5.0) POSITIVE = ( "highly detailed, sharp focus, natural textures, realistic, " "professional photography, 8k, intricate details" ) NEGATIVE = ( "blurry, out of focus, lowres, jpeg artifacts, oversmoothed, " "painting, cartoon, 3d render, watermark, text" ) class SdxlTileEngine(UpscaleEngine): def __init__(self) -> None: self._lock = threading.Lock() self._pipe = None self._device = "cuda" if torch.cuda.is_available() else "cpu" def name(self) -> str: return "sdxl_tile" def _load(self): with self._lock: if self._pipe is not None: return from diffusers import ( AutoencoderKL, ControlNetModel, StableDiffusionXLControlNetPipeline, ) log.info("Loading ControlNet (%s) ...", CONTROL_REPO) controlnet = ControlNetModel.from_pretrained( CONTROL_REPO, torch_dtype=torch.float16 ) log.info("Loading SDXL base (%s) ...", BASE_REPO) vae = AutoencoderKL.from_pretrained( BASE_REPO, subfolder="vae", torch_dtype=torch.float16 ) pipe = StableDiffusionXLControlNetPipeline.from_pretrained( BASE_REPO, controlnet=controlnet, vae=vae, torch_dtype=torch.float16, variant="fp16", use_safetensors=True, ) pipe = pipe.to(self._device) pipe.enable_vae_tiling() pipe.enable_vae_slicing() # enable_attention_slicing keeps peak VRAM low on 16 GB cards. try: pipe.enable_attention_slicing() except Exception: pass self._pipe = pipe log.info("SDXL + ControlNet-Tile ready on %s", self._device) def load(self) -> None: with gpu_context(): self._load() def upscale(self, image: Image.Image, scale: int) -> Image.Image: with gpu_context(): self._load() pipe = self._pipe assert pipe is not None rgb = image.convert("RGB") w, h = rgb.size w, h = w * scale, h * scale # Keep the long edge within the model's comfortable range. if max(w, h) > MAX_EDGE: k = MAX_EDGE / max(w, h) w, h = round(w * k), round(h * k) w, h = w - w % 8, h - h % 8 # SDXL latents want /8 dims init = rgb.resize((w, h), Image.LANCZOS) result = pipe( prompt=POSITIVE, negative_prompt=NEGATIVE, image=init, # both the init image and the tile-control input strength=STRENGTH, controlnet_conditioning_scale=CONTROL_SCALE, num_inference_steps=STEPS, guidance_scale=GUIDANCE, generator=torch.Generator(device=self._device).manual_seed(0), ).images[0] return result.convert("RGB")