from __future__ import annotations import torch from torch import nn from src.models.autoencoder.encoder import Encoder from src.models.autoencoder.decoder import Decoder from src.models.autoencoder.distributions import DiagonalGaussianDistribution class AutoencoderKL(nn.Module): """ VAE / AutoencoderKL wrapper posterior = vae.encode(x) z = posterior.sample() x_recon = vae.decode(z) Or directly: x_recon, posterior, z = vae(x) Input image range: x in [-1, 1] Output image range: x_recon in [-1, 1] """ def __init__( self, in_channels: int = 3, out_channels: int = 3, latent_channels: int = 8, base_channels: int = 128, channel_multipliers: list[int] | tuple[int, ...] = (1, 2, 4, 4), num_res_blocks: int = 3, dropout: float = 0.0, use_attention: bool = True, attention_heads: int = 4, scaling_factor: float = 1.0, attention_resolutions: tuple[int, ...] = (32,), ): super().__init__() self.in_channels = in_channels self.out_channels = out_channels self.latent_channels = latent_channels self.base_channels = base_channels self.channel_multipliers = list(channel_multipliers) self.num_res_blocks = num_res_blocks self.scaling_factor = scaling_factor self.encoder = Encoder( in_channels=in_channels, latent_channels=latent_channels, base_channels=base_channels, channel_multipliers=channel_multipliers, num_res_blocks=num_res_blocks, dropout=dropout, use_attention=use_attention, attention_heads=attention_heads, attention_resolutions= attention_resolutions ) self.decoder = Decoder( out_channels=out_channels, latent_channels=latent_channels, base_channels=base_channels, channel_multipliers=channel_multipliers, num_res_blocks=num_res_blocks, dropout=dropout, use_attention=use_attention, attention_heads=attention_heads, ) def encode( self, x: torch.Tensor, deterministic: bool = False, ) -> DiagonalGaussianDistribution: """ Encode image into posterior distribution. deterministic: If True, posterior.sample() will return mean only. Returns: DiagonalGaussianDistribution. """ moments = self.encoder(x) posterior = DiagonalGaussianDistribution( moments=moments, deterministic=deterministic, ) return posterior def decode( self, z: torch.Tensor, unscale: bool = True, ) -> torch.Tensor: """ Decode latent into image z: Latent tensor [B, latent_channels, H/8, W/8]. unscale: If True, divide by scaling_factor before decoding. Returns: Reconstructed image in [-1, 1]. """ if unscale: z = z / self.scaling_factor return self.decoder(z) def forward( self, x: torch.Tensor, sample_posterior: bool = True, ) -> tuple[torch.Tensor, DiagonalGaussianDistribution, torch.Tensor]: """ Full VAE forward pass. Args: x: Image tensor [B, 3, H, W], normalized to [-1, 1]. sample_posterior: If True: z = posterior.sample() If False: z = posterior.mode() Returns: x_recon: Reconstructed image [B, 3, H, W]. posterior: DiagonalGaussianDistribution object. z: Latent tensor before scaling. """ posterior = self.encode(x) if sample_posterior: z = posterior.sample() else: z = posterior.mode() x_recon = self.decode(z, unscale=False) return x_recon, posterior, z @torch.no_grad() def reconstruct( self, x: torch.Tensor, sample_posterior: bool = False, ) -> torch.Tensor: """ Convenience method for inference reconstruction. By default, uses posterior mode for stable reconstructions. """ x_recon, _, _ = self.forward( x, sample_posterior=sample_posterior, ) return x_recon @torch.no_grad() def encode_to_latent( self, x: torch.Tensor, sample_posterior: bool = False, scale: bool = True, ) -> torch.Tensor: """ Encode image into latent tensor for latent diffusion training. Usually for latent caching, use: sample_posterior=False because the posterior mean is deterministic and stable. If scale=True: z_scaled = z * scaling_factor Stable Diffusion-style LDMs often scale latents before diffusion. """ posterior = self.encode(x) if sample_posterior: z = posterior.sample() else: z = posterior.mode() if scale: z = z * self.scaling_factor return z @torch.no_grad() def decode_from_latent( self, z: torch.Tensor, unscale: bool = True, ) -> torch.Tensor: """ Decode latent tensor produced by diffusion model. """ return self.decode(z, unscale=unscale)