#
# SPDX-FileCopyrightText: Copyright (c) 1993-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from __future__ import annotations

import argparse
import inspect
import os
import random
import time
import warnings
from typing import Any, List

import numpy as np
import tensorrt as trt
import torch
from cuda.bindings import runtime as cudart
from diffusers.video_processor import VideoProcessor
from flux.content_filters import PixtralContentFilter
from tqdm import tqdm

from demo_diffusion import path as path_module
from demo_diffusion.model import (
    AutoencoderKLWanEncoderModel,
    AutoencoderKLWanModel,
    CosmosTransformerModel,
    T5Model,
    make_tokenizer,
)
from demo_diffusion.pipeline.diffusion_pipeline import DiffusionPipeline
from demo_diffusion.pipeline.type import PIPELINE_TYPE

TRT_LOGGER = trt.Logger(trt.Logger.ERROR)


class CosmosPipeline(DiffusionPipeline):
    """
    Application showcasing the acceleration of Cosmos pipelines using Nvidia TensorRT.
    """

    def __init__(
        self,
        version="cosmos-predict2-2b",
        pipeline_type=PIPELINE_TYPE.TXT2IMG,
        guidance_scale=6.0,
        max_sequence_length=512,
        t5_weight_streaming_budget_percentage=None,
        transformer_weight_streaming_budget_percentage=None,
        **kwargs,
    ):
        """
        Initializes the Cosmos pipeline.

        Args:
            version (`str`, defaults to `cosmos-1.0-7B`)
                Version of the underlying Cosmos model.
            guidance_scale (`float`, defaults to 3.5):
                Guidance scale is enabled by setting as > 1.
                Higher guidance scale encourages to generate images that are closely linked to the text prompt, usually at the expense of lower image quality.
            max_sequence_length (`int`, defaults to 512):
                Maximum sequence length to use with the `prompt`.
            t5_weight_streaming_budget_percentage (`int`, defaults to None):
                Weight streaming budget as a percentage of the size of total streamable weights for the T5 model.
            transformer_weight_streaming_budget_percentage (`int`, defaults to None):
                Weight streaming budget as a percentage of the size of total streamable weights for the Transformer model.
        """
        super().__init__(
            version=version,
            pipeline_type=pipeline_type,
            text_encoder_weight_streaming_budget_percentage=t5_weight_streaming_budget_percentage,
            denoiser_weight_streaming_budget_percentage=transformer_weight_streaming_budget_percentage,
            **kwargs,
        )
        self.guidance_scale = guidance_scale
        self.max_sequence_length = max_sequence_length
        self.do_classifier_free_guidance = self.guidance_scale > 1

        # WAR ONNX export error: Exporting the operator 'aten::_upsample_nearest_exact2d' to ONNX opset version 19 is not supported
        self.config["vae_torch_fallback"] = True
        self.config["vae_encoder_torch_fallback"] = True

    @classmethod
    def FromArgs(cls, args: argparse.Namespace, pipeline_type: PIPELINE_TYPE) -> CosmosPipeline:
        """Factory method to construct a `CosmosPipeline` object from parsed arguments.

        Overrides:
            DiffusionPipeline.FromArgs
        """
        MAX_BATCH_SIZE = 4
        DEVICE = "cuda"
        DO_RETURN_LATENTS = False

        # Resolve all paths.
        dd_path = path_module.resolve_path(
            cls.get_model_names(pipeline_type), args, pipeline_type, cls._get_pipeline_uid(args.version)
        )

        return cls(
            dd_path=dd_path,
            version=args.version,
            pipeline_type=pipeline_type,
            guidance_scale=args.guidance_scale,
            max_sequence_length=args.max_sequence_length,
            bf16=args.bf16,
            low_vram=args.low_vram,
            torch_fallback=args.torch_fallback,
            weight_streaming=args.ws,
            t5_weight_streaming_budget_percentage=args.t5_ws_percentage,
            transformer_weight_streaming_budget_percentage=args.transformer_ws_percentage,
            max_batch_size=MAX_BATCH_SIZE,
            denoising_steps=args.denoising_steps,
            scheduler=args.scheduler,
            device=DEVICE,
            output_dir=args.output_dir,
            hf_token=args.hf_token,
            verbose=args.verbose,
            nvtx_profile=args.nvtx_profile,
            use_cuda_graph=args.use_cuda_graph,
            framework_model_dir=args.framework_model_dir,
            return_latents=DO_RETURN_LATENTS,
            torch_inference=args.torch_inference,
        )

    @classmethod
    def get_model_names(cls, pipeline_type: PIPELINE_TYPE, controlnet_type: str = None) -> List[str]:
        """Return a list of model names used by this pipeline.

        Overrides:
            DiffusionPipeline.get_model_names
        """
        if pipeline_type.is_video2world():
            return ["vae_encoder", "t5", "transformer", "vae"]
        return ["t5", "transformer", "vae"]

    def download_onnx_models(self, model_name: str, model_config: dict[str, Any]) -> None:
        raise ValueError("ONNX models download is not supported for the Cosmos Pipeline")

    def _initialize_models(self, framework_model_dir, int8, fp8, fp4):
        # Load text tokenizer(s)
        self.tokenizer = make_tokenizer(
            self.version, self.pipeline_type, self.hf_token, framework_model_dir, tokenizer_type="t5"
        )

        # Load pipeline models
        models_args = {
            "version": self.version,
            "pipeline": self.pipeline_type,
            "device": self.device,
            "hf_token": self.hf_token,
            "verbose": self.verbose,
            "framework_model_dir": framework_model_dir,
            "max_batch_size": self.max_batch_size,
        }

        self.fp16 = True if not self.bf16 else False
        self.tf32 = True
        if "t5" in self.stages:
            # Known accuracy issues with FP16
            self.models["t5"] = T5Model(
                **models_args,
                fp16=self.fp16,
                tf32=self.tf32,
                bf16=self.bf16,
                text_maxlen=self.max_sequence_length,
                use_attention_mask=True,
            )

        if "transformer" in self.stages:
            self.models["transformer"] = CosmosTransformerModel(
                **models_args,
                bf16=self.bf16,
                fp16=self.fp16,
                int8=int8,
                fp8=fp8,
                tf32=self.tf32,
                text_maxlen=self.max_sequence_length,
                weight_streaming=self.weight_streaming,
                weight_streaming_budget_percentage=self.denoiser_weight_streaming_budget_percentage,
            )

        if "vae" in self.stages:
            self.models["vae"] = AutoencoderKLWanModel(**models_args, fp16=False, tf32=self.tf32, bf16=self.bf16)

        if "vae_encoder" in self.stages:
            self.models["vae_encoder"] = AutoencoderKLWanEncoderModel(
                **models_args, fp16=False, tf32=self.tf32, bf16=self.bf16
            )

        self.vae_scale_factor_temporal = (
            2 ** sum(self.models["vae"].config["temperal_downsample"])
            if "vae" in self.stages and self.models["vae"] is not None
            else 4
        )
        self.vae_scale_factor_spatial = (
            2 ** len(self.models["vae"].config["temperal_downsample"])
            if "vae" in self.stages and self.models["vae"] is not None
            else 8
        )

        self.video_processor = VideoProcessor(vae_scale_factor=self.vae_scale_factor_spatial)

    def encode_video(self, video):
        self.profile_start("vae_encoder", color="red")
        cast_to = (
            torch.float16
            if self.models["vae_encoder"].fp16
            else torch.bfloat16 if self.models["vae_encoder"].bf16 else torch.float32
        )
        video = video.to(dtype=cast_to)
        if self.torch_inference:
            image_latents = self.torch_models["vae_encoder"](video)
        else:
            image_latents = self.run_engine("vae_encoder", {"images": video})["latent"]
        self.profile_stop("vae_encoder")
        return image_latents

    def initialize_latents_text2image(
        self,
        batch_size,
        num_channels_latents,
        num_latent_frames,
        latent_height,
        latent_width,
        latents_dtype=torch.float32,
    ):
        latents_shape = (batch_size, num_channels_latents, num_latent_frames, latent_height, latent_width)
        latents = torch.randn(
            latents_shape,
            device=self.device,
            dtype=latents_dtype,
            generator=self.generator,
        )

        return latents * self.scheduler.config.sigma_max

    def initialize_latents_video2world(
        self,
        video,
        batch_size,
        num_channels_latents,
        num_frames,
        latent_height,
        latent_width,
        latents_dtype=torch.float32,
        do_classifier_free_guidance=False,
    ):
        num_cond_frames = video.size(2)
        if num_cond_frames >= num_frames:
            # Take the last `num_frames` frames for conditioning
            num_cond_latent_frames = (num_frames - 1) // self.vae_scale_factor_temporal + 1
            video = video[:, :, -num_frames:]
        else:
            num_cond_latent_frames = (num_cond_frames - 1) // self.vae_scale_factor_temporal + 1
            num_padding_frames = num_frames - num_cond_frames
            last_frame = video[:, :, -1:]
            padding = last_frame.repeat(1, 1, num_padding_frames, 1, 1)
            video = torch.cat([video, padding], dim=2)

        # Encode video
        with self.model_memory_manager(["vae_encoder"], low_vram=self.low_vram):
            video_latents = self.encode_video(
                video=video,
            )

        latents_mean = (
            torch.tensor(self.models["vae"].config["latents_mean"])
            .view(1, self.models["vae"].config["z_dim"], 1, 1, 1)
            .to(self.device, latents_dtype)
        )
        latents_std = (
            torch.tensor(self.models["vae"].config["latents_std"])
            .view(1, self.models["vae"].config["z_dim"], 1, 1, 1)
            .to(self.device, latents_dtype)
        )
        init_latents = (video_latents - latents_mean) / latents_std * self.scheduler.config.sigma_data

        num_latent_frames = (num_frames - 1) // self.vae_scale_factor_temporal + 1
        shape = (batch_size, num_channels_latents, num_latent_frames, latent_height, latent_width)

        latents = torch.randn(
            shape,
            device=self.device,
            dtype=latents_dtype,
            generator=self.generator,
        )

        latents = latents * self.scheduler.config.sigma_max

        padding_shape = (batch_size, 1, num_latent_frames, latent_height, latent_width)
        ones_padding = latents.new_ones(padding_shape)
        zeros_padding = latents.new_zeros(padding_shape)

        cond_indicator = latents.new_zeros(1, 1, latents.size(2), 1, 1)
        cond_indicator[:, :, :num_cond_latent_frames] = 1.0
        cond_mask = cond_indicator * ones_padding + (1 - cond_indicator) * zeros_padding

        uncond_indicator = uncond_mask = None
        if do_classifier_free_guidance:
            uncond_indicator = latents.new_zeros(1, 1, latents.size(2), 1, 1)
            uncond_indicator[:, :, :num_cond_latent_frames] = 1.0
            uncond_mask = uncond_indicator * ones_padding + (1 - uncond_indicator) * zeros_padding

        return latents, init_latents, cond_indicator, uncond_indicator, cond_mask, uncond_mask

    # Copied from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/flux/pipeline_flux_img2img.py#L416C1
    def get_timesteps(self, num_inference_steps, strength):
        # get the original timestep using init_timestep
        init_timestep = min(num_inference_steps * strength, num_inference_steps)

        t_start = int(max(num_inference_steps - init_timestep, 0))
        timesteps = self.scheduler.timesteps[t_start * self.scheduler.order :]
        if hasattr(self.scheduler, "set_begin_index"):
            self.scheduler.set_begin_index(t_start * self.scheduler.order)

        return timesteps, num_inference_steps - t_start

    def _duplicate_text_embeddings(self, batch_size, text_embeddings, num_outputs_per_prompt):
        # duplicate text embeddings for each generation per prompt, using mps friendly method
        _, seq_len, _ = text_embeddings.shape
        text_embeddings = text_embeddings.repeat(1, num_outputs_per_prompt, 1)
        text_embeddings = text_embeddings.view(batch_size * num_outputs_per_prompt, seq_len, -1)
        return text_embeddings

    def _prepare_timesteps(self, num_inference_steps):
        """Prepare timesteps for the scheduler."""
        sigmas_dtype = torch.float32 if torch.backends.mps.is_available() else torch.float64
        sigmas = torch.linspace(0, 1, num_inference_steps, dtype=sigmas_dtype)
        accept_sigmas = "sigmas" in set(inspect.signature(self.scheduler.set_timesteps).parameters.keys())
        if not accept_sigmas:
            raise ValueError(
                f"The current scheduler class {self.scheduler.__class__}'s `set_timesteps` does not support custom"
                f" sigmas schedules. Please check whether you are using the correct scheduler."
            )
        self.scheduler.set_timesteps(sigmas=sigmas, device=self.device)
        timesteps = self.scheduler.timesteps
        num_inference_steps = len(timesteps)
        if self.scheduler.config.get("final_sigmas_type", "zero") == "sigma_min":
            # Replace the last sigma (which is zero) with the minimum sigma value
            self.scheduler.sigmas[-1] = self.scheduler.sigmas[-2]
        return timesteps, num_inference_steps

    def _encode_text_prompts(self, prompt, negative_prompt, batch_size, num_outputs_per_prompt):
        """Encode text prompts using T5 encoder."""
        with self.model_memory_manager(["t5"], low_vram=self.low_vram):
            text_embeddings = self.encode_prompt(prompt)
            text_embeddings = self._duplicate_text_embeddings(batch_size, text_embeddings, num_outputs_per_prompt)
            negative_text_embeddings = None
            if self.do_classifier_free_guidance:
                negative_text_embeddings = self.encode_prompt(negative_prompt)
                negative_text_embeddings = self._duplicate_text_embeddings(
                    batch_size, negative_text_embeddings, num_outputs_per_prompt
                )
        return text_embeddings, negative_text_embeddings

    def _get_latents_normalization_params(self, device, dtype):
        """Get latents normalization parameters from VAE config."""
        latents_mean = (
            torch.tensor(self.models["vae"].config["latents_mean"])
            .view(1, self.models["vae"].config["z_dim"], 1, 1, 1)
            .to(device, dtype)
        )
        latents_std = (
            torch.tensor(self.models["vae"].config["latents_std"])
            .view(1, self.models["vae"].config["z_dim"], 1, 1, 1)
            .to(device, dtype)
        )
        return latents_mean, latents_std

    def _normalize_and_decode_latents(self, latents, is_video2world=False):
        """Normalize latents and decode using VAE."""
        latents_mean, latents_std = self._get_latents_normalization_params(latents.device, latents.dtype)

        if is_video2world:
            # For video2world: latents * std / sigma_data + mean
            latents = latents * latents_std / self.scheduler.config.sigma_data + latents_mean
        else:
            # For text2image: latents / (1/std) / sigma_data + mean
            latents_std_inv = 1.0 / latents_std
            latents = latents / latents_std_inv / self.scheduler.config.sigma_data + latents_mean

        with self.model_memory_manager(["vae"], low_vram=self.low_vram):
            video = self.decode_latent(latents)

        return video

    def encode_prompt(self, prompt, encoder="t5"):
        self.profile_start(encoder, color="green")

        def tokenize(prompt):
            text_inputs = self.tokenizer(
                prompt,
                padding="max_length",
                max_length=self.max_sequence_length,
                truncation=True,
                return_overflowing_tokens=False,
                return_length=False,
                return_tensors="pt",
            )
            text_input_ids = text_inputs.input_ids.to(self.device)
            attention_mask = text_inputs.attention_mask.bool().to(self.device)

            untruncated_ids = self.tokenizer(prompt, padding="longest", return_tensors="pt").input_ids.to(self.device)
            if untruncated_ids.shape[-1] >= text_input_ids.shape[-1] and not torch.equal(
                text_input_ids, untruncated_ids
            ):
                removed_text = self.tokenizer.batch_decode(untruncated_ids[:, self.max_sequence_length - 1 : -1])
                warnings.warn(
                    "The following part of your input was truncated because `max_sequence_length` is set to "
                    f"{self.max_sequence_length} tokens: {removed_text}"
                )

            if self.torch_inference or self.torch_fallback[encoder]:
                text_encoder_output = self.torch_models[encoder](
                    text_input_ids, attention_mask=attention_mask
                ).last_hidden_state
            else:
                # NOTE: output tensor for the encoder must be cloned because it will be overwritten when called again for prompt2
                text_encoder_output = self.run_engine(
                    encoder, {"input_ids": text_input_ids, "attention_mask": attention_mask}
                )["text_embeddings"]

            lengths = attention_mask.sum(dim=1).cpu()
            for i, length in enumerate(lengths):
                text_encoder_output[i, length:] = 0
            return text_encoder_output

        # Tokenize prompt
        text_encoder_output = tokenize(prompt)

        self.profile_stop(encoder)
        return (
            text_encoder_output.to(torch.float16)
            if self.fp16
            else text_encoder_output.to(torch.bfloat16) if self.bf16 else text_encoder_output.to(torch.float32)
        )

    def denoise_latent(
        self,
        latents,
        timesteps,
        text_embeddings,
        negative_text_embeddings,
        padding_mask,
        denoiser="transformer",
    ):
        do_autocast = self.torch_inference != "" and self.models[denoiser].fp16
        with torch.autocast("cuda", enabled=do_autocast, dtype=torch.float32):
            self.profile_start(denoiser, color="blue")

            for step_index, timestep in tqdm(enumerate(timesteps), total=len(timesteps), desc="Denoising"):
                # Prepare latents
                cast_to = (
                    torch.float16
                    if self.models[denoiser].fp16
                    else torch.bfloat16 if self.models[denoiser].bf16 else torch.float32
                )
                current_sigma = self.scheduler.sigmas[step_index]
                current_t = current_sigma / (current_sigma + 1)
                c_in = 1 - current_t
                c_skip = 1 - current_t
                c_out = -current_t
                timestep_inp = current_t.expand(latents.shape[0]).to(cast_to)  # [B, 1, T, 1, 1]
                latents_input = (latents * c_in).to(cast_to)
                # prepare inputs
                params = {
                    "hidden_states": latents_input,
                    "timestep": timestep_inp,
                    "encoder_hidden_states": text_embeddings,
                    "padding_mask": padding_mask,
                }

                if self.torch_inference or self.torch_fallback[denoiser]:
                    noise_pred = self.torch_models[denoiser](**params)["sample"]
                else:
                    noise_pred = self.run_engine(denoiser, params)["latent"].clone()

                noise_pred = (c_skip * latents + c_out * noise_pred.float()).to(cast_to)
                if self.do_classifier_free_guidance:
                    params = {
                        "hidden_states": latents_input,
                        "timestep": timestep_inp,
                        "encoder_hidden_states": negative_text_embeddings,
                        "padding_mask": padding_mask,
                    }

                    # Predict the noise residual
                    if self.torch_inference or self.torch_fallback[denoiser]:
                        noise_pred_uncond = self.torch_models[denoiser](**params)["sample"]
                    else:
                        noise_pred_uncond = self.run_engine(denoiser, params)["latent"].clone()

                    noise_pred_uncond = (c_skip * latents + c_out * noise_pred_uncond.float()).to(cast_to)
                    noise_pred = noise_pred + self.guidance_scale * (noise_pred - noise_pred_uncond)

                noise_pred = (latents - noise_pred) / current_sigma
                latents = self.scheduler.step(noise_pred, timestep, latents, return_dict=False)[0]

        self.profile_stop(denoiser)
        return latents.to(dtype=torch.bfloat16) if self.bf16 else latents.to(dtype=torch.float32)

    def denoise_latent_video2world(
        self,
        latents,
        timesteps,
        text_embeddings,
        negative_text_embeddings,
        padding_mask,
        fps,
        cond_mask,
        uncond_mask,
        t_conditioning,
        cond_indicator,
        conditioning_latents,
        uncond_indicator,
        unconditioning_latents,
        denoiser="transformer",
    ):
        do_autocast = self.torch_inference != "" and self.models[denoiser].fp16
        with torch.autocast("cuda", enabled=do_autocast, dtype=torch.float32):
            self.profile_start(denoiser, color="blue")

            for step_index, timestep in tqdm(enumerate(timesteps), total=len(timesteps), desc="Denoising"):
                # Prepare latents
                cast_to = (
                    torch.float16
                    if self.models[denoiser].fp16
                    else torch.bfloat16 if self.models[denoiser].bf16 else torch.float32
                )
                current_sigma = self.scheduler.sigmas[step_index]
                current_t = current_sigma / (current_sigma + 1)
                c_in = 1 - current_t
                c_skip = 1 - current_t
                c_out = -current_t
                timestep_inp = current_t.view(1, 1, 1, 1, 1).expand(
                    latents.size(0), -1, latents.size(2), -1, -1
                )  # [B, 1, T, 1, 1]
                latents_input = latents * c_in
                latents_input = (cond_indicator * conditioning_latents + (1 - cond_indicator) * latents_input).to(
                    cast_to
                )
                timestep_inp = (cond_indicator * t_conditioning + (1 - cond_indicator) * timestep_inp).to(cast_to)

                # prepare inputs
                params = {
                    "hidden_states": latents_input,
                    "timestep": timestep_inp,
                    "encoder_hidden_states": text_embeddings,
                    "padding_mask": padding_mask,
                    "fps": fps,
                    "condition_mask": cond_mask,
                }

                if self.torch_inference or self.torch_fallback[denoiser]:
                    noise_pred = self.torch_models[denoiser](**params)["sample"]
                else:
                    noise_pred = self.run_engine(denoiser, params)["latent"].clone()

                noise_pred = (c_skip * latents + c_out * noise_pred.float()).to(cast_to)
                noise_pred = cond_indicator * conditioning_latents + (1 - cond_indicator) * noise_pred
                if self.do_classifier_free_guidance:
                    latents_input = latents * c_in
                    latents_input = (
                        uncond_indicator * unconditioning_latents + (1 - uncond_indicator) * latents_input
                    ).to(cast_to)
                    timestep_inp = (uncond_indicator * t_conditioning + (1 - uncond_indicator) * timestep_inp).to(
                        cast_to
                    )
                    params = {
                        "hidden_states": latents_input,
                        "timestep": timestep_inp,
                        "encoder_hidden_states": negative_text_embeddings,
                        "padding_mask": padding_mask,
                        "fps": fps,
                        "condition_mask": uncond_mask,
                    }

                    # Predict the noise residual
                    if self.torch_inference or self.torch_fallback[denoiser]:
                        noise_pred_uncond = self.torch_models[denoiser](**params)["sample"]
                    else:
                        noise_pred_uncond = self.run_engine(denoiser, params)["latent"].clone()

                    noise_pred_uncond = (c_skip * latents + c_out * noise_pred_uncond.float()).to(cast_to)
                    noise_pred_uncond = (
                        uncond_indicator * unconditioning_latents + (1 - uncond_indicator) * noise_pred_uncond
                    )
                    noise_pred = noise_pred + self.guidance_scale * (noise_pred - noise_pred_uncond)

                noise_pred = (latents - noise_pred) / current_sigma
                latents = self.scheduler.step(noise_pred, timestep, latents, return_dict=False)[0]

        self.profile_stop(denoiser)
        return latents.to(dtype=torch.bfloat16) if self.bf16 else latents.to(dtype=torch.float32)

    def decode_latent(self, latents, decoder="vae"):
        self.profile_start(decoder, color="red")
        cast_to = (
            torch.float16
            if self.models[decoder].fp16
            else torch.bfloat16 if self.models[decoder].bf16 else torch.float32
        )
        latents = latents.to(dtype=cast_to)

        if self.torch_inference or self.torch_fallback[decoder]:
            video = self.torch_models[decoder](latents, return_dict=False)[0]
        else:
            video = self.run_engine(decoder, {"latent": latents})["frames"]

        self.profile_stop(decoder)
        return video

    def post_process_video(self, video):
        # Post-process video
        video = self.video_processor.postprocess_video(video, output_type="np")
        video = (video * 255).astype(np.uint8)
        video_batch = []
        for vid in video:
            # vid = self.safety_checker.check_video_safety(vid)
            video_batch.append(vid)
        video = np.stack(video_batch).astype(np.float32) / 255.0 * 2 - 1
        video = torch.from_numpy(video).permute(0, 4, 1, 2, 3)
        video = self.video_processor.postprocess_video(video, output_type="pil")

        if self.pipeline_type.is_video2world():
            return video

        image = [batch[0] for batch in video]
        if isinstance(video, torch.Tensor):
            image = torch.stack(image)
        elif isinstance(video, np.ndarray):
            image = np.stack(image)

        return image

    def _finalize_generation(self, video, walltime_ms, num_inference_steps, batch_size, warmup, save_output):
        """Handle post-processing, saving, and performance reporting."""
        if not warmup:
            self.print_summary(num_inference_steps, walltime_ms, batch_size)
            if not self.return_latents and save_output:
                # post-process video
                processed_output = self.post_process_video(video)

                # save output
                if self.pipeline_type.is_video2world():
                    return (processed_output[0], walltime_ms)
                return (np.array(processed_output), walltime_ms)

    def _check_integrity(self, images):
        integrity_checker = PixtralContentFilter(self.device)
        for image in images:
            image_ = np.array(image) / 255.0
            image_ = 2 * image_ - 1
            image_ = torch.from_numpy(image_).to(self.device, dtype=torch.float32).permute(0, 3, 1, 2)
            if integrity_checker.test_image(image_):
                raise ValueError("Your image has been flagged. Choose another prompt/image or try again.")

    def save_images(self, prompt, images, check_integrity=False):
        if check_integrity:
            self._check_integrity(images)
        for image in images:
            self.save_image(image, self.pipeline_type.name.lower(), prompt, self.seed)

    def save_video(
        self,
        prompt,
        videos,
        check_integrity=False,
    ):
        for frames in videos:
            if check_integrity:
                self._check_integrity([frames])
            prompt_prefix = "".join(set([prompt[i].replace(" ", "_")[:10] for i in range(len(prompt))]))
            video_name_prefix = "-".join(
                [self.pipeline_type.name.lower(), "fp16", str(self.seed), str(random.randint(1000, 9999))]
            )
            video_name_suffix = "torch" if self.torch_inference else "trt"
            video_path = prompt_prefix + "-" + video_name_prefix + "-" + video_name_suffix + ".gif"
            print(f"Saving video to: {video_path}")
            frames[0].save(
                os.path.join(self.output_dir, video_path),
                save_all=True,
                optimize=False,
                append_images=frames[1:],
                loop=0,
            )

    def print_summary(self, denoising_steps, walltime_ms, batch_size):
        print("|-----------------|--------------|")
        print("| {:^15} | {:^12} |".format("Module", "Latency"))
        print("|-----------------|--------------|")
        for stage in self.stages:
            print(
                "| {:^15} | {:>9.2f} ms |".format(
                    stage + " x " + str(denoising_steps) if stage == "transformer" else stage,
                    cudart.cudaEventElapsedTime(self.events[stage][0], self.events[stage][1])[1],
                )
            )
        print("|-----------------|--------------|")
        print("| {:^15} | {:>9.2f} ms |".format("Pipeline", walltime_ms))
        print("|-----------------|--------------|")
        print("Throughput: {:.2f} image/s".format(batch_size * 1000.0 / walltime_ms))

    def generate_image(
        self,
        prompt,
        negative_prompt,
        image_height,
        image_width,
        num_frames=1,
        num_images_per_prompt=1,
        save_image=True,
        warmup=False,
    ):
        batch_size = len(prompt)

        # Spatial dimensions of latent tensor
        num_latent_frames = (num_frames - 1) // self.vae_scale_factor_temporal + 1
        latent_height = image_height // self.vae_scale_factor_spatial
        latent_width = image_width // self.vae_scale_factor_spatial

        num_inference_steps = self.denoising_steps

        with torch.inference_mode(), trt.Runtime(TRT_LOGGER):
            torch.cuda.synchronize()
            e2e_tic = time.perf_counter()

            # Prepare timesteps
            timesteps, num_inference_steps = self._prepare_timesteps(num_inference_steps)

            # T5 text encoder
            text_embeddings, negative_text_embeddings = self._encode_text_prompts(
                prompt, negative_prompt, batch_size, num_images_per_prompt
            )

            num_channels_latents = self.models["transformer"].config["in_channels"]
            latents_dtype = torch.float16 if self.fp16 else torch.bfloat16 if self.bf16 else torch.float32

            # Initialize latents
            latents = self.initialize_latents_text2image(
                batch_size=batch_size,
                num_channels_latents=num_channels_latents,
                num_latent_frames=num_latent_frames,
                latent_height=latent_height,
                latent_width=latent_width,
                latents_dtype=latents_dtype,
            )
            padding_mask = latents.new_zeros(1, 1, image_height, image_width, dtype=latents_dtype)

            # denoiser
            with self.model_memory_manager(["transformer"], low_vram=self.low_vram):
                latents = self.denoise_latent(
                    latents,
                    timesteps,
                    text_embeddings,
                    negative_text_embeddings,
                    padding_mask,
                )

            # VAE decode latent
            video = self._normalize_and_decode_latents(latents, is_video2world=False)

            torch.cuda.synchronize()
            e2e_toc = time.perf_counter()

        walltime_ms = (e2e_toc - e2e_tic) * 1000.0
        return self._finalize_generation(
            video,
            walltime_ms,
            num_inference_steps,
            batch_size,
            warmup,
            save_image,
        )

    def generate_video(
        self,
        prompt,
        negative_prompt,
        image_height,
        image_width,
        input_image=None,
        input_video=None,
        num_frames=1,
        fps=16,
        num_videos_per_prompt=1,
        sigma_conditioning=0.0001,
        save_video=True,
        warmup=False,
    ):
        batch_size = len(prompt)

        # Spatial dimensions of latent tensor
        latent_height = image_height // self.vae_scale_factor_spatial
        latent_width = image_width // self.vae_scale_factor_spatial

        num_inference_steps = self.denoising_steps

        with torch.inference_mode(), trt.Runtime(TRT_LOGGER):
            torch.cuda.synchronize()
            e2e_tic = time.perf_counter()

            # Prepare timesteps
            timesteps, num_inference_steps = self._prepare_timesteps(num_inference_steps)

            # T5 text encoder
            text_embeddings, negative_text_embeddings = self._encode_text_prompts(
                prompt, negative_prompt, batch_size, num_videos_per_prompt
            )

            num_channels_latents = self.models["transformer"].config["in_channels"] - 1
            latents_dtype = torch.float16 if self.fp16 else torch.bfloat16 if self.bf16 else torch.float32

            # Process input conditioning
            if input_image is not None:
                video = (
                    self.video_processor.preprocess(input_image, image_height, image_width)
                    .unsqueeze(2)
                    .to(device=self.device, dtype=latents_dtype)
                )
            elif input_video is not None:
                video = self.video_processor.preprocess_video(input_video, image_height, image_width).to(
                    device=self.device, dtype=latents_dtype
                )
            else:
                raise ValueError("Video2world pipeline requires either input_image or input_video to be provided")

            # Initialize latents
            latents, conditioning_latents, cond_indicator, uncond_indicator, cond_mask, uncond_mask = (
                self.initialize_latents_video2world(
                    video,
                    batch_size=batch_size,
                    num_channels_latents=num_channels_latents,
                    num_frames=num_frames,
                    latent_height=latent_height,
                    latent_width=latent_width,
                    latents_dtype=latents_dtype,
                    do_classifier_free_guidance=self.do_classifier_free_guidance,
                )
            )
            unconditioning_latents = None

            cond_mask = cond_mask.to(latents_dtype)
            if self.do_classifier_free_guidance:
                uncond_mask = uncond_mask.to(latents_dtype)
                unconditioning_latents = conditioning_latents

            padding_mask = latents.new_zeros(1, 1, image_height, image_width, dtype=latents_dtype)
            sigma_conditioning = torch.tensor(sigma_conditioning, dtype=torch.float32, device=self.device)
            t_conditioning = sigma_conditioning / (sigma_conditioning + 1)

            # denoiser
            with self.model_memory_manager(["transformer"], low_vram=self.low_vram):
                latents = self.denoise_latent_video2world(
                    latents,
                    timesteps,
                    text_embeddings,
                    negative_text_embeddings,
                    padding_mask,
                    fps,
                    cond_mask,
                    uncond_mask,
                    t_conditioning,
                    cond_indicator,
                    conditioning_latents,
                    uncond_indicator,
                    unconditioning_latents,
                )

            # VAE decode latent
            video = self._normalize_and_decode_latents(latents, is_video2world=True)

            torch.cuda.synchronize()
            e2e_toc = time.perf_counter()

        walltime_ms = (e2e_toc - e2e_tic) * 1000.0
        return self._finalize_generation(
            video,
            walltime_ms,
            num_inference_steps,
            batch_size,
            warmup,
            save_video,
        )

    def infer(
        self,
        prompt,
        negative_prompt,
        image_height,
        image_width,
        input_image=None,
        input_video=None,
        num_frames=1,
        fps=16,
        num_images_per_prompt=1,
        num_videos_per_prompt=1,
        sigma_conditioning=0.0001,
        warmup=False,
        save_output=True,
    ):
        """
        Run the diffusion pipeline.

        Args:
            prompt (str):
                The text prompt to guide image generation.
            negative_prompt (str):
                The prompt not to guide the image generation. Ignored when not using guidance (i.e., ignored if `guidance_scale` is
                less than `1`).
            input_image (image):
                Input image used to initialize the latents.
            input_video (video):
                Input video used to initialize the latents.
            image_height (int):
                Height (in pixels) of the image to be generated. Must be a multiple of 8.
            image_width (int):
                Width (in pixels) of the image to be generated. Must be a multiple of 8.
            num_frames (int):
                The number of frames in the generated video.
            fps (int):
                The frames per second of the generated video.
            num_images_per_prompt (int):
                The number of images to generate per prompt.
            num_videos_per_prompt (int):
                The number of videos to generate per prompt.
            sigma_conditioning (`float`, defaults to `0.0001`):
                The sigma value used for scaling conditioning latents. Ideally, it should not be changed or should be
                set to a small value close to zero.
            warmup (bool):
                Indicate if this is a warmup run.
            save_output (bool):
                Save the generated image or video (if applicable)
        """
        if self.pipeline_type.is_txt2img():
            return self.generate_image(
                prompt,
                negative_prompt,
                image_height,
                image_width,
                num_frames,
                num_images_per_prompt,
                save_output,
                warmup,
            )
        elif self.pipeline_type.is_video2world():
            return self.generate_video(
                prompt,
                negative_prompt,
                image_height,
                image_width,
                input_image,
                input_video,
                num_frames,
                fps,
                num_videos_per_prompt,
                sigma_conditioning,
                save_output,
                warmup,
            )
        else:
            raise ValueError(f"Invalid pipeline type: {self.pipeline_type}")

    def run(
        self,
        prompt,
        negative_prompt,
        height,
        width,
        batch_count,
        num_warmup_runs,
        use_cuda_graph,
        **kwargs,
    ):
        if self.low_vram and self.use_cuda_graph:
            print("[W] Using low_vram, use_cuda_graph will be disabled")
            self.use_cuda_graph = False
        num_warmup_runs = max(1, num_warmup_runs) if use_cuda_graph else num_warmup_runs
        if num_warmup_runs > 0:
            print("[I] Warming up ..")
            for _ in range(num_warmup_runs):
                self.infer(prompt, negative_prompt, height, width, warmup=True, **kwargs)

        outputs = []
        for _ in range(batch_count):
            print("[I] Running Cosmos pipeline")
            if self.nvtx_profile:
                cudart.cudaProfilerStart()
            output, _ = self.infer(prompt, negative_prompt, height, width, warmup=False, **kwargs)
            outputs.append(output)
            if self.nvtx_profile:
                cudart.cudaProfilerStop()

        return outputs
