# Copyright 2025 Qwen-Image Team and The HuggingFace Team. All rights reserved.
#
# 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.

"""
Text and VAE encoder blocks for QwenImage pipelines.
"""

import PIL
import torch
from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, Qwen2VLProcessor

from ...configuration_utils import FrozenDict
from ...guiders import ClassifierFreeGuidance
from ...image_processor import InpaintProcessor, VaeImageProcessor, is_valid_image, is_valid_image_imagelist
from ...models import AutoencoderKLQwenImage, QwenImageControlNetModel, QwenImageMultiControlNetModel
from ...pipelines.qwenimage.pipeline_qwenimage_edit import calculate_dimensions
from ...utils import logging
from ...utils.torch_utils import unwrap_module
from ..modular_pipeline import ModularPipelineBlocks, PipelineState
from ..modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
from .modular_pipeline import QwenImageModularPipeline
from .prompt_templates import (
    QWENIMAGE_EDIT_PLUS_IMG_TEMPLATE,
    QWENIMAGE_EDIT_PLUS_PROMPT_TEMPLATE,
    QWENIMAGE_EDIT_PLUS_PROMPT_TEMPLATE_START_IDX,
    QWENIMAGE_EDIT_PROMPT_TEMPLATE,
    QWENIMAGE_EDIT_PROMPT_TEMPLATE_START_IDX,
    QWENIMAGE_LAYERED_CAPTION_PROMPT_CN,
    QWENIMAGE_LAYERED_CAPTION_PROMPT_EN,
    QWENIMAGE_PROMPT_TEMPLATE,
    QWENIMAGE_PROMPT_TEMPLATE_START_IDX,
)


logger = logging.get_logger(__name__)


def _extract_masked_hidden(hidden_states: torch.Tensor, mask: torch.Tensor):
    bool_mask = mask.bool()
    valid_lengths = bool_mask.sum(dim=1)
    selected = hidden_states[bool_mask]
    split_result = torch.split(selected, valid_lengths.tolist(), dim=0)
    return split_result


def get_qwen_prompt_embeds(
    text_encoder,
    tokenizer,
    prompt: str | list[str] = None,
    prompt_template_encode: str = QWENIMAGE_PROMPT_TEMPLATE,
    prompt_template_encode_start_idx: int = QWENIMAGE_PROMPT_TEMPLATE_START_IDX,
    tokenizer_max_length: int = 1024,
    device: torch.device | None = None,
):
    prompt = [prompt] if isinstance(prompt, str) else prompt

    template = prompt_template_encode
    drop_idx = prompt_template_encode_start_idx
    txt = [template.format(e) for e in prompt]
    txt_tokens = tokenizer(
        txt, max_length=tokenizer_max_length + drop_idx, padding=True, truncation=True, return_tensors="pt"
    ).to(device)
    encoder_hidden_states = text_encoder(
        input_ids=txt_tokens.input_ids,
        attention_mask=txt_tokens.attention_mask,
        output_hidden_states=True,
    )
    hidden_states = encoder_hidden_states.hidden_states[-1]

    split_hidden_states = _extract_masked_hidden(hidden_states, txt_tokens.attention_mask)
    split_hidden_states = [e[drop_idx:] for e in split_hidden_states]
    attn_mask_list = [torch.ones(e.size(0), dtype=torch.long, device=e.device) for e in split_hidden_states]
    max_seq_len = max([e.size(0) for e in split_hidden_states])
    prompt_embeds = torch.stack(
        [torch.cat([u, u.new_zeros(max_seq_len - u.size(0), u.size(1))]) for u in split_hidden_states]
    )
    encoder_attention_mask = torch.stack(
        [torch.cat([u, u.new_zeros(max_seq_len - u.size(0))]) for u in attn_mask_list]
    )

    prompt_embeds = prompt_embeds.to(device=device)

    return prompt_embeds, encoder_attention_mask


def get_qwen_prompt_embeds_edit(
    text_encoder,
    processor,
    prompt: str | list[str] = None,
    image: torch.Tensor | None = None,
    prompt_template_encode: str = QWENIMAGE_EDIT_PROMPT_TEMPLATE,
    prompt_template_encode_start_idx: int = QWENIMAGE_EDIT_PROMPT_TEMPLATE_START_IDX,
    device: torch.device | None = None,
):
    prompt = [prompt] if isinstance(prompt, str) else prompt

    template = prompt_template_encode
    drop_idx = prompt_template_encode_start_idx
    txt = [template.format(e) for e in prompt]

    model_inputs = processor(
        text=txt,
        images=image,
        padding=True,
        return_tensors="pt",
    ).to(device)

    outputs = text_encoder(
        input_ids=model_inputs["input_ids"],
        attention_mask=model_inputs["attention_mask"],
        pixel_values=model_inputs.get("pixel_values"),
        image_grid_thw=model_inputs.get("image_grid_thw"),
        output_hidden_states=True,
    )

    hidden_states = outputs.hidden_states[-1]
    split_hidden_states = _extract_masked_hidden(hidden_states, model_inputs["attention_mask"])
    split_hidden_states = [e[drop_idx:] for e in split_hidden_states]
    attn_mask_list = [torch.ones(e.size(0), dtype=torch.long, device=e.device) for e in split_hidden_states]
    max_seq_len = max([e.size(0) for e in split_hidden_states])
    prompt_embeds = torch.stack(
        [torch.cat([u, u.new_zeros(max_seq_len - u.size(0), u.size(1))]) for u in split_hidden_states]
    )
    encoder_attention_mask = torch.stack(
        [torch.cat([u, u.new_zeros(max_seq_len - u.size(0))]) for u in attn_mask_list]
    )

    prompt_embeds = prompt_embeds.to(device=device)

    return prompt_embeds, encoder_attention_mask


