# 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