"""Gradio demo: compress PBR texture sets to Intel TSNC format.""" from __future__ import annotations import tempfile from pathlib import Path import install_dependencies install_dependencies.install_private_package() try: import spaces ON_SPACES = True except ImportError: ON_SPACES = False def _gpu_decorator(**kwargs): def wrapper(fn): return fn return wrapper class _SpacesShim: GPU = staticmethod(_gpu_decorator) spaces = _SpacesShim() import gradio as gr import numpy as np import torch from numpy import iinfo from tsnc.hypernetwork import Hypernetwork, functional_bcf1, write_tsnc HF_MODEL_ID = "belcour/egsr2026_hypernetwork" CFG_FILE = "karajan_ZDitTv2_2026_06_10_lod.cfg" # ZeroGPU: CUDA is not visible in the main process until after .to("cuda"). device = "cuda" if torch.cuda.is_available() else "cpu" print(f"Loading hypernetwork on {device}...") hypernetwork = Hypernetwork.from_pretrained(HF_MODEL_ID, CFG_FILE) hypernetwork.eval().to(device) print("Hypernetwork ready.") def _validate_inputs(diffuse, normal, arm) -> tuple[int, int]: if diffuse is None or normal is None or arm is None: raise gr.Error("Please upload diffuse, normal, and ARM textures.") images = [("diffuse", diffuse), ("normal", normal), ("arm", arm)] shapes = {name: img.shape[:2] for name, img in images} ref_shape = shapes["diffuse"] for name, shape in shapes.items(): if shape != ref_shape: raise gr.Error( f"All textures must have the same resolution. " f"Diffuse is {ref_shape[0]}x{ref_shape[1]}, {name} is {shape[0]}x{shape[1]}." ) height, width = ref_shape if height != width: raise gr.Error(f"Textures must be square (got {height}x{width}).") return height, width def _numpy_to_tensor(img: np.ndarray) -> torch.Tensor: if iinfo(img.dtype).max > 255: img = img.astype(np.float32) / 65535.0 else: img = img.astype(np.float32) / 255.0 if img.ndim == 2: img = img[..., None] else: img = img[..., :3] return torch.tensor(img, dtype=torch.float32) def _stack_textures(diffuse: np.ndarray, normal: np.ndarray, arm: np.ndarray) -> torch.Tensor: tensors = [_numpy_to_tensor(img) for img in (diffuse, normal, arm)] ts_tensor = torch.cat(tensors, dim=-1).to(device) return ts_tensor[None].permute(0, 3, 1, 2) def _write_tsnc_to_temp(bcf1s, wb) -> str: outdir = tempfile.mkdtemp(prefix="tsnc_") out_name = "compressed.tsnc" write_tsnc(bcf1s, wb, cfg=hypernetwork.cfg, outdir=outdir, out_name=out_name) return str(Path(outdir) / out_name) def _gpu_duration(diffuse, normal, arm) -> int: """Scale GPU quota with texture resolution (square maps).""" for img in (diffuse, normal, arm): if img is not None: return max(120, int(img.shape[0] * img.shape[1] / 8192)) return 120 @spaces.GPU(duration=_gpu_duration) def compress(diffuse, normal, arm): _height, width = _validate_inputs(diffuse, normal, arm) with torch.inference_mode(): ts_tensor = _stack_textures(diffuse, normal, arm) bcf1s, wb = hypernetwork(ts_tensor) ts_out, _ = functional_bcf1( bcf1s, wb, W=width, lod=0.0, subpixel_shift=False, activation=hypernetwork.activation, ) tsnc_path = _write_tsnc_to_temp(bcf1s, wb) preview_diff = (ts_out[0, ..., 0:3].detach().cpu().clamp(0.0, 1.0).numpy() * 255.0).astype( np.uint8 ) preview_norm = (ts_out[0, ..., 3:6].detach().cpu().clamp(0.0, 1.0).numpy() * 255.0).astype( np.uint8 ) preview_arm = (ts_out[0, ..., 6:9].detach().cpu().clamp(0.0, 1.0).numpy() * 255.0).astype( np.uint8 ) return tsnc_path, preview_diff, preview_norm, preview_arm def build_demo() -> gr.Blocks: with gr.Blocks(title="TSNC Hypernetwork") as demo: gr.Markdown( """ # TSNC Hypernetwork Compress a PBR texture set (diffuse, normal, ARM) into Intel's [Texture Set Neural Compression (TSNC)](https://github.com/GameTechDev/TextureSetNeuralCompressionSample) format. **Requirements:** square textures, same resolution for all three maps. """ ) with gr.Row(): diffuse_in = gr.Image(label="Diffuse", type="numpy", image_mode="RGB") normal_in = gr.Image(label="Normal", type="numpy", image_mode="RGB") arm_in = gr.Image(label="ARM", type="numpy", image_mode="RGB") compress_btn = gr.Button("Compress", variant="primary") tsnc_out = gr.File(label="TSNC file") with gr.Row(): diff_out = gr.Image(label="Reconstructed diffuse", type="numpy") norm_out = gr.Image(label="Reconstructed normal", type="numpy") arm_out = gr.Image(label="Reconstructed ARM", type="numpy") compress_btn.click( fn=compress, inputs=[diffuse_in, normal_in, arm_in], outputs=[tsnc_out, diff_out, norm_out, arm_out], ) return demo if __name__ == "__main__": demo = build_demo() demo.launch()