def get_qwen_prompt_embeds_edit_plus(
    text_encoder,
    processor,
    prompt: str | list[str] = None,
    image: torch.Tensor | list[PIL.Image.Image, PIL.Image.Image] | None = None,
    prompt_template_encode: str = QWENIMAGE_EDIT_PLUS_PROMPT_TEMPLATE,
    img_template_encode: str = QWENIMAGE_EDIT_PLUS_IMG_TEMPLATE,
    prompt_template_encode_start_idx: int = QWENIMAGE_EDIT_PLUS_PROMPT_TEMPLATE_START_IDX,
    device: torch.device | None = None,
):
    prompt = [prompt] if isinstance(prompt, str) else prompt
    if isinstance(image, list):
        base_img_prompt = ""
        for i, img in enumerate(image):
            base_img_prompt += img_template_encode.format(i + 1)
    elif image is not None:
        base_img_prompt = img_template_encode.format(1)
    else:
        base_img_prompt = ""

    template = prompt_template_encode

    drop_idx = prompt_template_encode_start_idx
    txt = [template.format(base_img_prompt + e) for e in prompt]

    model_inputs = processor(
        text=txt,
        images=image,
        padding=True,
        return_tensors="pt",
    ).to(device)
    outputs = text_encoder(
        input_ids=model_inputs["input_ids"],
        attention_mask=model_inputs["attention_mask"],
        pixel_values=model_inputs.get("pixel_values"),
        image_grid_thw=model_inputs.get("image_grid_thw"),
        output_hidden_states=True,
    )

    hidden_states = outputs.hidden_states[-1]
    split_hidden_states = _extract_masked_hidden(hidden_states, model_inputs["attention_mask"])
    split_hidden_states = [e[drop_idx:] for e in split_hidden_states]
    attn_mask_list = [torch.ones(e.size(0), dtype=torch.long, device=e.device) for e in split_hidden_states]
    max_seq_len = max([e.size(0) for e in split_hidden_states])
    prompt_embeds = torch.stack(
        [torch.cat([u, u.new_zeros(max_seq_len - u.size(0), u.size(1))]) for u in split_hidden_states]
    )
    encoder_attention_mask = torch.stack(
        [torch.cat([u, u.new_zeros(max_seq_len - u.size(0))]) for u in attn_mask_list]
    )

    prompt_embeds = prompt_embeds.to(device=device)
    return prompt_embeds, encoder_attention_mask


# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents
def retrieve_latents(
    encoder_output: torch.Tensor, generator: torch.Generator | None = None, sample_mode: str = "sample"
):
    if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
        return encoder_output.latent_dist.sample(generator)
    elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
        return encoder_output.latent_dist.mode()
    elif hasattr(encoder_output, "latents"):
        return encoder_output.latents
    else:
        raise AttributeError("Could not access latents of provided encoder_output")


# Modified from diffusers.pipelines.qwenimage.pipeline_qwenimage.QwenImagePipeline._encode_vae_image
def encode_vae_image(
    image: torch.Tensor,
    vae: AutoencoderKLQwenImage,
    generator: torch.Generator,
    device: torch.device,
    dtype: torch.dtype,
    latent_channels: int = 16,
    sample_mode: str = "argmax",
):
    if not isinstance(image, torch.Tensor):
        raise ValueError(f"Expected image to be a tensor, got {type(image)}.")

    # preprocessed image should be a 4D tensor: batch_size, num_channels, height, width
    if image.dim() == 4:
        image = image.unsqueeze(2)
    elif image.dim() != 5:
        raise ValueError(f"Expected image dims 4 or 5, got {image.dim()}.")

    image = image.to(device=device, dtype=dtype)

    if isinstance(generator, list):
        image_latents = [
            retrieve_latents(vae.encode(image[i : i + 1]), generator=generator[i], sample_mode=sample_mode)
            for i in range(image.shape[0])
        ]
        image_latents = torch.cat(image_latents, dim=0)
    else:
        image_latents = retrieve_latents(vae.encode(image), generator=generator, sample_mode=sample_mode)
    latents_mean = (
        torch.tensor(vae.config.latents_mean)
        .view(1, latent_channels, 1, 1, 1)
        .to(image_latents.device, image_latents.dtype)
    )
    latents_std = (
        torch.tensor(vae.config.latents_std)
        .view(1, latent_channels, 1, 1, 1)
        .to(image_latents.device, image_latents.dtype)
    )
    image_latents = (image_latents - latents_mean) / latents_std

    return image_latents


# ====================
# 1. RESIZE
# ====================
# In QwenImage pipelines, resize is a separate step because the resized image is used in VL encoding and vae encoder blocks:
#
#   image (PIL.Image.Image)
#       │
#       ▼
#   resized_image ([PIL.Image.Image])
#       │
#       ├──► text_encoder ──► prompt_embeds, prompt_embeds_mask
#       │    (VL encoding needs the resized image for vision-language fusion)
#       │
#       └──► image_processor ──► processed_image (torch.Tensor, pixel space)
#                │
#                ▼
#            vae_encoder ──► image_latents (torch.Tensor, latent space)
#
# In most of our other pipelines, resizing is done as part of the image preprocessing step.
# ====================


# auto_docstring
class QwenImageEditResizeStep(ModularPipelineBlocks):
    """
    Image Resize step that resize the image to target area while maintaining the aspect ratio.

      Components:
          image_resize_processor (`VaeImageProcessor`)

      Inputs:
          image (`Image | list`):
              Reference image(s) for denoising. Can be a single image or list of images.

      Outputs:
          resized_image (`list`):
              The resized images
    """

    model_name = "qwenimage-edit"

    @property
    def description(self) -> str:
        return "Image Resize step that resize the image to target area while maintaining the aspect ratio."

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [
            ComponentSpec(
                "image_resize_processor",
                VaeImageProcessor,
                config=FrozenDict({"vae_scale_factor": 16}),
                default_creation_method="from_config",
            ),
        ]

    @property
    def inputs(self) -> list[InputParam]:
        return [InputParam.template("image")]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam(
                name="resized_image",
                type_hint=list[PIL.Image.Image],
                description="The resized images",
            ),
        ]

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState):
        block_state = self.get_block_state(state)

        images = block_state.image

        if not is_valid_image_imagelist(images):
            raise ValueError(f"Images must be image or list of images but are {type(images)}")

        if is_valid_image(images):
            images = [images]

        image_width, image_height = images[0].size
        calculated_width, calculated_height, _ = calculate_dimensions(1024 * 1024, image_width / image_height)

        resized_images = [
            components.image_resize_processor.resize(image, height=calculated_height, width=calculated_width)
            for image in images
        ]

        block_state.resized_image = resized_images
        self.set_block_state(state, block_state)
        return components, state


