import torch
from diffusers import DDIMScheduler, CosineDPMSolverMultistepScheduler
from diffusers.schedulers.scheduling_dpmsolver_sde import BrownianTreeNoiseSampler
from diffusers import AudioLDM2Pipeline, StableAudioPipeline
from transformers import RobertaTokenizer, RobertaTokenizerFast, VitsTokenizer
from diffusers.models.unets.unet_2d_condition import UNet2DConditionOutput
from diffusers.models.embeddings import get_1d_rotary_pos_embed
from typing import Any, Dict, List, Optional, Tuple, Union
import gradio as gr


class PipelineWrapper(torch.nn.Module):
    def __init__(self, model_id: str,
                 device: torch.device,
                 double_precision: bool = False,
                 token: Optional[str] = None, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.model_id = model_id
        self.device = device
        self.double_precision = double_precision
        self.token = token

    def get_sigma(self, timestep: int) -> float:
        sqrt_recipm1_alphas_cumprod = torch.sqrt(1.0 / self.model.scheduler.alphas_cumprod - 1)
        return sqrt_recipm1_alphas_cumprod[timestep]

    def load_scheduler(self) -> None:
        pass

    def get_fn_STFT(self) -> torch.nn.Module:
        pass

    def get_sr(self) -> int:
        return 16000

    def vae_encode(self, x: torch.Tensor) -> torch.Tensor:
        pass

    def vae_decode(self, x: torch.Tensor) -> torch.Tensor:
        pass

    def decode_to_mel(self, x: torch.Tensor) -> torch.Tensor:
        pass

    def setup_extra_inputs(self, *args, **kwargs) -> None:
        pass

    def encode_text(self, prompts: List[str], **kwargs
                    ) -> Tuple[Optional[torch.Tensor], Optional[torch.Tensor], Optional[torch.Tensor]]:
        pass

    def get_variance(self, timestep: torch.Tensor, prev_timestep: torch.Tensor) -> torch.Tensor:
        pass

    def get_alpha_prod_t_prev(self, prev_timestep: torch.Tensor) -> torch.Tensor:
        pass

    def get_noise_shape(self, x0: torch.Tensor, num_steps: int) -> Tuple[int, ...]:
        variance_noise_shape = (num_steps,
                                self.model.unet.config.in_channels,
                                x0.shape[-2],
                                x0.shape[-1])
        return variance_noise_shape

    def sample_xts_from_x0(self, x0: torch.Tensor, num_inference_steps: int = 50) -> torch.Tensor:
        """
        Samples from P(x_1:T|x_0)
        """
        alpha_bar = self.model.scheduler.alphas_cumprod
        sqrt_one_minus_alpha_bar = (1-alpha_bar) ** 0.5

        variance_noise_shape = self.get_noise_shape(x0, num_inference_steps + 1)
        timesteps = self.model.scheduler.timesteps.to(self.device)
        t_to_idx = {int(v): k for k, v in enumerate(timesteps)}
        xts = torch.zeros(variance_noise_shape).to(x0.device)
        xts[0] = x0
        for t in reversed(timesteps):
            idx = num_inference_steps - t_to_idx[int(t)]
            xts[idx] = x0 * (alpha_bar[t] ** 0.5) + torch.randn_like(x0) * sqrt_one_minus_alpha_bar[t]

        return xts

    def get_zs_from_xts(self, xt: torch.Tensor, xtm1: torch.Tensor, noise_pred: torch.Tensor,
                        t: torch.Tensor, eta: float = 0, numerical_fix: bool = True, **kwargs
                        ) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
        # pred of x0
        alpha_bar = self.model.scheduler.alphas_cumprod
        if self.model.scheduler.config.prediction_type == 'epsilon':
            pred_original_sample = (xt - (1 - alpha_bar[t]) ** 0.5 * noise_pred) / alpha_bar[t] ** 0.5
        elif self.model.scheduler.config.prediction_type == 'v_prediction':
            pred_original_sample = (alpha_bar[t] ** 0.5) * xt - ((1 - alpha_bar[t]) ** 0.5) * noise_pred

        # direction to xt
        prev_timestep = t - self.model.scheduler.config.num_train_timesteps // \
            self.model.scheduler.num_inference_steps

        alpha_prod_t_prev = self.get_alpha_prod_t_prev(prev_timestep)
        variance = self.get_variance(t, prev_timestep)

        if self.model.scheduler.config.prediction_type == 'epsilon':
            radom_noise_pred = noise_pred
        elif self.model.scheduler.config.prediction_type == 'v_prediction':
            radom_noise_pred = (alpha_bar[t] ** 0.5) * noise_pred + ((1 - alpha_bar[t]) ** 0.5) * xt

        pred_sample_direction = (1 - alpha_prod_t_prev - eta * variance) ** (0.5) * radom_noise_pred

        mu_xt = alpha_prod_t_prev ** (0.5) * pred_original_sample + pred_sample_direction

        z = (xtm1 - mu_xt) / (eta * variance ** 0.5)

        # correction to avoid error accumulation
        if numerical_fix:
            xtm1 = mu_xt + (eta * variance ** 0.5)*z

        return z, xtm1, None

    def reverse_step_with_custom_noise(self, model_output: torch.Tensor, timestep: torch.Tensor, sample: torch.Tensor,
                                       variance_noise: Optional[torch.Tensor] = None, eta: float = 0, **kwargs
                                       ) -> torch.Tensor:
        # 1. get previous step value (=t-1)
        prev_timestep = timestep - self.model.scheduler.config.num_train_timesteps // \
            self.model.scheduler.num_inference_steps
        # 2. compute alphas, betas
        alpha_prod_t = self.model.scheduler.alphas_cumprod[timestep]
        alpha_prod_t_prev = self.get_alpha_prod_t_prev(prev_timestep)
        beta_prod_t = 1 - alpha_prod_t
        # 3. compute predicted original sample from predicted noise also called
        # "predicted x_0" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf
        if self.model.scheduler.config.prediction_type == 'epsilon':
            pred_original_sample = (sample - beta_prod_t ** (0.5) * model_output) / alpha_prod_t ** (0.5)
        elif self.model.scheduler.config.prediction_type == 'v_prediction':
            pred_original_sample = (alpha_prod_t ** 0.5) * sample - (beta_prod_t ** 0.5) * model_output

        # 5. compute variance: "sigma_t(η)" -> see formula (16)
        # σ_t = sqrt((1 − α_t−1)/(1 − α_t)) * sqrt(1 − α_t/α_t−1)
        # variance = self.scheduler._get_variance(timestep, prev_timestep)
        variance = self.get_variance(timestep, prev_timestep)
        # std_dev_t = eta * variance ** (0.5)
        # Take care of asymetric reverse process (asyrp)
        if self.model.scheduler.config.prediction_type == 'epsilon':
            model_output_direction = model_output
        elif self.model.scheduler.config.prediction_type == 'v_prediction':
            model_output_direction = (alpha_prod_t**0.5) * model_output + (beta_prod_t**0.5) * sample
        # 6. compute "direction pointing to x_t" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf
        # pred_sample_direction = (1 - alpha_prod_t_prev - std_dev_t**2) ** (0.5) * model_output_direction
        pred_sample_direction = (1 - alpha_prod_t_prev - eta * variance) ** (0.5) * model_output_direction
        # 7. compute x_t without "random noise" of formula (12) from https://arxiv.org/pdf/2010.02502.pdf
        prev_sample = alpha_prod_t_prev ** (0.5) * pred_original_sample + pred_sample_direction
        # 8. Add noice if eta > 0
        if eta > 0:
            if variance_noise is None:
                variance_noise = torch.randn(model_output.shape, device=self.device)
            sigma_z = eta * variance ** (0.5) * variance_noise
            prev_sample = prev_sample + sigma_z

        return prev_sample

    def unet_forward(self,
                     sample: torch.FloatTensor,
                     timestep: Union[torch.Tensor, float, int],
                     encoder_hidden_states: torch.Tensor,
                     class_labels: Optional[torch.Tensor] = None,
                     timestep_cond: Optional[torch.Tensor] = None,
                     attention_mask: Optional[torch.Tensor] = None,
                     cross_attention_kwargs: Optional[Dict[str, Any]] = None,
                     added_cond_kwargs: Optional[Dict[str, torch.Tensor]] = None,
                     down_block_additional_residuals: Optional[Tuple[torch.Tensor]] = None,
                     mid_block_additional_residual: Optional[torch.Tensor] = None,
                     encoder_attention_mask: Optional[torch.Tensor] = None,
                     replace_h_space: Optional[torch.Tensor] = None,
                     replace_skip_conns: Optional[Dict[int, torch.Tensor]] = None,
                     return_dict: bool = True,
                     zero_out_resconns: Optional[Union[int, List]] = None) -> Tuple:
        pass


class AudioLDM2Wrapper(PipelineWrapper):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        if self.double_precision:
            self.model = AudioLDM2Pipeline.from_pretrained(self.model_id, torch_dtype=torch.float64, token=self.token
                                                           ).to(self.device)
        else:
            try:
                self.model = AudioLDM2Pipeline.from_pretrained(self.model_id, local_files_only=True, token=self.token
                                                               ).to(self.device)
            except FileNotFoundError:
                self.model = AudioLDM2Pipeline.from_pretrained(self.model_id, local_files_only=False, token=self.token
                                                               ).to(self.device)

    def load_scheduler(self) -> None:
        self.model.scheduler = DDIMScheduler.from_pretrained(self.model_id, subfolder="scheduler")

    def get_fn_STFT(self) -> torch.nn.Module:
        from audioldm.audio import TacotronSTFT
        return TacotronSTFT(
            filter_length=1024,
            hop_length=160,
            win_length=1024,
            n_mel_channels=64,
            sampling_rate=16000,
            mel_fmin=0,
            mel_fmax=8000,
        )

    def vae_encode(self, x: torch.Tensor) -> torch.Tensor:
        # self.model.vae.disable_tiling()
        if x.shape[2] % 4:
            x = torch.nn.functional.pad(x, (0, 0, 4 - (x.shape[2] % 4), 0))
        return (self.model.vae.encode(x).latent_dist.mode() * self.model.vae.config.scaling_factor).float()
        # return (self.encode_no_tiling(x).latent_dist.mode() * self.model.vae.config.scaling_factor).float()

    def vae_decode(self, x: torch.Tensor) -> torch.Tensor:
        return self.model.vae.decode(1 / self.model.vae.config.scaling_factor * x).sample

    def decode_to_mel(self, x: torch.Tensor) -> torch.Tensor:
        if self.double_precision:
            tmp = self.model.mel_spectrogram_to_waveform(x[:, 0].detach().double()).detach()
        tmp = self.model.mel_spectrogram_to_waveform(x[:, 0].detach().float()).detach()
        if len(tmp.shape) == 1:
            tmp = tmp.unsqueeze(0)
        return tmp

    def encode_text(self, prompts: List[str], negative: bool = False, 
                    save_compute: bool = False, cond_length: int = 0, **kwargs
                    ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
        tokenizers = [self.model.tokenizer, self.model.tokenizer_2]
        text_encoders = [self.model.text_encoder, self.model.text_encoder_2]
        prompt_embeds_list = []
        attention_mask_list = []

        for tokenizer, text_encoder in zip(tokenizers, text_encoders):
            text_inputs = tokenizer(
                prompts,
                padding="max_length" if (save_compute and negative) or isinstance(tokenizer, (RobertaTokenizer, RobertaTokenizerFast))
                else True,
                max_length=tokenizer.model_max_length
                if (not save_compute) or ((not negative) or isinstance(tokenizer, (RobertaTokenizer, RobertaTokenizerFast, VitsTokenizer)))
                else cond_length,
                truncation=True,
                return_tensors="pt",
            )

            text_input_ids = text_inputs.input_ids
            attention_mask = text_inputs.attention_mask
            untruncated_ids = tokenizer(prompts, padding="longest", return_tensors="pt").input_ids

            if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] \
                    and not torch.equal(text_input_ids, untruncated_ids):
                removed_text = tokenizer.batch_decode(
                    untruncated_ids[:, tokenizer.model_max_length - 1: -1])
                print(f"The following part of your input was truncated because {text_encoder.config.model_type} can "
                      f"only handle sequences up to {tokenizer.model_max_length} tokens: {removed_text}"
                      )

            text_input_ids = text_input_ids.to(self.device)
            attention_mask = attention_mask.to(self.device)

            with torch.no_grad():
                if text_encoder.config.model_type == "clap":
                    prompt_embeds = text_encoder.get_text_features(
                        text_input_ids,
                        attention_mask=attention_mask,
                    )
                    # append the seq-len dim: (bs, hidden_size) -> (bs, seq_len, hidden_size)
                    prompt_embeds = prompt_embeds[:, None, :]
                    # make sure that we attend to this single hidden-state
                    attention_mask = attention_mask.new_ones((len(prompts), 1))
                else:
                    prompt_embeds = text_encoder(
                        text_input_ids,
                        attention_mask=attention_mask,
                    )
                    prompt_embeds = prompt_embeds[0]

            prompt_embeds_list.append(prompt_embeds)
            attention_mask_list.append(attention_mask)

        # print(f'prompt[0].shape: {prompt_embeds_list[0].shape}')
        # print(f'prompt[1].shape: {prompt_embeds_list[1].shape}')
        # print(f'attn[0].shape: {attention_mask_list[0].shape}')
        # print(f'attn[1].shape: {attention_mask_list[1].shape}')

        projection_output = self.model.projection_model(
            hidden_states=prompt_embeds_list[0],
            hidden_states_1=prompt_embeds_list[1],
            attention_mask=attention_mask_list[0],
            attention_mask_1=attention_mask_list[1],
        )
        projected_prompt_embeds = projection_output.hidden_states
        projected_attention_mask = projection_output.attention_mask

        generated_prompt_embeds = self.model.generate_language_model(
            projected_prompt_embeds,
            attention_mask=projected_attention_mask,
            max_new_tokens=None,
        )

        prompt_embeds = prompt_embeds.to(dtype=self.model.text_encoder_2.dtype, device=self.device)
        attention_mask = (
            attention_mask.to(device=self.device)
            if attention_mask is not None
            else torch.ones(prompt_embeds.shape[:2], dtype=torch.long, device=self.device)
        )
        generated_prompt_embeds = generated_prompt_embeds.to(dtype=self.model.language_model.dtype, device=self.device)

        return generated_prompt_embeds, prompt_embeds, attention_mask

    def get_variance(self, timestep: torch.Tensor, prev_timestep: torch.Tensor) -> torch.Tensor:
        alpha_prod_t = self.model.scheduler.alphas_cumprod[timestep]
        alpha_prod_t_prev = self.get_alpha_prod_t_prev(prev_timestep)
        beta_prod_t = 1 - alpha_prod_t
        beta_prod_t_prev = 1 - alpha_prod_t_prev
        variance = (beta_prod_t_prev / beta_prod_t) * (1 - alpha_prod_t / alpha_prod_t_prev)
        return variance

    def get_alpha_prod_t_prev(self, prev_timestep: torch.Tensor) -> torch.Tensor:
        return self.model.scheduler.alphas_cumprod[prev_timestep] if prev_timestep >= 0 \
            else self.model.scheduler.final_alpha_cumprod

    def unet_forward(self,
                     sample: torch.FloatTensor,
                     timestep: Union[torch.Tensor, float, int],
                     encoder_hidden_states: torch.Tensor,
                     timestep_cond: Optional[torch.Tensor] = None,
                     class_labels: Optional[torch.Tensor] = None,
                     attention_mask: Optional[torch.Tensor] = None,
                     encoder_attention_mask: Optional[torch.Tensor] = None,
                     return_dict: bool = True,
                     cross_attention_kwargs: Optional[Dict[str, Any]] = None,
                     mid_block_additional_residual: Optional[torch.Tensor] = None,
                     replace_h_space: Optional[torch.Tensor] = None,
                     replace_skip_conns: Optional[Dict[int, torch.Tensor]] = None,
                     zero_out_resconns: Optional[Union[int, List]] = None) -> Tuple:

        # Translation
        encoder_hidden_states_1 = class_labels
        class_labels = None
        encoder_attention_mask_1 = encoder_attention_mask
        encoder_attention_mask = None

        # return self.model.unet(sample, timestep,
        #                        encoder_hidden_states=generated_prompt_embeds,
        #                        encoder_hidden_states_1=encoder_hidden_states_1,
        #                        encoder_attention_mask_1=encoder_attention_mask_1,
        #                        ), None, None

        # By default samples have to be AT least a multiple of the overall upsampling factor.
        # The overall upsampling factor is equal to 2 ** (# num of upsampling layers).
        # However, the upsampling interpolation output size can be forced to fit any upsampling size
        # on the fly if necessary.
        default_overall_up_factor = 2 ** self.model.unet.num_upsamplers

        # upsample size should be forwarded when sample is not a multiple of `default_overall_up_factor`
        forward_upsample_size = False
        upsample_size = None

        if any(s % default_overall_up_factor != 0 for s in sample.shape[-2:]):
            # print("Forward upsample size to force interpolation output size.")
            forward_upsample_size = True

        # ensure attention_mask is a bias, and give it a singleton query_tokens dimension
        # expects mask of shape:
        #   [batch, key_tokens]
        # adds singleton query_tokens dimension:
        #   [batch,                    1, key_tokens]
        # this helps to broadcast it as a bias over attention scores, which will be in one of the following shapes:
        #   [batch,  heads, query_tokens, key_tokens] (e.g. torch sdp attn)
        #   [batch * heads, query_tokens, key_tokens] (e.g. xformers or classic attn)
        if attention_mask is not None:
            # assume that mask is expressed as:
            #   (1 = keep,      0 = discard)
            # convert mask into a bias that can be added to attention scores:
            #       (keep = +0,     discard = -10000.0)
            attention_mask = (1 - attention_mask.to(sample.dtype)) * -10000.0
            attention_mask = attention_mask.unsqueeze(1)

        # convert encoder_attention_mask to a bias the same way we do for attention_mask
        if encoder_attention_mask is not None:
            encoder_attention_mask = (1 - encoder_attention_mask.to(sample.dtype)) * -10000.0
            encoder_attention_mask = encoder_attention_mask.unsqueeze(1)

        if encoder_attention_mask_1 is not None:
            encoder_attention_mask_1 = (1 - encoder_attention_mask_1.to(sample.dtype)) * -10000.0
            encoder_attention_mask_1 = encoder_attention_mask_1.unsqueeze(1)

        # 1. time
        timesteps = timestep
        if not torch.is_tensor(timesteps):
            is_mps = sample.device.type == "mps"
            if isinstance(timestep, float):
                dtype = torch.float32 if is_mps else torch.float64
            else:
                dtype = torch.int32 if is_mps else torch.int64
            timesteps = torch.tensor([timesteps], dtype=dtype, device=sample.device)
        elif len(timesteps.shape) == 0:
            timesteps = timesteps[None].to(sample.device)

        # broadcast to batch dimension in a way that's compatible with ONNX/Core ML
        timesteps = timesteps.expand(sample.shape[0])

        t_emb = self.model.unet.time_proj(timesteps)

        # `Timesteps` does not contain any weights and will always return f32 tensors
        # but time_embedding might actually be running in fp16. so we need to cast here.
        # there might be better ways to encapsulate this.
        t_emb = t_emb.to(dtype=sample.dtype)

        emb = self.model.unet.time_embedding(t_emb, timestep_cond)
        aug_emb = None

        if self.model.unet.class_embedding is not None:
            if class_labels is None:
                raise ValueError("class_labels should be provided when num_class_embeds > 0")

            if self.model.unet.config.class_embed_type == "timestep":
                class_labels = self.model.unet.time_proj(class_labels)

                # `Timesteps` does not contain any weights and will always return f32 tensors
                # there might be better ways to encapsulate this.
                class_labels = class_labels.to(dtype=sample.dtype)

            class_emb = self.model.unet.class_embedding(class_labels).to(dtype=sample.dtype)

            if self.model.unet.config.class_embeddings_concat:
                emb = torch.cat([emb, class_emb], dim=-1)
            else:
                emb = emb + class_emb

        emb = emb + aug_emb if aug_emb is not None else emb

        if self.model.unet.time_embed_act is not None:
            emb = self.model.unet.time_embed_act(emb)

        # 2. pre-process
        sample = self.model.unet.conv_in(sample)

        # 3. down
        down_block_res_samples = (sample,)
        for downsample_block in self.model.unet.down_blocks:
            if hasattr(downsample_block, "has_cross_attention") and downsample_block.has_cross_attention:
                sample, res_samples = downsample_block(
                    hidden_states=sample,
                    temb=emb,
                    encoder_hidden_states=encoder_hidden_states,
                    attention_mask=attention_mask,
                    cross_attention_kwargs=cross_attention_kwargs,
                    encoder_attention_mask=encoder_attention_mask,
                    encoder_hidden_states_1=encoder_hidden_states_1,
                    encoder_attention_mask_1=encoder_attention_mask_1,
                )
            else:
                sample, res_samples = downsample_block(hidden_states=sample, temb=emb)

            down_block_res_samples += res_samples

        # 4. mid
        if self.model.unet.mid_block is not None:
            sample = self.model.unet.mid_block(
                sample,
                emb,
                encoder_hidden_states=encoder_hidden_states,
                attention_mask=attention_mask,
                cross_attention_kwargs=cross_attention_kwargs,
                encoder_attention_mask=encoder_attention_mask,
                encoder_hidden_states_1=encoder_hidden_states_1,
                encoder_attention_mask_1=encoder_attention_mask_1,
            )

        if replace_h_space is None:
            h_space = sample.clone()
        else:
            h_space = replace_h_space
            sample = replace_h_space.clone()

        if mid_block_additional_residual is not None:
            sample = sample + mid_block_additional_residual

        extracted_res_conns = {}
        # 5. up
        for i, upsample_block in enumerate(self.model.unet.up_blocks):
            is_final_block = i == len(self.model.unet.up_blocks) - 1

            res_samples = down_block_res_samples[-len(upsample_block.resnets):]
            down_block_res_samples = down_block_res_samples[: -len(upsample_block.resnets)]
            if replace_skip_conns is not None and replace_skip_conns.get(i):
                res_samples = replace_skip_conns.get(i)

            if zero_out_resconns is not None:
                if (type(zero_out_resconns) is int and i >= (zero_out_resconns - 1)) or \
                        type(zero_out_resconns) is list and i in zero_out_resconns:
                    res_samples = [torch.zeros_like(x) for x in res_samples]
                # down_block_res_samples = [torch.zeros_like(x) for x in down_block_res_samples]

            extracted_res_conns[i] = res_samples

            # if we have not reached the final block and need to forward the
            # upsample size, we do it here
            if not is_final_block and forward_upsample_size:
                upsample_size = down_block_res_samples[-1].shape[2:]

            if hasattr(upsample_block, "has_cross_attention") and upsample_block.has_cross_attention:
                sample = upsample_block(
                    hidden_states=sample,
                    temb=emb,
                    res_hidden_states_tuple=res_samples,
                    encoder_hidden_states=encoder_hidden_states,
                    cross_attention_kwargs=cross_attention_kwargs,
                    upsample_size=upsample_size,
                    attention_mask=attention_mask,
                    encoder_attention_mask=encoder_attention_mask,
                    encoder_hidden_states_1=encoder_hidden_states_1,
                    encoder_attention_mask_1=encoder_attention_mask_1,
                )
            else:
                sample = upsample_block(
                    hidden_states=sample, temb=emb, res_hidden_states_tuple=res_samples, upsample_size=upsample_size
                )

        # 6. post-process
        if self.model.unet.conv_norm_out:
            sample = self.model.unet.conv_norm_out(sample)
            sample = self.model.unet.conv_act(sample)
        sample = self.model.unet.conv_out(sample)

        if not return_dict:
            return (sample,)

        return UNet2DConditionOutput(sample=sample), h_space, extracted_res_conns


class StableAudWrapper(PipelineWrapper):
    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        try:
            self.model = StableAudioPipeline.from_pretrained(self.model_id, token=self.token, local_files_only=True
                                                             ).to(self.device)
        except FileNotFoundError:
            self.model = StableAudioPipeline.from_pretrained(self.model_id, token=self.token, local_files_only=False
                                                             ).to(self.device)
        self.model.transformer.eval()
        self.model.vae.eval()

        if self.double_precision:
            self.model = self.model.to(torch.float64)

    def load_scheduler(self) -> None:
        self.model.scheduler = CosineDPMSolverMultistepScheduler.from_pretrained(
            self.model_id, subfolder="scheduler", token=self.token)

    def encode_text(self, prompts: List[str], negative: bool = False, **kwargs) -> Tuple[torch.Tensor, None, torch.Tensor]:
        text_inputs = self.model.tokenizer(
            prompts,
            padding="max_length",
            max_length=self.model.tokenizer.model_max_length,
            truncation=True,
            return_tensors="pt",
        )

        text_input_ids = text_inputs.input_ids.to(self.device)
        attention_mask = text_inputs.attention_mask.to(self.device)

        self.model.text_encoder.eval()
        with torch.no_grad():
            prompt_embeds = self.model.text_encoder(text_input_ids, attention_mask=attention_mask)[0]

        if negative and attention_mask is not None:  # set the masked tokens to the null embed
            prompt_embeds = torch.where(attention_mask.to(torch.bool).unsqueeze(2), prompt_embeds, 0.0)

        prompt_embeds = self.model.projection_model(text_hidden_states=prompt_embeds).text_hidden_states

        if attention_mask is None:
            raise gr.Error("Shouldn't reach here. Please raise an issue if you do.")
            """prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])
            if attention_mask is not None and negative_attention_mask is None:
                negative_attention_mask = torch.ones_like(attention_mask)
            elif attention_mask is None and negative_attention_mask is not None:
                attention_mask = torch.ones_like(negative_attention_mask)"""

        if prompts == [""]:  # empty
            return torch.zeros_like(prompt_embeds, device=prompt_embeds.device), None, None

        prompt_embeds = prompt_embeds * attention_mask.unsqueeze(-1).to(prompt_embeds.dtype)
        prompt_embeds = prompt_embeds * attention_mask.unsqueeze(-1).to(prompt_embeds.dtype)
        return prompt_embeds, None, attention_mask

    def get_fn_STFT(self) -> torch.nn.Module:
        from audioldm.audio import TacotronSTFT
        return TacotronSTFT(
            filter_length=1024,
            hop_length=160,
            win_length=1024,
            n_mel_channels=64,
            sampling_rate=44100,
            mel_fmin=0,
            mel_fmax=22050,
        )

    def vae_encode(self, x: torch.Tensor) -> torch.Tensor:
        x = x.unsqueeze(0)

        audio_vae_length = int(self.model.transformer.config.sample_size * self.model.vae.hop_length)
        audio_shape = (1, self.model.vae.config.audio_channels, audio_vae_length)

        # check num_channels
        if x.shape[1] == 1 and self.model.vae.config.audio_channels == 2:
            x = x.repeat(1, 2, 1)

        audio_length = x.shape[-1]
        audio = x.new_zeros(audio_shape)
        audio[:, :, : min(audio_length, audio_vae_length)] = x[:, :, :audio_vae_length]

        encoded_audio = self.model.vae.encode(audio.to(self.device)).latent_dist
        encoded_audio = encoded_audio.sample()
        return encoded_audio

    def vae_decode(self, x: torch.Tensor) -> torch.Tensor:
        torch.cuda.empty_cache()
        # return self.model.vae.decode(1 / self.model.vae.config.scaling_factor * x).sample
        aud = self.model.vae.decode(x).sample
        return aud[:, :, self.waveform_start:self.waveform_end]

    def setup_extra_inputs(self, x: torch.Tensor, init_timestep: torch.Tensor,
                           extra_info: Optional[Any] = None,
                           audio_start_in_s: float = 0, audio_end_in_s: Optional[float] = None,
                           save_compute: bool = False) -> None:
        max_audio_length_in_s = self.model.transformer.config.sample_size * self.model.vae.hop_length / \
            self.model.vae.config.sampling_rate
        if audio_end_in_s is None:
            audio_end_in_s = max_audio_length_in_s

        if audio_end_in_s - audio_start_in_s > max_audio_length_in_s:
            raise ValueError(
                f"The total audio length requested ({audio_end_in_s-audio_start_in_s}s) is longer "
                f"than the model maximum possible length ({max_audio_length_in_s}). "
                f"Make sure that 'audio_end_in_s-audio_start_in_s<={max_audio_length_in_s}'."
            )

        self.waveform_start = int(audio_start_in_s * self.model.vae.config.sampling_rate)
        self.waveform_end = int(audio_end_in_s * self.model.vae.config.sampling_rate)

        self.seconds_start_hidden_states, self.seconds_end_hidden_states = self.model.encode_duration(
            audio_start_in_s, audio_end_in_s, self.device, False, 1)

        if save_compute:
            self.seconds_start_hidden_states = torch.cat([self.seconds_start_hidden_states, self.seconds_start_hidden_states], dim=0)
            self.seconds_end_hidden_states = torch.cat([self.seconds_end_hidden_states, self.seconds_end_hidden_states], dim=0)

        self.audio_duration_embeds = torch.cat([self.seconds_start_hidden_states,
                                                self.seconds_end_hidden_states], dim=2)

        # 7. Prepare rotary positional embedding
        self.rotary_embedding = get_1d_rotary_pos_embed(
            self.model.rotary_embed_dim,
            x.shape[2] + self.audio_duration_embeds.shape[1],
            use_real=True,
            repeat_interleave_real=False,
        )

        self.model.scheduler._init_step_index(init_timestep)

        # fix lower_order_nums for the reverse step - Option 1: only start from first order
        # self.model.scheduler.lower_order_nums = 0
        # self.model.scheduler.model_outputs = [None] * self.model.scheduler.config.solver_order
        # fix lower_order_nums for the reverse step - Option 2: start from the correct order with history
        t_to_idx = {float(v): k for k, v in enumerate(self.model.scheduler.timesteps)}
        idx = len(self.model.scheduler.timesteps) - t_to_idx[float(init_timestep)] - 1
        self.model.scheduler.model_outputs = [None, extra_info[idx] if extra_info is not None else None]
        self.model.scheduler.lower_order_nums = min(self.model.scheduler.step_index,
                                                    self.model.scheduler.config.solver_order)

        # if rand check:
        #     x *= self.model.scheduler.init_noise_sigma
        # return x

    def sample_xts_from_x0(self, x0: torch.Tensor, num_inference_steps: int = 50) -> torch.Tensor:
        """
        Samples from P(x_1:T|x_0)
        """

        sigmas = self.model.scheduler.sigmas
        shapes = self.get_noise_shape(x0, num_inference_steps + 1)
        xts = torch.zeros(shapes).to(x0.device)
        xts[0] = x0

        timesteps = self.model.scheduler.timesteps.to(self.device)
        t_to_idx = {float(v): k for k, v in enumerate(timesteps)}
        for t in reversed(timesteps):
            # idx = t_to_idx[int(t)]
            idx = num_inference_steps - t_to_idx[float(t)]
            n = torch.randn_like(x0)
            xts[idx] = x0 + n * sigmas[t_to_idx[float(t)]]
        return xts

    def get_zs_from_xts(self, xt: torch.Tensor, xtm1: torch.Tensor, data_pred: torch.Tensor,
                        t: torch.Tensor, numerical_fix: bool = True, first_order: bool = False, **kwargs
                        ) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
        # pred of x0
        sigmas = self.model.scheduler.sigmas
        timesteps = self.model.scheduler.timesteps
        solver_order = self.model.scheduler.config.solver_order

        if self.model.scheduler.step_index is None:
            self.model.scheduler._init_step_index(t)
        curr_step_index = self.model.scheduler.step_index

        # Improve numerical stability for small number of steps
        lower_order_final = (curr_step_index == len(timesteps) - 1) and (
            self.model.scheduler.config.euler_at_final
            or (self.model.scheduler.config.lower_order_final and len(timesteps) < 15)
            or self.model.scheduler.config.final_sigmas_type == "zero")
        lower_order_second = ((curr_step_index == len(timesteps) - 2) and
                              self.model.scheduler.config.lower_order_final and len(timesteps) < 15)

        data_pred = self.model.scheduler.convert_model_output(data_pred, sample=xt)
        for i in range(solver_order - 1):
            self.model.scheduler.model_outputs[i] = self.model.scheduler.model_outputs[i + 1]
        self.model.scheduler.model_outputs[-1] = data_pred

        # instead of brownian noise, here we calculate the noise ourselves
        if (curr_step_index == len(timesteps) - 1) and self.model.scheduler.config.final_sigmas_type == "zero":
            z = torch.zeros_like(xt)
        elif first_order or solver_order == 1 or self.model.scheduler.lower_order_nums < 1 or lower_order_final:
            sigma_t, sigma_s = sigmas[curr_step_index + 1], sigmas[curr_step_index]
            h = torch.log(sigma_s) - torch.log(sigma_t)
            z = (xtm1 - (sigma_t / sigma_s * torch.exp(-h)) * xt - (1 - torch.exp(-2.0 * h)) * data_pred) \
                / (sigma_t * torch.sqrt(1.0 - torch.exp(-2 * h)))
        elif solver_order == 2 or self.model.scheduler.lower_order_nums < 2 or lower_order_second:
            sigma_t = sigmas[curr_step_index + 1]
            sigma_s0 = sigmas[curr_step_index]
            sigma_s1 = sigmas[curr_step_index - 1]
            m0, m1 = self.model.scheduler.model_outputs[-1], self.model.scheduler.model_outputs[-2]
            h, h_0 = torch.log(sigma_s0) - torch.log(sigma_t), torch.log(sigma_s1) - torch.log(sigma_s0)
            r0 = h_0 / h
            D0, D1 = m0, (1.0 / r0) * (m0 - m1)

            # sde-dpmsolver++
            z = (xtm1 - (sigma_t / sigma_s0 * torch.exp(-h)) * xt
                 - (1 - torch.exp(-2.0 * h)) * D0
                 - 0.5 * (1 - torch.exp(-2.0 * h)) * D1) \
                / (sigma_t * torch.sqrt(1.0 - torch.exp(-2 * h)))

        # correction to avoid error accumulation
        if numerical_fix:
            if first_order or solver_order == 1 or self.model.scheduler.lower_order_nums < 1 or lower_order_final:
                xtm1 = self.model.scheduler.dpm_solver_first_order_update(data_pred, sample=xt, noise=z)
            elif solver_order == 2 or self.model.scheduler.lower_order_nums < 2 or lower_order_second:
                xtm1 = self.model.scheduler.multistep_dpm_solver_second_order_update(
                    self.model.scheduler.model_outputs, sample=xt, noise=z)
            # If not perfect recon - maybe TODO fix self.model.scheduler.model_outputs as well?

        if self.model.scheduler.lower_order_nums < solver_order:
            self.model.scheduler.lower_order_nums += 1
        # upon completion increase step index by one
        self.model.scheduler._step_index += 1

        return z, xtm1, self.model.scheduler.model_outputs[-2]

    def get_sr(self) -> int:
        return self.model.vae.config.sampling_rate

    def get_noise_shape(self, x0: torch.Tensor, num_steps: int) -> Tuple[int, int, int]:
        variance_noise_shape = (num_steps,
                                self.model.transformer.config.in_channels,
                                int(self.model.transformer.config.sample_size))
        return variance_noise_shape

    def reverse_step_with_custom_noise(self, model_output: torch.Tensor, timestep: torch.Tensor, sample: torch.Tensor,
                                       variance_noise: Optional[torch.Tensor] = None,
                                       first_order: bool = False, **kwargs
                                       ) -> torch.Tensor:
        if self.model.scheduler.step_index is None:
            self.model.scheduler._init_step_index(timestep)

        # Improve numerical stability for small number of steps
        lower_order_final = (self.model.scheduler.step_index == len(self.model.scheduler.timesteps) - 1) and (
            self.model.scheduler.config.euler_at_final
            or (self.model.scheduler.config.lower_order_final and len(self.model.scheduler.timesteps) < 15)
            or self.model.scheduler.config.final_sigmas_type == "zero"
        )
        lower_order_second = (
            (self.model.scheduler.step_index == len(self.model.scheduler.timesteps) - 2) and
            self.model.scheduler.config.lower_order_final and len(self.model.scheduler.timesteps) < 15
        )

        model_output = self.model.scheduler.convert_model_output(model_output, sample=sample)
        for i in range(self.model.scheduler.config.solver_order - 1):
            self.model.scheduler.model_outputs[i] = self.model.scheduler.model_outputs[i + 1]
        self.model.scheduler.model_outputs[-1] = model_output

        if variance_noise is None:
            if self.model.scheduler.noise_sampler is None:
                self.model.scheduler.noise_sampler = BrownianTreeNoiseSampler(
                    model_output, sigma_min=self.model.scheduler.config.sigma_min,
                    sigma_max=self.model.scheduler.config.sigma_max, seed=None)
            variance_noise = self.model.scheduler.noise_sampler(
                self.model.scheduler.sigmas[self.model.scheduler.step_index],
                self.model.scheduler.sigmas[self.model.scheduler.step_index + 1]).to(model_output.device)

        if first_order or self.model.scheduler.config.solver_order == 1 or \
                self.model.scheduler.lower_order_nums < 1 or lower_order_final:
            prev_sample = self.model.scheduler.dpm_solver_first_order_update(
                model_output, sample=sample, noise=variance_noise)
        elif self.model.scheduler.config.solver_order == 2 or \
                self.model.scheduler.lower_order_nums < 2 or lower_order_second:
            prev_sample = self.model.scheduler.multistep_dpm_solver_second_order_update(
                self.model.scheduler.model_outputs, sample=sample, noise=variance_noise)

        if self.model.scheduler.lower_order_nums < self.model.scheduler.config.solver_order:
            self.model.scheduler.lower_order_nums += 1

        # upon completion increase step index by one
        self.model.scheduler._step_index += 1

        return prev_sample

    def unet_forward(self,
                     sample: torch.FloatTensor,
                     timestep: Union[torch.Tensor, float, int],
                     encoder_hidden_states: torch.Tensor,
                     encoder_attention_mask: Optional[torch.Tensor] = None,
                     return_dict: bool = True,
                     **kwargs) -> Tuple:

        # Create text_audio_duration_embeds and audio_duration_embeds
        embeds = torch.cat([encoder_hidden_states, self.seconds_start_hidden_states, self.seconds_end_hidden_states],
                           dim=1)
        if encoder_attention_mask is None:
            # handle the batched case
            if embeds.shape[0] > 1:
                embeds[0] = torch.zeros_like(embeds[0], device=embeds.device)
            else:
                embeds = torch.zeros_like(embeds, device=embeds.device)

        noise_pred = self.model.transformer(sample,
                                            timestep.unsqueeze(0),
                                            encoder_hidden_states=embeds,
                                            global_hidden_states=self.audio_duration_embeds,
                                            rotary_embedding=self.rotary_embedding)

        if not return_dict:
            return (noise_pred.sample,)

        return noise_pred, None, None


def load_model(model_id: str, device: torch.device,
               double_precision: bool = False, token: Optional[str] = None) -> PipelineWrapper:
    if 'audioldm2' in model_id:
        ldm_stable = AudioLDM2Wrapper(model_id=model_id, device=device, double_precision=double_precision, token=token)
    elif 'stable-audio' in model_id:
        ldm_stable = StableAudWrapper(model_id=model_id, device=device, double_precision=double_precision, token=token)
    ldm_stable.load_scheduler()
    torch.cuda.empty_cache()
    return ldm_stable