Download model.py from ArshPathan/TexelDiff-16-62M: direct link, hf CLI and curl.
- Browser
- Download file 8.8 kB
-
https://huggingface.co/ArshPathan/TexelDiff-16-62M/resolve/main/model.py
- Command line
-
hf download hf://ArshPathan/TexelDiff-16-62M/model.py
-
curl -L -o model.py https://huggingface.co/ArshPathan/TexelDiff-16-62M/resolve/main/model.py
8.8 kB
| """Minecraft texture diffusion model: standalone inference code. | |
| Usage: | |
| from model import TextureGenerator | |
| gen = TextureGenerator("ArshPathan/TexelDiff-16-62M") # or a local folder | |
| imgs = gen("minecraft item texture: gold ingot", res=16, n=8, guidance=4.0) | |
| imgs[0].resize((256, 256), 0).save("ingot.png") # 0 = nearest neighbour | |
| """ | |
| import json, math | |
| from pathlib import Path | |
| from types import SimpleNamespace | |
| import numpy as np, torch, torch.nn as nn, torch.nn.functional as F | |
| from PIL import Image | |
| CFG = SimpleNamespace(tileable=False) # read by conv(); set from config.json on load | |
| def sinusoidal(t, dim): | |
| half = dim // 2 | |
| freqs = torch.exp(-math.log(10000) * torch.arange(half, device=t.device) / half) | |
| a = t[:, None].float() * freqs[None] | |
| return torch.cat([a.sin(), a.cos()], -1) | |
| def conv(i, o, k=3): | |
| return nn.Conv2d(i, o, k, padding=k // 2, padding_mode="circular" if CFG.tileable else "zeros") | |
| class ResBlock(nn.Module): | |
| def __init__(self, i, o, emb): | |
| super().__init__() | |
| self.n1 = nn.GroupNorm(8, i); self.c1 = conv(i, o) | |
| self.emb = nn.Linear(emb, o * 2) | |
| self.n2 = nn.GroupNorm(8, o); self.c2 = conv(o, o) | |
| self.skip = nn.Conv2d(i, o, 1) if i != o else nn.Identity() | |
| def forward(self, x, e): | |
| h = self.c1(F.silu(self.n1(x))) | |
| sc, sh = self.emb(F.silu(e))[:, :, None, None].chunk(2, 1) | |
| h = self.n2(h) * (1 + sc) + sh | |
| h = self.c2(F.silu(h)) | |
| return h + self.skip(x) | |
| class AttnBlock(nn.Module): | |
| def __init__(self, ch, ctx_dim, head_dim): | |
| super().__init__() | |
| heads = max(1, ch // head_dim) | |
| self.norm = nn.GroupNorm(8, ch) | |
| self.ln1, self.ln2, self.ln3 = nn.LayerNorm(ch), nn.LayerNorm(ch), nn.LayerNorm(ch) | |
| self.sa = nn.MultiheadAttention(ch, heads, batch_first=True) | |
| self.ca = nn.MultiheadAttention(ch, heads, kdim=ctx_dim, vdim=ctx_dim, batch_first=True) | |
| self.ff = nn.Sequential(nn.Linear(ch, ch * 4), nn.GELU(), nn.Linear(ch * 4, ch)) | |
| def forward(self, x, ctx): | |
| B, C, H, W = x.shape | |
| h = self.norm(x).flatten(2).transpose(1, 2) | |
| q = self.ln1(h); h = h + self.sa(q, q, q, need_weights=False)[0] | |
| q = self.ln2(h); h = h + self.ca(q, ctx, ctx, need_weights=False)[0] | |
| h = h + self.ff(self.ln3(h)) | |
| return x + h.transpose(1, 2).reshape(B, C, H, W) | |
| class Down(nn.Module): | |
| def __init__(self, c): super().__init__(); self.c = conv(c, c) | |
| def forward(self, x): return self.c(F.avg_pool2d(x, 2)) | |
| class Up(nn.Module): | |
| def __init__(self, c): super().__init__(); self.c = conv(c, c) | |
| def forward(self, x): return self.c(F.interpolate(x, scale_factor=2, mode="nearest")) | |
| class UNet(nn.Module): | |
| def __init__(self, cfg): | |
| super().__init__() | |
| ch, emb, mults = cfg.base_ch, cfg.base_ch * 4, cfg.ch_mult | |
| self.ch = ch | |
| mlp = lambda i: nn.Sequential(nn.Linear(i, emb), nn.SiLU(), nn.Linear(emb, emb)) | |
| self.t_mlp, self.r_mlp, self.p_mlp = mlp(ch), mlp(ch), mlp(cfg.text_dim) | |
| self.inc = conv(4, ch) | |
| self.down = nn.ModuleList(); chans = [ch]; cur = ch | |
| for li, m in enumerate(mults): | |
| out = ch * m | |
| for _ in range(cfg.n_res_blocks): | |
| blk = nn.ModuleList([ResBlock(cur, out, emb)]) | |
| if li in cfg.attn_levels: blk.append(AttnBlock(out, cfg.text_dim, cfg.head_dim)) | |
| self.down.append(blk); cur = out; chans.append(cur) | |
| if li != len(mults) - 1: | |
| self.down.append(nn.ModuleList([Down(cur)])); chans.append(cur) | |
| self.mid = nn.ModuleList([ResBlock(cur, cur, emb), AttnBlock(cur, cfg.text_dim, cfg.head_dim), ResBlock(cur, cur, emb)]) | |
| self.up = nn.ModuleList() | |
| for li, m in reversed(list(enumerate(mults))): | |
| out = ch * m | |
| for j in range(cfg.n_res_blocks + 1): | |
| skip = chans.pop() | |
| blk = nn.ModuleList([ResBlock(cur + skip, out, emb)]) | |
| if li in cfg.attn_levels: blk.append(AttnBlock(out, cfg.text_dim, cfg.head_dim)) | |
| cur = out | |
| if li != 0 and j == cfg.n_res_blocks: blk.append(Up(cur)) | |
| self.up.append(blk) | |
| self.out_norm = nn.GroupNorm(8, cur) | |
| self.out = conv(cur, 4) | |
| nn.init.zeros_(self.out.weight); nn.init.zeros_(self.out.bias) | |
| def run(self, blk, x, e, ctx): | |
| for m in blk: | |
| if isinstance(m, ResBlock): x = m(x, e) | |
| elif isinstance(m, AttnBlock): x = m(x, ctx) | |
| else: x = m(x) | |
| return x | |
| def forward(self, x, t, res, pooled, ctx): | |
| e = (self.t_mlp(sinusoidal(t, self.ch)) | |
| + self.r_mlp(sinusoidal(torch.log2(res.float()) * 100, self.ch)) | |
| + self.p_mlp(pooled)) | |
| h = self.inc(x); hs = [h] | |
| for blk in self.down: | |
| h = self.run(blk, h, e, ctx); hs.append(h) | |
| h = self.run(self.mid, h, e, ctx) | |
| for blk in self.up: | |
| h = self.run(blk, torch.cat([h, hs.pop()], 1), e, ctx) | |
| return self.out(F.silu(self.out_norm(h))) | |
| def _tuplify(c): | |
| for k in ("ch_mult", "attn_levels", "resolutions"): | |
| if k in c: c[k] = tuple(c[k]) | |
| return c | |
| def _cosine_alpha_bar(T, s=0.008): | |
| t = torch.linspace(0, T, T + 1, dtype=torch.float64) / T | |
| f = torch.cos((t + s) / (1 + s) * math.pi / 2) ** 2 | |
| ac = f / f[0] | |
| betas = (1 - ac[1:] / ac[:-1]).clamp(0, 0.999) | |
| return torch.cumprod(1 - betas, 0).float() | |
| class TextureGenerator: | |
| def __init__(self, repo_or_dir, device=None): | |
| from safetensors.torch import load_file | |
| from transformers import CLIPTokenizer, CLIPTextModel | |
| self.device = torch.device(device or ("cuda" if torch.cuda.is_available() else "cpu")) | |
| p = Path(repo_or_dir) | |
| if p.is_dir(): | |
| cfg_path, w_path = p / "config.json", p / "model.safetensors" | |
| else: | |
| from huggingface_hub import hf_hub_download | |
| cfg_path = hf_hub_download(repo_or_dir, "config.json") | |
| w_path = hf_hub_download(repo_or_dir, "model.safetensors") | |
| self.cfg = SimpleNamespace(**_tuplify(json.load(open(cfg_path)))) | |
| CFG.tileable = self.cfg.tileable | |
| self.model = UNet(self.cfg) | |
| self.model.load_state_dict(load_file(str(w_path))) | |
| self.model.to(self.device).eval() | |
| self.tok = CLIPTokenizer.from_pretrained(self.cfg.text_model) | |
| self.tenc = CLIPTextModel.from_pretrained(self.cfg.text_model).to(self.device).eval().requires_grad_(False) | |
| self.ac = _cosine_alpha_bar(self.cfg.timesteps).to(self.device) | |
| def _encode(self, texts): | |
| t = self.tok(list(texts), padding="max_length", truncation=True, | |
| max_length=self.cfg.text_len, return_tensors="pt").to(self.device) | |
| o = self.tenc(**t) | |
| return o.last_hidden_state.float(), o.pooler_output.float() | |
| def sample(self, prompt, res=16, n=8, steps=50, guidance=4.0, seed=None): | |
| """Returns a (n, 4, res, res) tensor in [-1, 1] (RGBA).""" | |
| assert res in self.cfg.resolutions, f"res must be one of {self.cfg.resolutions}" | |
| dev, T = self.device, self.cfg.timesteps | |
| g = torch.Generator(device=dev) | |
| if seed is not None: g.manual_seed(int(seed)) | |
| ctx, pooled = self._encode([prompt] * n) | |
| nctx, npool = self._encode([""]) | |
| ctx_all = torch.cat([ctx, nctx.expand(n, -1, -1)]); pool_all = torch.cat([pooled, npool.expand(n, -1)]) | |
| resb = torch.full((2 * n,), res, device=dev) | |
| x = torch.randn(n, 4, res, res, device=dev, generator=g) | |
| ts = torch.linspace(T - 1, 0, steps, device=dev).long() | |
| for i, t in enumerate(ts): | |
| a_t = self.ac[t] | |
| a_prev = self.ac[ts[i + 1]] if i + 1 < len(ts) else torch.tensor(1.0, device=dev) | |
| out = self.model(torch.cat([x, x]), torch.full((2 * n,), int(t), device=dev), resb, pool_all, ctx_all) | |
| e_c, e_u = out[:n], out[n:] | |
| eps = e_u + guidance * (e_c - e_u) | |
| x0 = ((x - (1 - a_t).sqrt() * eps) / a_t.sqrt()).clamp(-1, 1) | |
| eps = (x - a_t.sqrt() * x0) / (1 - a_t).sqrt() | |
| x = a_prev.sqrt() * x0 + (1 - a_prev).sqrt() * eps | |
| return x | |
| def __call__(self, prompt, res=16, n=8, steps=50, guidance=4.0, seed=None, opaque=False, snap_alpha=True): | |
| x = self.sample(prompt, res, n, steps, guidance, seed) | |
| if opaque: | |
| x[:, 3] = 1.0 | |
| elif snap_alpha: | |
| x[:, 3] = torch.where(x[:, 3] > 0, 1.0, -1.0) | |
| x[:, :3] = x[:, :3] * (x[:, 3:4] > 0) | |
| a = ((x.clamp(-1, 1) + 1) * 127.5).round().byte().permute(0, 2, 3, 1).cpu().numpy() | |
| return [Image.fromarray(i, "RGBA") for i in a] | |