# auto_docstring
class QwenImageLayeredResizeStep(ModularPipelineBlocks):
    """
    Image Resize step that resize the image to a target area (defined by the resolution parameter from user) while
    maintaining the aspect ratio.

      Components:
          image_resize_processor (`VaeImageProcessor`)

      Inputs:
          image (`Image | list`):
              Reference image(s) for denoising. Can be a single image or list of images.
          resolution (`int`, *optional*, defaults to 640):
              The target area to resize the image to, can be 1024 or 640

      Outputs:
          resized_image (`list`):
              The resized images
    """

    model_name = "qwenimage-layered"

    @property
    def description(self) -> str:
        return "Image Resize step that resize the image to a target area (defined by the resolution parameter from user) while maintaining the aspect ratio."

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [
            ComponentSpec(
                "image_resize_processor",
                VaeImageProcessor,
                config=FrozenDict({"vae_scale_factor": 16}),
                default_creation_method="from_config",
            ),
        ]

    @property
    def inputs(self) -> list[InputParam]:
        return [
            InputParam.template("image"),
            InputParam(
                name="resolution",
                default=640,
                type_hint=int,
                description="The target area to resize the image to, can be 1024 or 640",
            ),
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam(
                name="resized_image",
                type_hint=list[PIL.Image.Image],
                description="The resized images",
            )
        ]

    @staticmethod
    def check_inputs(resolution: int):
        if resolution not in [1024, 640]:
            raise ValueError(f"Resolution must be 1024 or 640 but is {resolution}")

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState):
        block_state = self.get_block_state(state)

        self.check_inputs(resolution=block_state.resolution)

        images = block_state.image

        if not is_valid_image_imagelist(images):
            raise ValueError(f"Images must be image or list of images but are {type(images)}")

        if is_valid_image(images):
            images = [images]

        image_width, image_height = images[0].size
        target_area = block_state.resolution * block_state.resolution
        calculated_width, calculated_height, _ = calculate_dimensions(target_area, image_width / image_height)

        resized_images = [
            components.image_resize_processor.resize(image, height=calculated_height, width=calculated_width)
            for image in images
        ]

        block_state.resized_image = resized_images
        self.set_block_state(state, block_state)
        return components, state


# auto_docstring
class QwenImageEditPlusResizeStep(ModularPipelineBlocks):
    """
    Resize images for QwenImage Edit Plus pipeline.
      Produces two outputs: resized_image (1024x1024) for VAE encoding, resized_cond_image (384x384) for VL text
      encoding. Each image is resized independently based on its own aspect ratio.

      Components:
          image_resize_processor (`VaeImageProcessor`)

      Inputs:
          image (`Image | list`):
              Reference image(s) for denoising. Can be a single image or list of images.

      Outputs:
          resized_image (`list`):
              Images resized to 1024x1024 target area for VAE encoding
          resized_cond_image (`list`):
              Images resized to 384x384 target area for VL text encoding
    """

    model_name = "qwenimage-edit-plus"

    @property
    def description(self) -> str:
        return (
            "Resize images for QwenImage Edit Plus pipeline.\n"
            "Produces two outputs: resized_image (1024x1024) for VAE encoding, "
            "resized_cond_image (384x384) for VL text encoding.\n"
            "Each image is resized independently based on its own aspect ratio."
        )

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [
            ComponentSpec(
                "image_resize_processor",
                VaeImageProcessor,
                config=FrozenDict({"vae_scale_factor": 16}),
                default_creation_method="from_config",
            ),
        ]

    @property
    def inputs(self) -> list[InputParam]:
        # image
        return [InputParam.template("image")]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam(
                name="resized_image",
                type_hint=list[PIL.Image.Image],
                description="Images resized to 1024x1024 target area for VAE encoding",
            ),
            OutputParam(
                name="resized_cond_image",
                type_hint=list[PIL.Image.Image],
                description="Images resized to 384x384 target area for VL text encoding",
            ),
        ]

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState):
        block_state = self.get_block_state(state)

        images = block_state.image

        if not is_valid_image_imagelist(images):
            raise ValueError(f"Images must be image or list of images but are {type(images)}")

        if is_valid_image(images):
            images = [images]

        # Resize each image independently based on its own aspect ratio
        resized_images = []
        resized_cond_images = []
        for image in images:
            image_width, image_height = image.size

            # For VAE encoder (1024x1024 target area)
            vae_width, vae_height, _ = calculate_dimensions(1024 * 1024, image_width / image_height)
            resized_images.append(components.image_resize_processor.resize(image, height=vae_height, width=vae_width))

            # For VL text encoder (384x384 target area)
            vl_width, vl_height, _ = calculate_dimensions(384 * 384, image_width / image_height)
            resized_cond_images.append(
                components.image_resize_processor.resize(image, height=vl_height, width=vl_width)
            )

        block_state.resized_image = resized_images
        block_state.resized_cond_image = resized_cond_images
        self.set_block_state(state, block_state)
        return components, state


# ====================
# 2. GET IMAGE PROMPT
# ====================


# auto_docstring
class QwenImageLayeredGetImagePromptStep(ModularPipelineBlocks):
    """
    Auto-caption step that generates a text prompt from the input image if none is provided.
      Uses the VL model (text_encoder) to generate a description of the image. If prompt is already provided, this step
      passes through unchanged.

      Components:
          text_encoder (`Qwen2_5_VLForConditionalGeneration`) processor (`Qwen2VLProcessor`)

      Inputs:
          prompt (`str`, *optional*):
              The prompt or prompts to guide image generation.
          resized_image (`Image`):
              The image to generate caption from, should be resized use the resize step
          use_en_prompt (`bool`, *optional*, defaults to False):
              Whether to use English prompt template

      Outputs:
          prompt (`str`):
              The prompt or prompts to guide image generation. If not provided, updated using image caption
    """

    model_name = "qwenimage-layered"

    def __init__(self):
        self.image_caption_prompt_en = QWENIMAGE_LAYERED_CAPTION_PROMPT_EN
        self.image_caption_prompt_cn = QWENIMAGE_LAYERED_CAPTION_PROMPT_CN
        super().__init__()

    @property
    def description(self) -> str:
        return (
            "Auto-caption step that generates a text prompt from the input image if none is provided.\n"
            "Uses the VL model (text_encoder) to generate a description of the image.\n"
            "If prompt is already provided, this step passes through unchanged."
        )

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [
            ComponentSpec("text_encoder", Qwen2_5_VLForConditionalGeneration),
            ComponentSpec("processor", Qwen2VLProcessor),
        ]

    @property
    def inputs(self) -> list[InputParam]:
        return [
            InputParam.template(
                "prompt", required=False
            ),  # it is not required for qwenimage-layered, unlike other pipelines
            InputParam(
                name="resized_image",
                required=True,
                type_hint=PIL.Image.Image,
                description="The image to generate caption from, should be resized use the resize step",
            ),
            InputParam(
                name="use_en_prompt",
                default=False,
                type_hint=bool,
                description="Whether to use English prompt template",
            ),
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam(
                name="prompt",
                type_hint=str,
                description="The prompt or prompts to guide image generation. If not provided, updated using image caption",
            ),
        ]

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState) -> PipelineState:
        block_state = self.get_block_state(state)

        device = components._execution_device

        # If prompt is empty or None, generate caption from image
        if block_state.prompt is None or block_state.prompt == "" or block_state.prompt == " ":
            if block_state.use_en_prompt:
                caption_prompt = self.image_caption_prompt_en
            else:
                caption_prompt = self.image_caption_prompt_cn

            model_inputs = components.processor(
                text=caption_prompt,
                images=block_state.resized_image,
                padding=True,
                return_tensors="pt",
            ).to(device)

            generated_ids = components.text_encoder.generate(**model_inputs, max_new_tokens=512)
            generated_ids_trimmed = [
                out_ids[len(in_ids) :] for in_ids, out_ids in zip(model_inputs.input_ids, generated_ids)
            ]
            output_text = components.processor.batch_decode(
                generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
            )[0]

            block_state.prompt = output_text.strip()

        self.set_block_state(state, block_state)
        return components, state


