#!/usr/bin/env python3 """ Tiny AutoEncoder for Stable Diffusion (DNN for encoding / decoding SD's latent space) """ import torch import torch.nn as nn def conv(n_in, n_out, **kwargs): return nn.Conv2d(n_in, n_out, 3, padding=1, **kwargs) class Clamp(nn.Module): def forward(self, x): return torch.tanh(x / 3) * 3 class Block(nn.Module): def __init__(self, n_in, n_out, use_midblock_gn=False): super().__init__() self.conv = nn.Sequential(conv(n_in, n_out), nn.ReLU(), conv(n_out, n_out), nn.ReLU(), conv(n_out, n_out)) self.skip = nn.Conv2d(n_in, n_out, 1, bias=False) if n_in != n_out else nn.Identity() self.fuse = nn.ReLU() self.pool = None if use_midblock_gn: conv1x1, n_gn = lambda n_in, n_out: nn.Conv2d(n_in, n_out, 1, bias=False), n_in*4 self.pool = nn.Sequential(conv1x1(n_in, n_gn), nn.GroupNorm(4, n_gn), nn.ReLU(inplace=True), conv1x1(n_gn, n_in)) def forward(self, x): if self.pool is not None: x = x + self.pool(x) return self.fuse(self.conv(x) + self.skip(x)) def Encoder(latent_channels=4, use_midblock_gn=False, image_channels=3): mb_kw = dict(use_midblock_gn=use_midblock_gn) return nn.Sequential( conv(image_channels, 64), Block(64, 64), conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), conv(64, 64, stride=2, bias=False), Block(64, 64, **mb_kw), Block(64, 64, **mb_kw), Block(64, 64, **mb_kw), conv(64, latent_channels), ) def Decoder(latent_channels=4, use_midblock_gn=False, image_channels=3): mb_kw = dict(use_midblock_gn=use_midblock_gn) return nn.Sequential( Clamp(), conv(latent_channels, 64), nn.ReLU(), Block(64, 64, **mb_kw), Block(64, 64, **mb_kw), Block(64, 64, **mb_kw), nn.Upsample(scale_factor=2), conv(64, 64, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False), Block(64, 64), conv(64, image_channels), ) def F32Encoder(latent_channels=32, image_channels=3): """Encoder variant with 32x spatial downscaling instead of 8x.""" return nn.Sequential( conv(image_channels, 32, stride=2), nn.ReLU(inplace=True), conv(32, 64, stride=2), nn.ReLU(inplace=True), Block(64, 64), conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), conv(64, latent_channels), ) def F32Decoder(latent_channels=32, image_channels=3): """Decoder variant with 32x spatial upscaling instead of 8x.""" return nn.Sequential( Clamp(), conv(latent_channels, 256), nn.ReLU(), Block(256, 256), Block(256, 256), Block(256, 256), nn.Upsample(scale_factor=2), conv(256, 128, bias=False), Block(128, 128), Block(128, 128), Block(128, 128), nn.Upsample(scale_factor=2), conv(128, 64, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False), Block(64, 64), conv(64, image_channels), ) def F16Encoder(latent_channels=64, image_channels=3): """Encoder variant with 16x spatial downscaling, 2x2 input patchify, and wider low-res stages.""" return nn.Sequential( nn.PixelUnshuffle(2), conv(image_channels * 4, 64), nn.ReLU(inplace=True), Block(64, 64), conv(64, 64, stride=2, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), conv(64, 128, stride=2, bias=False), Block(128, 128), Block(128, 128), Block(128, 128), conv(128, 256, stride=2, bias=False), Block(256, 256), Block(256, 256), Block(256, 256), conv(256, latent_channels), ) def F16Decoder(latent_channels=64, image_channels=3): """Decoder variant with 16x spatial upscaling, wider low-res stages, and 2x2 output patchify.""" return nn.Sequential( Clamp(), conv(latent_channels, 256), nn.ReLU(), Block(256, 256), Block(256, 256), Block(256, 256), nn.Upsample(scale_factor=2), conv(256, 128, bias=False), Block(128, 128), Block(128, 128), Block(128, 128), nn.Upsample(scale_factor=2), conv(128, 64, bias=False), Block(64, 64), Block(64, 64), Block(64, 64), nn.Upsample(scale_factor=2), conv(64, 64, bias=False), Block(64, 64), conv(64, image_channels * 4), nn.PixelShuffle(2), ) class TAESD(nn.Module): latent_magnitude = 3 latent_shift = 0.5 def __init__(self, encoder_path="taesd_encoder.pth", decoder_path="taesd_decoder.pth", latent_channels=None, arch_variant=None, image_channels=None): """Initialize pretrained TAESD on the given device from the given checkpoints.""" super().__init__() if latent_channels is None: latent_channels, arch_variant = self.guess_latent_channels_and_arch(str(encoder_path)) if image_channels is None: image_channels = self.guess_image_channels(str(encoder_path or decoder_path)) # 3 for RGB, 4 for RGBA (straight alpha) self.image_channels = image_channels self.latent_channels = latent_channels # flux_2 required global pooling/norm for accurate distillation, enable conditionally self.encoder = Encoder(latent_channels, use_midblock_gn=(arch_variant in ["flux_2"]), image_channels=image_channels) self.decoder = Decoder(latent_channels, use_midblock_gn=(arch_variant in ["flux_2"]), image_channels=image_channels) # sana dcae requires 32x spatial downscaling, enable conditionally if arch_variant == "f32": self.encoder, self.decoder = F32Encoder(latent_channels, image_channels), F32Decoder(latent_channels, image_channels) # qwen image 2.1 vae requires 16x spatial downscaling, enable conditionally if arch_variant == "f16": self.encoder, self.decoder = F16Encoder(latent_channels, image_channels), F16Decoder(latent_channels, image_channels) if encoder_path is not None: self.encoder.load_state_dict(torch.load(encoder_path, map_location="cpu", weights_only=True)) if decoder_path is not None: self.decoder.load_state_dict(torch.load(decoder_path, map_location="cpu", weights_only=True)) def guess_latent_channels(self, encoder_path): """Guess latent channel count based on encoder filename""" return self.guess_latent_channels_and_arch(encoder_path)[0] def guess_latent_channels_and_arch(self, encoder_path): """Guess latent channel count and architecture variant based on encoder filename""" if "taef1" in encoder_path: return 16, None if "taef2" in encoder_path: return 32, "flux_2" if "taesd3" in encoder_path: return 16, None if "taesana" in encoder_path: return 32, "f32" # f32c32 if "taeqi2_1" in encoder_path: return 64, "f16" # f16c64 return 4, None def guess_image_channels(self, encoder_path): """Guess image channel count (3 for RGB, 4 for RGBA) based on encoder filename""" if "taeqi2_1" in encoder_path: return 4 # qwen image 2.1 vae is rgba return 3 @staticmethod def scale_latents(x): """raw latents -> [0, 1]""" return x.div(2 * TAESD.latent_magnitude).add(TAESD.latent_shift).clamp(0, 1) @staticmethod def unscale_latents(x): """[0, 1] -> raw latents""" return x.sub(TAESD.latent_shift).mul(2 * TAESD.latent_magnitude) @torch.no_grad() def main(): from PIL import Image import sys import torchvision.transforms.functional as TF dev = torch.device("cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu") print("Using device", dev) taesd = TAESD().to(dev) # rgba models also accept rgb images, as rgba with alpha=1 im_mode = "RGBA" if taesd.image_channels == 4 else "RGB" for im_path in sys.argv[1:]: im = TF.to_tensor(Image.open(im_path).convert(im_mode)).unsqueeze(0).to(dev) # encode image, quantize, and save to file im_enc = taesd.scale_latents(taesd.encoder(im)).mul_(255).round_().byte() # fold 4k channels into k vertically stacked 4-channel tiles (no-op for 4-channel latents) im_enc = im_enc[0].unflatten(0, (-1, 4)).transpose(0, 1).flatten(1, 2) enc_path = im_path + ".encoded.png" TF.to_pil_image(im_enc).save(enc_path) print(f"Encoded {im_path} to {enc_path}") # load the saved file, dequantize, and decode im_enc = TF.to_tensor(Image.open(enc_path)) im_enc = im_enc.unflatten(1, (taesd.latent_channels // 4, -1)).transpose(0, 1).flatten(0, 1) im_enc = taesd.unscale_latents(im_enc.unsqueeze(0).to(dev)) im_dec = taesd.decoder(im_enc).clamp(0, 1) dec_path = im_path + ".decoded.png" print(f"Decoded {enc_path} to {dec_path}") TF.to_pil_image(im_dec[0]).save(dec_path) if __name__ == "__main__": main()