# ====================
# 3. TEXT ENCODER
# ====================


# auto_docstring
class QwenImageTextEncoderStep(ModularPipelineBlocks):
    """
    Text Encoder step that generates text embeddings to guide the image generation.

      Components:
          text_encoder (`Qwen2_5_VLForConditionalGeneration`): The text encoder to use tokenizer (`Qwen2Tokenizer`):
          The tokenizer to use guider (`ClassifierFreeGuidance`)

      Inputs:
          prompt (`str`):
              The prompt or prompts to guide image generation.
          negative_prompt (`str`, *optional*):
              The prompt or prompts not to guide the image generation.
          max_sequence_length (`int`, *optional*, defaults to 1024):
              Maximum sequence length for prompt encoding.

      Outputs:
          prompt_embeds (`Tensor`):
              The prompt embeddings.
          prompt_embeds_mask (`Tensor`):
              The encoder attention mask.
          negative_prompt_embeds (`Tensor`):
              The negative prompt embeddings.
          negative_prompt_embeds_mask (`Tensor`):
              The negative prompt embeddings mask.
    """

    model_name = "qwenimage"

    def __init__(self):
        self.prompt_template_encode = QWENIMAGE_PROMPT_TEMPLATE
        self.prompt_template_encode_start_idx = QWENIMAGE_PROMPT_TEMPLATE_START_IDX
        self.tokenizer_max_length = 1024
        super().__init__()

    @property
    def description(self) -> str:
        return "Text Encoder step that generates text embeddings to guide the image generation."

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [
            ComponentSpec("text_encoder", Qwen2_5_VLForConditionalGeneration, description="The text encoder to use"),
            ComponentSpec("tokenizer", Qwen2Tokenizer, description="The tokenizer to use"),
            ComponentSpec(
                "guider",
                ClassifierFreeGuidance,
                config=FrozenDict({"guidance_scale": 4.0}),
                default_creation_method="from_config",
            ),
        ]

    @property
    def inputs(self) -> list[InputParam]:
        return [
            InputParam.template("prompt"),
            InputParam.template("negative_prompt"),
            InputParam.template("max_sequence_length", default=1024),
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam.template("prompt_embeds"),
            OutputParam.template("prompt_embeds_mask"),
            OutputParam.template("negative_prompt_embeds"),
            OutputParam.template("negative_prompt_embeds_mask"),
        ]

    @staticmethod
    def check_inputs(prompt, negative_prompt, max_sequence_length):
        if not isinstance(prompt, str) and not isinstance(prompt, list):
            raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")

        if (
            negative_prompt is not None
            and not isinstance(negative_prompt, str)
            and not isinstance(negative_prompt, list)
        ):
            raise ValueError(f"`negative_prompt` has to be of type `str` or `list` but is {type(negative_prompt)}")

        if max_sequence_length is not None and max_sequence_length > 1024:
            raise ValueError(f"`max_sequence_length` cannot be greater than 1024 but is {max_sequence_length}")

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState):
        block_state = self.get_block_state(state)

        device = components._execution_device
        self.check_inputs(block_state.prompt, block_state.negative_prompt, block_state.max_sequence_length)

        block_state.prompt_embeds, block_state.prompt_embeds_mask = get_qwen_prompt_embeds(
            components.text_encoder,
            components.tokenizer,
            prompt=block_state.prompt,
            prompt_template_encode=self.prompt_template_encode,
            prompt_template_encode_start_idx=self.prompt_template_encode_start_idx,
            tokenizer_max_length=self.tokenizer_max_length,
            device=device,
        )

        block_state.prompt_embeds = block_state.prompt_embeds[:, : block_state.max_sequence_length]
        block_state.prompt_embeds_mask = block_state.prompt_embeds_mask[:, : block_state.max_sequence_length]

        block_state.negative_prompt_embeds = None
        block_state.negative_prompt_embeds_mask = None
        if components.requires_unconditional_embeds:
            negative_prompt = block_state.negative_prompt or ""
            block_state.negative_prompt_embeds, block_state.negative_prompt_embeds_mask = get_qwen_prompt_embeds(
                components.text_encoder,
                components.tokenizer,
                prompt=negative_prompt,
                prompt_template_encode=self.prompt_template_encode,
                prompt_template_encode_start_idx=self.prompt_template_encode_start_idx,
                tokenizer_max_length=self.tokenizer_max_length,
                device=device,
            )
            block_state.negative_prompt_embeds = block_state.negative_prompt_embeds[
                :, : block_state.max_sequence_length
            ]
            block_state.negative_prompt_embeds_mask = block_state.negative_prompt_embeds_mask[
                :, : block_state.max_sequence_length
            ]

        self.set_block_state(state, block_state)
        return components, state


# auto_docstring
class QwenImageEditTextEncoderStep(ModularPipelineBlocks):
    """
    Text Encoder step that processes both prompt and image together to generate text embeddings for guiding image
    generation.

      Components:
          text_encoder (`Qwen2_5_VLForConditionalGeneration`) processor (`Qwen2VLProcessor`) guider
          (`ClassifierFreeGuidance`)

      Inputs:
          prompt (`str`):
              The prompt or prompts to guide image generation.
          negative_prompt (`str`, *optional*):
              The prompt or prompts not to guide the image generation.
          resized_image (`Image`):
              The image prompt to encode, should be resized using resize step

      Outputs:
          prompt_embeds (`Tensor`):
              The prompt embeddings.
          prompt_embeds_mask (`Tensor`):
              The encoder attention mask.
          negative_prompt_embeds (`Tensor`):
              The negative prompt embeddings.
          negative_prompt_embeds_mask (`Tensor`):
              The negative prompt embeddings mask.
    """

    model_name = "qwenimage"

    def __init__(self):
        self.prompt_template_encode = QWENIMAGE_EDIT_PROMPT_TEMPLATE
        self.prompt_template_encode_start_idx = QWENIMAGE_EDIT_PROMPT_TEMPLATE_START_IDX
        super().__init__()

    @property
    def description(self) -> str:
        return "Text Encoder step that processes both prompt and image together to generate text embeddings for guiding image generation."

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [
            ComponentSpec("text_encoder", Qwen2_5_VLForConditionalGeneration),
            ComponentSpec("processor", Qwen2VLProcessor),
            ComponentSpec(
                "guider",
                ClassifierFreeGuidance,
                config=FrozenDict({"guidance_scale": 4.0}),
                default_creation_method="from_config",
            ),
        ]

    @property
    def inputs(self) -> list[InputParam]:
        return [
            InputParam.template("prompt"),
            InputParam.template("negative_prompt"),
            InputParam(
                name="resized_image",
                required=True,
                type_hint=PIL.Image.Image,
                description="The image prompt to encode, should be resized using resize step",
            ),
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam.template("prompt_embeds"),
            OutputParam.template("prompt_embeds_mask"),
            OutputParam.template("negative_prompt_embeds"),
            OutputParam.template("negative_prompt_embeds_mask"),
        ]

    @staticmethod
    def check_inputs(prompt, negative_prompt):
        if not isinstance(prompt, str) and not isinstance(prompt, list):
            raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")

        if (
            negative_prompt is not None
            and not isinstance(negative_prompt, str)
            and not isinstance(negative_prompt, list)
        ):
            raise ValueError(f"`negative_prompt` has to be of type `str` or `list` but is {type(negative_prompt)}")

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState):
        block_state = self.get_block_state(state)

        self.check_inputs(block_state.prompt, block_state.negative_prompt)

        device = components._execution_device

        block_state.prompt_embeds, block_state.prompt_embeds_mask = get_qwen_prompt_embeds_edit(
            components.text_encoder,
            components.processor,
            prompt=block_state.prompt,
            image=block_state.resized_image,
            prompt_template_encode=self.prompt_template_encode,
            prompt_template_encode_start_idx=self.prompt_template_encode_start_idx,
            device=device,
        )

        block_state.negative_prompt_embeds = None
        block_state.negative_prompt_embeds_mask = None
        if components.requires_unconditional_embeds:
            negative_prompt = block_state.negative_prompt or " "
            block_state.negative_prompt_embeds, block_state.negative_prompt_embeds_mask = get_qwen_prompt_embeds_edit(
                components.text_encoder,
                components.processor,
                prompt=negative_prompt,
                image=block_state.resized_image,
                prompt_template_encode=self.prompt_template_encode,
                prompt_template_encode_start_idx=self.prompt_template_encode_start_idx,
                device=device,
            )

        self.set_block_state(state, block_state)
        return components, state


# auto_docstring
class QwenImageEditPlusTextEncoderStep(ModularPipelineBlocks):
    """
    Text Encoder step for QwenImage Edit Plus that processes prompt and multiple images together to generate text
    embeddings for guiding image generation.

      Components:
          text_encoder (`Qwen2_5_VLForConditionalGeneration`) processor (`Qwen2VLProcessor`) guider
          (`ClassifierFreeGuidance`)

      Inputs:
          prompt (`str`):
              The prompt or prompts to guide image generation.
          negative_prompt (`str`, *optional*):
              The prompt or prompts not to guide the image generation.
          resized_cond_image (`Tensor`):
              The image(s) to encode, can be a single image or list of images, should be resized to 384x384 using
              resize step

      Outputs:
          prompt_embeds (`Tensor`):
              The prompt embeddings.
          prompt_embeds_mask (`Tensor`):
              The encoder attention mask.
          negative_prompt_embeds (`Tensor`):
              The negative prompt embeddings.
          negative_prompt_embeds_mask (`Tensor`):
              The negative prompt embeddings mask.
    """

    model_name = "qwenimage-edit-plus"

    def __init__(self):
        self.prompt_template_encode = QWENIMAGE_EDIT_PLUS_PROMPT_TEMPLATE
        self.img_template_encode = QWENIMAGE_EDIT_PLUS_IMG_TEMPLATE
        self.prompt_template_encode_start_idx = QWENIMAGE_EDIT_PLUS_PROMPT_TEMPLATE_START_IDX
        super().__init__()

    @property
    def description(self) -> str:
        return (
            "Text Encoder step for QwenImage Edit Plus that processes prompt and multiple images together "
            "to generate text embeddings for guiding image generation."
        )

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [
            ComponentSpec("text_encoder", Qwen2_5_VLForConditionalGeneration),
            ComponentSpec("processor", Qwen2VLProcessor),
            ComponentSpec(
                "guider",
                ClassifierFreeGuidance,
                config=FrozenDict({"guidance_scale": 4.0}),
                default_creation_method="from_config",
            ),
        ]

    @property
    def inputs(self) -> list[InputParam]:
        return [
            InputParam.template("prompt"),
            InputParam.template("negative_prompt"),
            InputParam(
                name="resized_cond_image",
                required=True,
                type_hint=torch.Tensor,
                description="The image(s) to encode, can be a single image or list of images, should be resized to 384x384 using resize step",
            ),
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam.template("prompt_embeds"),
            OutputParam.template("prompt_embeds_mask"),
            OutputParam.template("negative_prompt_embeds"),
            OutputParam.template("negative_prompt_embeds_mask"),
        ]

    @staticmethod
    def check_inputs(prompt, negative_prompt):
        if not isinstance(prompt, str) and not isinstance(prompt, list):
            raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")

        if (
            negative_prompt is not None
            and not isinstance(negative_prompt, str)
            and not isinstance(negative_prompt, list)
        ):
            raise ValueError(f"`negative_prompt` has to be of type `str` or `list` but is {type(negative_prompt)}")

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState):
        block_state = self.get_block_state(state)

        self.check_inputs(block_state.prompt, block_state.negative_prompt)

        device = components._execution_device

        block_state.prompt_embeds, block_state.prompt_embeds_mask = get_qwen_prompt_embeds_edit_plus(
            components.text_encoder,
            components.processor,
            prompt=block_state.prompt,
            image=block_state.resized_cond_image,
            prompt_template_encode=self.prompt_template_encode,
            img_template_encode=self.img_template_encode,
            prompt_template_encode_start_idx=self.prompt_template_encode_start_idx,
            device=device,
        )

        block_state.negative_prompt_embeds = None
        block_state.negative_prompt_embeds_mask = None
        if components.requires_unconditional_embeds:
            negative_prompt = block_state.negative_prompt or " "
            block_state.negative_prompt_embeds, block_state.negative_prompt_embeds_mask = (
                get_qwen_prompt_embeds_edit_plus(
                    components.text_encoder,
                    components.processor,
                    prompt=negative_prompt,
                    image=block_state.resized_cond_image,
                    prompt_template_encode=self.prompt_template_encode,
                    img_template_encode=self.img_template_encode,
                    prompt_template_encode_start_idx=self.prompt_template_encode_start_idx,
                    device=device,
                )
            )

        self.set_block_state(state, block_state)
        return components, state


# ====================
# 4. IMAGE PREPROCESS
# ====================


# auto_docstring
class QwenImageInpaintProcessImagesInputStep(ModularPipelineBlocks):
    """
    Image Preprocess step for inpainting task. This processes the image and mask inputs together. Images will be
    resized to the given height and width.

      Components:
          image_mask_processor (`InpaintProcessor`)

      Inputs:
          mask_image (`Image`):
              Mask image for inpainting.
          image (`Image | list`):
              Reference image(s) for denoising. Can be a single image or list of images.
          height (`int`, *optional*):
              The height in pixels of the generated image.
          width (`int`, *optional*):
              The width in pixels of the generated image.
          padding_mask_crop (`int`, *optional*):
              Padding for mask cropping in inpainting.

      Outputs:
          processed_image (`Tensor`):
              The processed image
          processed_mask_image (`Tensor`):
              The processed mask image
          mask_overlay_kwargs (`dict`):
              The kwargs for the postprocess step to apply the mask overlay
    """

    model_name = "qwenimage"

    @property
    def description(self) -> str:
        return "Image Preprocess step for inpainting task. This processes the image and mask inputs together. Images will be resized to the given height and width."

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [
            ComponentSpec(
                "image_mask_processor",
                InpaintProcessor,
                config=FrozenDict({"vae_scale_factor": 16}),
                default_creation_method="from_config",
            ),
        ]

    @property
    def inputs(self) -> list[InputParam]:
        return [
            InputParam.template("mask_image"),
            InputParam.template("image"),
            InputParam.template("height"),
            InputParam.template("width"),
            InputParam.template("padding_mask_crop"),
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam(
                name="processed_image",
                type_hint=torch.Tensor,
                description="The processed image",
            ),
            OutputParam(
                name="processed_mask_image",
                type_hint=torch.Tensor,
                description="The processed mask image",
            ),
            OutputParam(
                name="mask_overlay_kwargs",
                type_hint=dict,
                description="The kwargs for the postprocess step to apply the mask overlay",
            ),
        ]

    @staticmethod
    def check_inputs(height, width, vae_scale_factor):
        if height is not None and height % (vae_scale_factor * 2) != 0:
            raise ValueError(f"Height must be divisible by {vae_scale_factor * 2} but is {height}")

        if width is not None and width % (vae_scale_factor * 2) != 0:
            raise ValueError(f"Width must be divisible by {vae_scale_factor * 2} but is {width}")

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState):
        block_state = self.get_block_state(state)

        self.check_inputs(
            height=block_state.height, width=block_state.width, vae_scale_factor=components.vae_scale_factor
        )
        height = block_state.height or components.default_height
        width = block_state.width or components.default_width

        block_state.processed_image, block_state.processed_mask_image, block_state.mask_overlay_kwargs = (
            components.image_mask_processor.preprocess(
                image=block_state.image,
                mask=block_state.mask_image,
                height=height,
                width=width,
                padding_mask_crop=block_state.padding_mask_crop,
            )
        )

        self.set_block_state(state, block_state)
        return components, state


# auto_docstring
class QwenImageEditInpaintProcessImagesInputStep(ModularPipelineBlocks):
    """
    Image Preprocess step for inpainting task. This processes the image and mask inputs together. Images should be
    resized first.

      Components:
          image_mask_processor (`InpaintProcessor`)

      Inputs:
          mask_image (`Image`):
              Mask image for inpainting.
          resized_image (`Image`):
              The resized image. should be generated using a resize step
          padding_mask_crop (`int`, *optional*):
              Padding for mask cropping in inpainting.

      Outputs:
          processed_image (`Tensor`):
              The processed image
          processed_mask_image (`Tensor`):
              The processed mask image
          mask_overlay_kwargs (`dict`):
              The kwargs for the postprocess step to apply the mask overlay
    """

    model_name = "qwenimage-edit"

    @property
    def description(self) -> str:
        return "Image Preprocess step for inpainting task. This processes the image and mask inputs together. Images should be resized first."

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [
            ComponentSpec(
                "image_mask_processor",
                InpaintProcessor,
                config=FrozenDict({"vae_scale_factor": 16}),
                default_creation_method="from_config",
            ),
        ]

    @property
    def inputs(self) -> list[InputParam]:
        return [
            InputParam.template("mask_image"),
            InputParam(
                name="resized_image",
                required=True,
                type_hint=PIL.Image.Image,
                description="The resized image. should be generated using a resize step",
            ),
            InputParam.template("padding_mask_crop"),
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam(name="processed_image", type_hint=torch.Tensor, description="The processed image"),
            OutputParam(
                name="processed_mask_image",
                type_hint=torch.Tensor,
                description="The processed mask image",
            ),
            OutputParam(
                name="mask_overlay_kwargs",
                type_hint=dict,
                description="The kwargs for the postprocess step to apply the mask overlay",
            ),
        ]

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState):
        block_state = self.get_block_state(state)

        width, height = block_state.resized_image[0].size

        block_state.processed_image, block_state.processed_mask_image, block_state.mask_overlay_kwargs = (
            components.image_mask_processor.preprocess(
                image=block_state.resized_image,
                mask=block_state.mask_image,
                height=height,
                width=width,
                padding_mask_crop=block_state.padding_mask_crop,
            )
        )

        self.set_block_state(state, block_state)
        return components, state


# auto_docstring
class QwenImageProcessImagesInputStep(ModularPipelineBlocks):
    """
    Image Preprocess step. will resize the image to the given height and width.

      Components:
          image_processor (`VaeImageProcessor`)

      Inputs:
          image (`Image | list`):
              Reference image(s) for denoising. Can be a single image or list of images.
          height (`int`, *optional*):
              The height in pixels of the generated image.
          width (`int`, *optional*):
              The width in pixels of the generated image.

      Outputs:
          processed_image (`Tensor`):
              The processed image
    """

    model_name = "qwenimage"

    @property
    def description(self) -> str:
        return "Image Preprocess step. will resize the image to the given height and width."

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [
            ComponentSpec(
                "image_processor",
                VaeImageProcessor,
                config=FrozenDict({"vae_scale_factor": 16}),
                default_creation_method="from_config",
            ),
        ]

    @property
    def inputs(self) -> list[InputParam]:
        return [
            InputParam.template("image"),
            InputParam.template("height"),
            InputParam.template("width"),
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam(
                name="processed_image",
                type_hint=torch.Tensor,
                description="The processed image",
            )
        ]

    @staticmethod
    def check_inputs(height, width, vae_scale_factor):
        if height is not None and height % (vae_scale_factor * 2) != 0:
            raise ValueError(f"Height must be divisible by {vae_scale_factor * 2} but is {height}")

        if width is not None and width % (vae_scale_factor * 2) != 0:
            raise ValueError(f"Width must be divisible by {vae_scale_factor * 2} but is {width}")

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState):
        block_state = self.get_block_state(state)

        self.check_inputs(
            height=block_state.height, width=block_state.width, vae_scale_factor=components.vae_scale_factor
        )
        height = block_state.height or components.default_height
        width = block_state.width or components.default_width

        block_state.processed_image = components.image_processor.preprocess(
            image=block_state.image,
            height=height,
            width=width,
        )

        self.set_block_state(state, block_state)
        return components, state


# auto_docstring
class QwenImageEditProcessImagesInputStep(ModularPipelineBlocks):
    """
    Image Preprocess step. Images needs to be resized first.

      Components:
          image_processor (`VaeImageProcessor`)

      Inputs:
          resized_image (`list`):
              The resized image. should be generated using a resize step

      Outputs:
          processed_image (`Tensor`):
              The processed image
    """

    model_name = "qwenimage-edit"

    @property
    def description(self) -> str:
        return "Image Preprocess step. Images needs to be resized first."

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [
            ComponentSpec(
                "image_processor",
                VaeImageProcessor,
                config=FrozenDict({"vae_scale_factor": 16}),
                default_creation_method="from_config",
            ),
        ]

    @property
    def inputs(self) -> list[InputParam]:
        return [
            InputParam(
                name="resized_image",
                required=True,
                type_hint=list[PIL.Image.Image],
                description="The resized image. should be generated using a resize step",
            ),
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam(
                name="processed_image",
                type_hint=torch.Tensor,
                description="The processed image",
            )
        ]

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState):
        block_state = self.get_block_state(state)

        width, height = block_state.resized_image[0].size

        block_state.processed_image = components.image_processor.preprocess(
            image=block_state.resized_image,
            height=height,
            width=width,
        )

        self.set_block_state(state, block_state)
        return components, state


# auto_docstring
class QwenImageEditPlusProcessImagesInputStep(ModularPipelineBlocks):
    """
    Image Preprocess step. Images can be resized first. If a list of images is provided, will return a list of
    processed images.

      Components:
          image_processor (`VaeImageProcessor`)

      Inputs:
          resized_image (`list`):
              The resized image. should be generated using a resize step

      Outputs:
          processed_image (`Tensor`):
              The processed image
    """

    model_name = "qwenimage-edit-plus"

    @property
    def description(self) -> str:
        return "Image Preprocess step. Images can be resized first. If a list of images is provided, will return a list of processed images."

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [
            ComponentSpec(
                "image_processor",
                VaeImageProcessor,
                config=FrozenDict({"vae_scale_factor": 16}),
                default_creation_method="from_config",
            ),
        ]

    @property
    def inputs(self) -> list[InputParam]:
        return [
            InputParam(
                name="resized_image",
                required=True,
                type_hint=list[PIL.Image.Image],
                description="The resized image. should be generated using a resize step",
            )
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam(
                name="processed_image",
                type_hint=torch.Tensor,
                description="The processed image",
            )
        ]

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState):
        block_state = self.get_block_state(state)

        image = block_state.resized_image

        is_image_list = isinstance(image, list)
        if not is_image_list:
            image = [image]

        processed_images = []
        for img in image:
            img_width, img_height = img.size
            processed_images.append(
                components.image_processor.preprocess(image=img, height=img_height, width=img_width)
            )

        if is_image_list:
            block_state.processed_image = processed_images
        else:
            block_state.processed_image = processed_images[0]

        self.set_block_state(state, block_state)
        return components, state


# ====================
# 5. VAE ENCODER
# ====================


# auto_docstring
class QwenImageVaeEncoderStep(ModularPipelineBlocks):
    """
    VAE Encoder step that converts processed_image into latent representations image_latents.
      Handles both single images and lists of images with varied resolutions.

      Components:
          vae (`AutoencoderKLQwenImage`)

      Inputs:
          processed_image (`Tensor`):
              The image tensor to encode
          generator (`Generator`, *optional*):
              Torch generator for deterministic generation.

      Outputs:
          image_latents (`Tensor`):
              The latent representation of the input image.
    """

    model_name = "qwenimage"

    def __init__(self, input: InputParam | None = None, output: OutputParam | None = None):
        """Initialize a VAE encoder step for converting images to latent representations.

        Handles both single images and lists of images. When input is a list, outputs a list of latents. When input is
        a single tensor, outputs a single latent tensor.

        Args:
            input (InputParam, optional): Input parameter for the processed image. Defaults to "processed_image".
            output (OutputParam, optional): Output parameter for the image latents. Defaults to "image_latents".
        """
        if input is None:
            input = InputParam(
                name="processed_image", required=True, type_hint=torch.Tensor, description="The image tensor to encode"
            )

        if output is None:
            output = OutputParam.template("image_latents")

        if not isinstance(input, InputParam):
            raise ValueError(f"input must be InputParam but is {type(input)}")
        if not isinstance(output, OutputParam):
            raise ValueError(f"output must be OutputParam but is {type(output)}")

        self._input = input
        self._output = output
        self._image_input_name = input.name
        self._image_latents_output_name = output.name
        super().__init__()

    @property
    def description(self) -> str:
        return (
            f"VAE Encoder step that converts {self._image_input_name} into latent representations {self._image_latents_output_name}.\n"
            "Handles both single images and lists of images with varied resolutions."
        )

    @property
    def expected_components(self) -> list[ComponentSpec]:
        return [ComponentSpec("vae", AutoencoderKLQwenImage)]

    @property
    def inputs(self) -> list[InputParam]:
        return [
            self._input,  # default is "processed_image"
            InputParam.template("generator"),
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [self._output]  # default is "image_latents"

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState) -> PipelineState:
        block_state = self.get_block_state(state)

        device = components._execution_device
        dtype = components.vae.dtype

        image = getattr(block_state, self._image_input_name)
        is_image_list = isinstance(image, list)
        if not is_image_list:
            image = [image]

        # Handle both single image and list of images
        image_latents = []
        for img in image:
            image_latents.append(
                encode_vae_image(
                    image=img,
                    vae=components.vae,
                    generator=block_state.generator,
                    device=device,
                    dtype=dtype,
                    latent_channels=components.num_channels_latents,
                )
            )
        if not is_image_list:
            image_latents = image_latents[0]

        setattr(block_state, self._image_latents_output_name, image_latents)

        self.set_block_state(state, block_state)

        return components, state


# auto_docstring
class QwenImageControlNetVaeEncoderStep(ModularPipelineBlocks):
    """
    VAE Encoder step that converts `control_image` into latent representations control_image_latents.

      Components:
          vae (`AutoencoderKLQwenImage`) controlnet (`QwenImageControlNetModel`) control_image_processor
          (`VaeImageProcessor`)

      Inputs:
          control_image (`Image`):
              Control image for ControlNet conditioning.
          height (`int`, *optional*):
              The height in pixels of the generated image.
          width (`int`, *optional*):
              The width in pixels of the generated image.
          generator (`Generator`, *optional*):
              Torch generator for deterministic generation.

      Outputs:
          control_image_latents (`Tensor`):
              The latents representing the control image
    """

    model_name = "qwenimage"

    @property
    def description(self) -> str:
        return "VAE Encoder step that converts `control_image` into latent representations control_image_latents.\n"

    @property
    def expected_components(self) -> list[ComponentSpec]:
        components = [
            ComponentSpec("vae", AutoencoderKLQwenImage),
            ComponentSpec("controlnet", QwenImageControlNetModel),
            ComponentSpec(
                "control_image_processor",
                VaeImageProcessor,
                config=FrozenDict({"vae_scale_factor": 16}),
                default_creation_method="from_config",
            ),
        ]
        return components

    @property
    def inputs(self) -> list[InputParam]:
        inputs = [
            InputParam.template("control_image"),
            InputParam.template("height"),
            InputParam.template("width"),
            InputParam.template("generator"),
        ]
        return inputs

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam(
                "control_image_latents",
                type_hint=torch.Tensor,
                description="The latents representing the control image",
            )
        ]

    @staticmethod
    def check_inputs(height, width, vae_scale_factor):
        if height is not None and height % (vae_scale_factor * 2) != 0:
            raise ValueError(f"Height must be divisible by {vae_scale_factor * 2} but is {height}")

        if width is not None and width % (vae_scale_factor * 2) != 0:
            raise ValueError(f"Width must be divisible by {vae_scale_factor * 2} but is {width}")

    @torch.no_grad()
    def __call__(self, components: QwenImageModularPipeline, state: PipelineState) -> PipelineState:
        block_state = self.get_block_state(state)

        self.check_inputs(block_state.height, block_state.width, components.vae_scale_factor)

        device = components._execution_device
        dtype = components.vae.dtype

        height = block_state.height or components.default_height
        width = block_state.width or components.default_width

        controlnet = unwrap_module(components.controlnet)
        if isinstance(controlnet, QwenImageMultiControlNetModel) and not isinstance(block_state.control_image, list):
            block_state.control_image = [block_state.control_image]

        if isinstance(controlnet, QwenImageMultiControlNetModel):
            block_state.control_image_latents = []
            for control_image_ in block_state.control_image:
                control_image_ = components.control_image_processor.preprocess(
                    image=control_image_,
                    height=height,
                    width=width,
                )

                control_image_latents_ = encode_vae_image(
                    image=control_image_,
                    vae=components.vae,
                    generator=block_state.generator,
                    device=device,
                    dtype=dtype,
                    latent_channels=components.num_channels_latents,
                    sample_mode="sample",
                )
                block_state.control_image_latents.append(control_image_latents_)

        elif isinstance(controlnet, QwenImageControlNetModel):
            control_image = components.control_image_processor.preprocess(
                image=block_state.control_image,
                height=height,
                width=width,
            )
            block_state.control_image_latents = encode_vae_image(
                image=control_image,
                vae=components.vae,
                generator=block_state.generator,
                device=device,
                dtype=dtype,
                latent_channels=components.num_channels_latents,
                sample_mode="sample",
            )

        else:
            raise ValueError(
                f"Expected controlnet to be a QwenImageControlNetModel or QwenImageMultiControlNetModel, got {type(controlnet)}"
            )

        self.set_block_state(state, block_state)

        return components, state


# ====================
# 6. PERMUTE LATENTS
# ====================


# auto_docstring
class QwenImageLayeredPermuteLatentsStep(ModularPipelineBlocks):
    """
    Permute image latents from (B, C, 1, H, W) to (B, 1, C, H, W) for Layered packing.

      Inputs:
          image_latents (`Tensor`):
              image latents used to guide the image generation. Can be generated from vae_encoder step.

      Outputs:
          image_latents (`Tensor`):
              The latent representation of the input image. (permuted from [B, C, 1, H, W] to [B, 1, C, H, W])
    """

    model_name = "qwenimage-layered"

    @property
    def description(self) -> str:
        return "Permute image latents from (B, C, 1, H, W) to (B, 1, C, H, W) for Layered packing."

    @property
    def inputs(self) -> list[InputParam]:
        return [
            InputParam.template("image_latents"),
        ]

    @property
    def intermediate_outputs(self) -> list[OutputParam]:
        return [
            OutputParam.template("image_latents", note="permuted from [B, C, 1, H, W] to [B, 1, C, H, W]"),
        ]

    @torch.no_grad()
    def __call__(self, components, state: PipelineState) -> PipelineState:
        block_state = self.get_block_state(state)

        # Permute: (B, C, 1, H, W) -> (B, 1, C, H, W)
        latents = block_state.image_latents
        block_state.image_latents = latents.permute(0, 2, 1, 3, 4)

        self.set_block_state(state, block_state)
        return components, state
