kr-cen commited on
Commit
3ac5228
·
verified ·
1 Parent(s): b58c8e2

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +97 -0
  2. model_index.json +28 -0
  3. modeling_qwen_image.py +628 -0
README.md ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ base_model:
4
+ - Qwen-Image-Edit
5
+ pipeline_tag: any-to-any
6
+ ---
7
+
8
+ # 🔥 Qwen-Image-MICo • Qwen-Image-Edit-Variant finetuned on Multi-Image Composition Dataset (MICo-150K)
9
+
10
+ [![Paper](https://img.shields.io/badge/paper-A42C25?style=for-the-badge&logo=arxiv&logoColor=white)](https://www.arxiv.org/pdf/2512.07348)
11
+ [![ArXiv](https://img.shields.io/badge/arXiv-A42C25?style=for-the-badge&logo=arxiv&logoColor=white&color=blue)](https://www.arxiv.org/abs/2512.07348)
12
+ [![Github](https://img.shields.io/badge/MICo150K-000000?style=for-the-badge&logo=github&logoColor=000&logoColor=white)](https://github.com/A113N-W3I/MICo-150K)
13
+ [![Project Page](https://img.shields.io/badge/Project_Page-00CED1?style=for-the-badge&logo=web&logoColor=white)](https://mico-150k.github.io/)
14
+
15
+
16
+ <!-- <p align="left">
17
+ <a href="https://mico-150k.github.io/">
18
+ <img
19
+ src="https://img.shields.io/badge/MICo-Website-0A66C2?logo=safari&logoColor=white" style="display: inline-block; vertical-align: middle;"
20
+ alt="MICo Website"
21
+ />
22
+ </a>
23
+ <a href="https://www.arxiv.org/abs/2512.07348">
24
+ <img
25
+ src="https://img.shields.io/badge/MICo-Paper-red?logo=arxiv&logoColor=red" style="display: inline-block; vertical-align: middle;"
26
+ alt="MICo Paper on arXiv"
27
+ />
28
+ </a>
29
+ <a href="https://github.com/A113N-W3I/MICo-150K" target="_blank" style="margin: 2px;">
30
+ <img
31
+ alt="Github" src="https://img.shields.io/badge/MICo-Bench-536af5?color=536af5&logo=github" style="display: inline-block; vertical-align: middle;"
32
+ alt="MICo Bench"
33
+ />
34
+ </a>
35
+
36
+ </p> -->
37
+
38
+ ## 🎮 Demo
39
+
40
+ 🔥🔥🔥 Please visit our [huggingface space](https://huggingface.co/spaces/kr-cen/Qwen-Image-MICo), the model is implemented on huggingface ZeroGPU.
41
+
42
+ ## 🎨 Gallery
43
+
44
+ ![image](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/iLg061DXAoF06rdjbvtUz.png)
45
+
46
+ ![image](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/UOUzNBr1DYV06P-3Vhh61.png)
47
+
48
+ ![image](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/d2CUsbgGowEpu9TakEay4.png)
49
+
50
+ ![image](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/yebFWqvwSk8B53QBOq5NY.png)
51
+
52
+ ![image](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/eQExPUwbIpTqUsC3lH6U1.png)
53
+
54
+ ![image](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/c-qzX3-iUjfjXt0PVT4fD.png)
55
+
56
+ ![image](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/SRZe_670S-W3Si_80ZWrN.png)
57
+
58
+ ![image](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/8UZy2mc6Rb-ld5NWAK4Pp.png)
59
+
60
+ ![image](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/aEKO82a-0AYLegLVmgnkO.png)
61
+
62
+ ## 🌋 Emergent Capabilities
63
+
64
+ Hint: The following examples are editing types that are NOT present in our MICo-150K dataset, but these editing capabilities emerged in Qwen-Image-MICo during training.
65
+
66
+ Thus, this capability may be unstable, and the following examples are for reference only.
67
+
68
+ ![1-1](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/hnqvHE-HCXZXcRV-5PaKb.png)
69
+
70
+ ![1-2](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/XQZtDY9OlcXNxYJicmVGw.png)
71
+
72
+ ![1-3](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/7sMrQYCH1LrDzphLEEBGt.png)
73
+
74
+ ![1-4](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/Vep391HA0rZgjDBm1zJuW.png)
75
+
76
+ ![1-5](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/1MHNw6lu0QrjRFNf0-xVy.png)
77
+
78
+ ![1-6](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/PDXQD15iY5PcXSQ1IEP_K.png)
79
+
80
+ ![1-7](https://cdn-uploads.huggingface.co/production/uploads/655db6a58c2d4379a70837c0/eeGAwDkYNy4dkF01rdjUQ.png)
81
+
82
+ ## ✍️ Citation
83
+
84
+ If you find this work useful, please cite:
85
+
86
+ ~~~
87
+ @article{wei2025mico,
88
+ title={MICo-150K: A Comprehensive Dataset Advancing Multi-Image Composition},
89
+ author={Wei, Xinyu and Cen, Kangrui and Wei, Hongyang and Guo, Zhen and Li, Bairui and Wang, Zeqing and Zhang, Jinrui and Zhang, Lei},
90
+ journal={arXiv preprint arXiv:2512.07348},
91
+ year={2025}
92
+ }
93
+ ~~~
94
+
95
+ ## License
96
+
97
+ Qwen-Image-MICo is licensed under the Apache 2.0 license.
model_index.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "QwenImageEditPipeline",
3
+ "_diffusers_version": "0.35.0.dev0",
4
+ "processor": [
5
+ "transformers",
6
+ "Qwen2VLProcessor"
7
+ ],
8
+ "scheduler": [
9
+ "diffusers",
10
+ "FlowMatchEulerDiscreteScheduler"
11
+ ],
12
+ "text_encoder": [
13
+ "transformers",
14
+ "Qwen2_5_VLForConditionalGeneration"
15
+ ],
16
+ "tokenizer": [
17
+ "transformers",
18
+ "Qwen2Tokenizer"
19
+ ],
20
+ "transformer": [
21
+ "diffusers",
22
+ "QwenImageTransformer2DModel"
23
+ ],
24
+ "vae": [
25
+ "diffusers",
26
+ "AutoencoderKLQwenImage"
27
+ ]
28
+ }
modeling_qwen_image.py ADDED
@@ -0,0 +1,628 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Qwen-Image Team and The HuggingFace Team. All rights reserved.
2
+ # Independently adapted and refactored by the MICo Team.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import inspect
17
+ import math
18
+ from typing import Any, Callable, Dict, List, Optional, Union, Tuple
19
+
20
+ import numpy as np
21
+ import torch
22
+ from PIL import Image
23
+
24
+ from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, Qwen2VLProcessor, PretrainedConfig
25
+ from diffusers.image_processor import PipelineImageInput, VaeImageProcessor
26
+ from diffusers.loaders import QwenImageLoraLoaderMixin
27
+ from diffusers.models import AutoencoderKLQwenImage, QwenImageTransformer2DModel
28
+ from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
29
+ from diffusers.utils import is_torch_xla_available, logging, replace_example_docstring
30
+ from diffusers.utils.torch_utils import randn_tensor
31
+ from diffusers.pipelines.pipeline_utils import DiffusionPipeline
32
+ from diffusers.pipelines.qwenimage.pipeline_output import QwenImagePipelineOutput
33
+
34
+ if is_torch_xla_available():
35
+ import torch_xla.core.xla_model as xm
36
+ XLA_AVAILABLE = True
37
+ else:
38
+ XLA_AVAILABLE = False
39
+
40
+ logger = logging.get_logger(__name__)
41
+
42
+
43
+ class _MICoPipelineUtils:
44
+ """
45
+ Internal utility class grouping static computational methods for the diffusion pipeline.
46
+ Maintains clean namespace and modularizes mathematical operations.
47
+ """
48
+
49
+ @staticmethod
50
+ def compute_temporal_shift(
51
+ image_seq_len: int,
52
+ base_seq_len: int = 256,
53
+ max_seq_len: int = 4096,
54
+ base_shift: float = 0.5,
55
+ max_shift: float = 1.15,
56
+ ) -> float:
57
+ """Calculates the sequence shift parameter based on sequence length ratios."""
58
+ slope = (max_shift - base_shift) / (max_seq_len - base_seq_len)
59
+ intercept = base_shift - slope * base_seq_len
60
+ return image_seq_len * slope + intercept
61
+
62
+ @staticmethod
63
+ def fetch_timesteps(
64
+ scheduler,
65
+ num_inference_steps: Optional[int] = None,
66
+ device: Optional[Union[str, torch.device]] = None,
67
+ timesteps: Optional[List[int]] = None,
68
+ sigmas: Optional[List[float]] = None,
69
+ **kwargs,
70
+ ) -> Tuple[torch.Tensor, int]:
71
+ """Resolves custom timesteps or sigmas from the given scheduler instance."""
72
+ if timesteps is not None and sigmas is not None:
73
+ raise ValueError("Configuration Error: Define either `timesteps` or `sigmas`, but not both.")
74
+
75
+ if timesteps is not None:
76
+ if "timesteps" not in inspect.signature(scheduler.set_timesteps).parameters:
77
+ raise ValueError(f"Scheduler {scheduler.__class__.__name__} rejects custom timesteps.")
78
+ scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
79
+ elif sigmas is not None:
80
+ if "sigmas" not in inspect.signature(scheduler.set_timesteps).parameters:
81
+ raise ValueError(f"Scheduler {scheduler.__class__.__name__} rejects custom sigmas.")
82
+ scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
83
+ else:
84
+ scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
85
+
86
+ return scheduler.timesteps, len(scheduler.timesteps)
87
+
88
+ @staticmethod
89
+ def extract_latent_distribution(
90
+ encoder_output: Any, generator: Optional[torch.Generator] = None, mode: str = "sample"
91
+ ) -> torch.Tensor:
92
+ """Safely extracts tensors from VAE distributions."""
93
+ if hasattr(encoder_output, "latent_dist"):
94
+ return encoder_output.latent_dist.sample(generator) if mode == "sample" else encoder_output.latent_dist.mode()
95
+ elif hasattr(encoder_output, "latents"):
96
+ return encoder_output.latents
97
+ raise AttributeError("Failed to extract latents: Unsupported encoder output format.")
98
+
99
+ @staticmethod
100
+ def pad_image_dimensions(image: Image.Image, multiple_of: int = 32) -> Image.Image:
101
+ """Resizes a PIL image ensuring its dimensions are strict multiples of the given integer."""
102
+ w, h = image.size
103
+ aligned_w = round(w / multiple_of) * multiple_of
104
+ aligned_h = round(h / multiple_of) * multiple_of
105
+ return image.resize((aligned_w, aligned_h))
106
+
107
+
108
+ class QwenImageConfig(PretrainedConfig):
109
+ """Configuration class for parameterized model tracking."""
110
+ model_type = "qwen_image_transformer"
111
+
112
+ def __init__(
113
+ self,
114
+ attention_head_dim=128,
115
+ num_attention_heads=24,
116
+ num_layers=60,
117
+ in_channels=64,
118
+ out_channels=16,
119
+ patch_size=2,
120
+ joint_attention_dim=3584,
121
+ axes_dims_rope=[16, 56, 56],
122
+ guidance_embeds=False,
123
+ **kwargs,
124
+ ):
125
+ self.attention_head_dim = attention_head_dim
126
+ self.num_attention_heads = num_attention_heads
127
+ self.num_layers = num_layers
128
+ self.in_channels = in_channels
129
+ self.out_channels = out_channels
130
+ self.patch_size = patch_size
131
+ self.joint_attention_dim = joint_attention_dim
132
+ self.axes_dims_rope = axes_dims_rope
133
+ self.guidance_embeds = guidance_embeds
134
+ super().__init__(**kwargs)
135
+
136
+
137
+ MICO_EXAMPLE_DOCS = """
138
+ Examples:
139
+ ```py
140
+ >>> import torch
141
+ >>> from PIL import Image
142
+ >>> from diffusers import QwenImageEditPipeline
143
+ >>> from diffusers.utils import load_image
144
+
145
+ >>> # Initialize the MICo-adapted Multi-Modal Pipeline
146
+ >>> pipeline = QwenImageEditPipeline.from_pretrained("Qwen/Qwen-Image-Edit", torch_dtype=torch.bfloat16)
147
+ >>> pipeline.to("cuda")
148
+
149
+ >>> # Fetch source vision data
150
+ >>> source_vision = load_image("https://.../sample_workspace.png").convert("RGB")
151
+
152
+ >>> # Define agent-based instruction for visual modification
153
+ >>> task_instruction = (
154
+ ... "Modify the scene into a futuristic computer vision lab, "
155
+ ... "featuring an autonomous agent analyzing a complex LaTeX manuscript, highly detailed."
156
+ ... )
157
+
158
+ >>> generated_output = pipeline(source_vision, task_instruction, num_inference_steps=50).images[0]
159
+ >>> generated_output.save("mico_agent_output.png")
160
+ ```
161
+ """
162
+
163
+
164
+ class QwenImageEditPipeline(DiffusionPipeline, QwenImageLoraLoaderMixin):
165
+ r"""
166
+ Advanced Pipeline Architecture adapted for dynamic image editing via multi-modal instructions.
167
+
168
+ Architecture Components:
169
+ transformer: The core MMDiT architecture handling conditional denoising.
170
+ scheduler: Flow-matching scheduler controlling the noise traversal sequence.
171
+ vae: Handles transition between pixel space and latent dimensional space.
172
+ text_encoder: Qwen2.5-VL instruction encoder framework.
173
+ tokenizer: Linguistic boundary mapping utility.
174
+ """
175
+
176
+ model_cpu_offload_seq = "text_encoder->transformer->vae"
177
+ _callback_tensor_inputs = ["latents", "prompt_embeds"]
178
+
179
+ def __init__(
180
+ self,
181
+ scheduler: FlowMatchEulerDiscreteScheduler,
182
+ vae: AutoencoderKLQwenImage,
183
+ text_encoder: Qwen2_5_VLForConditionalGeneration,
184
+ tokenizer: Qwen2Tokenizer,
185
+ processor: Qwen2VLProcessor,
186
+ transformer: QwenImageTransformer2DModel,
187
+ ):
188
+ super().__init__()
189
+ self.register_modules(
190
+ vae=vae,
191
+ text_encoder=text_encoder,
192
+ tokenizer=tokenizer,
193
+ processor=processor,
194
+ transformer=transformer,
195
+ scheduler=scheduler,
196
+ )
197
+
198
+ self.vae_scale_factor = 2 ** len(self.vae.temperal_downsample) if getattr(self, "vae", None) else 8
199
+ self.latent_channels = self.vae.config.z_dim if getattr(self, "vae", None) else 16
200
+ self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * 2)
201
+ self.vl_processor = processor
202
+ self.tokenizer_max_length = 1024
203
+
204
+ self.system_message = (
205
+ "Describe the key features of the input image (color, shape, size, texture, objects, background), "
206
+ "then explain how the user's text instruction should alter or modify the image. Generate a new "
207
+ "image that meets the user's requirements while maintaining consistency with the original input where appropriate."
208
+ )
209
+ self.prompt_template_encode_start_idx = 64
210
+ self.default_sample_size = 128
211
+
212
+ def _extract_masked_hidden(self, hidden_states: torch.Tensor, mask: torch.Tensor):
213
+ """Isolates valid computational vectors from padded hidden states."""
214
+ bool_mask = mask.bool()
215
+ valid_lengths = bool_mask.sum(dim=1)
216
+ selected_states = hidden_states[bool_mask]
217
+ return torch.split(selected_states, valid_lengths.tolist(), dim=0)
218
+
219
+ def _get_qwen_prompt_embeds(
220
+ self,
221
+ prompts: Union[str, List[str]] = None,
222
+ images: List[List[Image.Image]] = None,
223
+ device: Optional[torch.device] = None,
224
+ dtype: Optional[torch.dtype] = None,
225
+ ):
226
+ """Processes and extracts embeddings via the visual-language processor framework."""
227
+ device = device or self._execution_device
228
+ dtype = dtype or self.text_encoder.dtype
229
+
230
+ prompts = [prompts] if isinstance(prompts, str) else prompts
231
+
232
+ # Standardize nested image lists
233
+ if isinstance(images, Image.Image):
234
+ images = [[images]]
235
+ elif isinstance(images[0], Image.Image):
236
+ images = [images]
237
+
238
+ assert len(prompts) == len(images), "Data integrity error: Prompt and vision input batch sizes mismatch."
239
+
240
+ compiled_texts = []
241
+ for prompt, image_list in zip(prompts, images):
242
+ chat_payload = [
243
+ {"role": "system", "content": self.system_message},
244
+ {
245
+ "role": "user",
246
+ "content": [{"type": "image", "image": img} for img in image_list] + [{"type": "text", "text": prompt}],
247
+ },
248
+ ]
249
+ compiled_texts.append(self.processor.apply_chat_template(chat_payload, tokenize=False, add_generation_prompt=True))
250
+
251
+ model_inputs = self.processor(
252
+ text=compiled_texts,
253
+ images=images,
254
+ do_resize=False,
255
+ padding=True,
256
+ return_tensors="pt"
257
+ ).to(self.device)
258
+
259
+ drop_idx = self.prompt_template_encode_start_idx
260
+ outputs = self.text_encoder(
261
+ input_ids=model_inputs.input_ids,
262
+ attention_mask=model_inputs.attention_mask,
263
+ pixel_values=model_inputs.pixel_values,
264
+ image_grid_thw=model_inputs.image_grid_thw,
265
+ output_hidden_states=True,
266
+ )
267
+
268
+ hidden_states = outputs.hidden_states[-1]
269
+ split_states = self._extract_masked_hidden(hidden_states, model_inputs.attention_mask)
270
+ split_states = [state[drop_idx:] for state in split_states]
271
+
272
+ attn_mask_list = [torch.ones(state.size(0), dtype=torch.long, device=state.device) for state in split_states]
273
+ max_seq_len = max([state.size(0) for state in split_states])
274
+
275
+ prompt_embeds = torch.stack(
276
+ [torch.cat([state, state.new_zeros(max_seq_len - state.size(0), state.size(1))]) for state in split_states]
277
+ ).to(dtype=dtype, device=device)
278
+
279
+ encoder_attention_mask = torch.stack(
280
+ [torch.cat([mask, mask.new_zeros(max_seq_len - mask.size(0))]) for mask in attn_mask_list]
281
+ )
282
+
283
+ return prompt_embeds, encoder_attention_mask
284
+
285
+ def encode_prompt(
286
+ self,
287
+ prompt: Union[str, List[str]],
288
+ images: List[Image.Image] = None,
289
+ device: Optional[torch.device] = None,
290
+ num_images_per_prompt: int = 1,
291
+ prompt_embeds: Optional[torch.Tensor] = None,
292
+ prompt_embeds_mask: Optional[torch.Tensor] = None,
293
+ max_sequence_length: int = 1024,
294
+ ):
295
+ """Constructs and duplicates conditional multi-modal latents for execution batches."""
296
+ device = device or self._execution_device
297
+ prompt = [prompt] if isinstance(prompt, str) else prompt
298
+ batch_size = len(prompt) if prompt_embeds is None else prompt_embeds.shape[0]
299
+
300
+ if prompt_embeds is None:
301
+ prompt_embeds, prompt_embeds_mask = self._get_qwen_prompt_embeds(prompt, images, device)
302
+
303
+ _, seq_len, _ = prompt_embeds.shape
304
+ prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1).view(batch_size * num_images_per_prompt, seq_len, -1)
305
+ prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_images_per_prompt, 1).view(batch_size * num_images_per_prompt, seq_len)
306
+
307
+ return prompt_embeds, prompt_embeds_mask
308
+
309
+ def check_inputs(
310
+ self,
311
+ prompt,
312
+ height,
313
+ width,
314
+ negative_prompt=None,
315
+ prompt_embeds=None,
316
+ negative_prompt_embeds=None,
317
+ prompt_embeds_mask=None,
318
+ negative_prompt_embeds_mask=None,
319
+ callback_on_step_end_tensor_inputs=None,
320
+ max_sequence_length=None,
321
+ ):
322
+ """Performs pre-flight integrity checks on all pipeline parameters."""
323
+ spatial_constraint = self.vae_scale_factor * 2
324
+ if height % spatial_constraint != 0 or width % spatial_constraint != 0:
325
+ logger.warning(f"Resolution ({width}x{height}) is not divisible by {spatial_constraint}. Auto-resizing will trigger.")
326
+
327
+ if callback_on_step_end_tensor_inputs:
328
+ invalid_cbs = [k for k in callback_on_step_end_tensor_inputs if k not in self._callback_tensor_inputs]
329
+ if invalid_cbs:
330
+ raise ValueError(f"Invalid callback tensors: {invalid_cbs}. Permitted: {self._callback_tensor_inputs}")
331
+
332
+ if bool(prompt is not None) == bool(prompt_embeds is not None):
333
+ raise ValueError("Configuration logic trap: Define strictly one of `prompt` OR `prompt_embeds`.")
334
+
335
+ if prompt is not None and not isinstance(prompt, (str, list)):
336
+ raise ValueError(f"`prompt` validation failed. Expected str/list, got {type(prompt)}")
337
+
338
+ if negative_prompt is not None and negative_prompt_embeds is not None:
339
+ raise ValueError("Conflict: Both `negative_prompt` and `negative_prompt_embeds` were supplied.")
340
+
341
+ if prompt_embeds is not None and prompt_embeds_mask is None:
342
+ raise ValueError("Masking integrity error: `prompt_embeds_mask` is mandatory when injecting `prompt_embeds`.")
343
+
344
+ if negative_prompt_embeds is not None and negative_prompt_embeds_mask is None:
345
+ raise ValueError("Masking integrity error: `negative_prompt_embeds_mask` is mandatory alongside `negative_prompt_embeds`.")
346
+
347
+ if max_sequence_length is not None and max_sequence_length > 1024:
348
+ raise ValueError(f"Sequence cap exceeded: {max_sequence_length} > 1024 limit.")
349
+
350
+ @staticmethod
351
+ def _pack_latents(latents, batch_size, num_channels_latents, height, width):
352
+ """Transforms standard latent layouts into packed multi-patch configurations."""
353
+ latents = latents.view(batch_size, num_channels_latents, height // 2, 2, width // 2, 2)
354
+ latents = latents.permute(0, 2, 4, 1, 3, 5)
355
+ return latents.reshape(batch_size, (height // 2) * (width // 2), num_channels_latents * 4)
356
+
357
+ @staticmethod
358
+ def _unpack_latents(latents, height, width, vae_scale_factor):
359
+ """Reverses multi-patch configurations back to classical latent layouts."""
360
+ batch_size, _, channels = latents.shape
361
+ h_adjusted = 2 * (int(height) // (vae_scale_factor * 2))
362
+ w_adjusted = 2 * (int(width) // (vae_scale_factor * 2))
363
+
364
+ latents = latents.view(batch_size, h_adjusted // 2, w_adjusted // 2, channels // 4, 2, 2)
365
+ latents = latents.permute(0, 3, 1, 4, 2, 5)
366
+ return latents.reshape(batch_size, channels // 4, 1, h_adjusted, w_adjusted)
367
+
368
+ def _encode_vae_image(self, image: torch.Tensor, generator: torch.Generator):
369
+ """Converts raw pixel space into initial VAE distributions."""
370
+ if isinstance(generator, list):
371
+ latents_cache = [
372
+ _MICoPipelineUtils.extract_latent_distribution(self.vae.encode(image[i : i + 1]), generator=generator[i], mode="argmax")
373
+ for i in range(image.shape[0])
374
+ ]
375
+ image_latents = torch.cat(latents_cache, dim=0)
376
+ else:
377
+ image_latents = _MICoPipelineUtils.extract_latent_distribution(self.vae.encode(image), generator=generator, mode="argmax")
378
+
379
+ latents_mean = torch.tensor(self.vae.config.latents_mean).view(1, self.latent_channels, 1, 1, 1).to(image_latents.device, image_latents.dtype)
380
+ latents_std = torch.tensor(self.vae.config.latents_std).view(1, self.latent_channels, 1, 1, 1).to(image_latents.device, image_latents.dtype)
381
+
382
+ return (image_latents - latents_mean) / latents_std
383
+
384
+ def enable_vae_slicing(self): self.vae.enable_slicing()
385
+ def disable_vae_slicing(self): self.vae.disable_slicing()
386
+ def enable_vae_tiling(self): self.vae.enable_tiling()
387
+ def disable_vae_tiling(self): self.vae.disable_tiling()
388
+
389
+ def prepare_latents(
390
+ self, images, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None
391
+ ):
392
+ """Allocates and standardizes initial noise vectors and vision encodings for the step loop."""
393
+ h_adjusted = 2 * (int(height) // (self.vae_scale_factor * 2))
394
+ w_adjusted = 2 * (int(width) // (self.vae_scale_factor * 2))
395
+ noise_shape = (batch_size, 1, num_channels_latents, h_adjusted, w_adjusted)
396
+
397
+ processed_img_latents = []
398
+ for img_tensor in images:
399
+ img_tensor = img_tensor.to(device=device, dtype=dtype)
400
+ current_latents = self._encode_vae_image(img_tensor, generator) if img_tensor.shape[1] != self.latent_channels else img_tensor
401
+
402
+ if batch_size > current_latents.shape[0] and batch_size % current_latents.shape[0] == 0:
403
+ duplication_factor = batch_size // current_latents.shape[0]
404
+ current_latents = torch.cat([current_latents] * duplication_factor, dim=0)
405
+ elif batch_size > current_latents.shape[0]:
406
+ raise ValueError(f"Batch misalignment: Cannot broadcast {current_latents.shape[0]} inputs to size {batch_size}.")
407
+
408
+ img_h, img_w = current_latents.shape[3:]
409
+ packed_img = self._pack_latents(current_latents, batch_size, num_channels_latents, img_h, img_w)
410
+ processed_img_latents.append(packed_img)
411
+
412
+ if isinstance(generator, list) and len(generator) != batch_size:
413
+ raise ValueError(f"Generator constraint failed. Passed {len(generator)} generators for batch size {batch_size}.")
414
+
415
+ if latents is None:
416
+ latents = randn_tensor(noise_shape, generator=generator, device=device, dtype=dtype)
417
+ latents = self._pack_latents(latents, batch_size, num_channels_latents, h_adjusted, w_adjusted)
418
+ else:
419
+ latents = latents.to(device=device, dtype=dtype)
420
+
421
+ return latents, processed_img_latents
422
+
423
+ @property
424
+ def guidance_scale(self): return self._guidance_scale
425
+
426
+ @property
427
+ def attention_kwargs(self): return self._attention_kwargs
428
+
429
+ @property
430
+ def num_timesteps(self): return self._num_timesteps
431
+
432
+ @property
433
+ def current_timestep(self): return self._current_timestep
434
+
435
+ @property
436
+ def interrupt(self): return self._interrupt
437
+
438
+ def _prepare_vision_inputs(self, images: List[PipelineImageInput]) -> Tuple[List[Image.Image], int, int]:
439
+ """Calculates area-constrained ratios and standardizes spatial constraints."""
440
+ total_pixels = sum(math.prod(img.size) for img in images)
441
+ ratio = 1024 / (total_pixels ** 0.5)
442
+
443
+ resized_imgs = [img.resize(size=(round(img.width * ratio), round(img.height * ratio))) for img in images]
444
+ aligned_imgs = [_MICoPipelineUtils.pad_image_dimensions(img, multiple_of=32) for img in resized_imgs]
445
+
446
+ return aligned_imgs, aligned_imgs[0].width, aligned_imgs[0].height
447
+
448
+ def _decode_latents_to_image(self, latents, height, width, output_type):
449
+ """Transforms structural noise output back to human-interpretable pixels."""
450
+ if output_type == "latent":
451
+ return latents
452
+
453
+ unpacked_latents = self._unpack_latents(latents, height, width, self.vae_scale_factor).to(self.vae.dtype)
454
+
455
+ l_mean = torch.tensor(self.vae.config.latents_mean).view(1, self.vae.config.z_dim, 1, 1, 1).to(unpacked_latents.device, unpacked_latents.dtype)
456
+ l_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(1, self.vae.config.z_dim, 1, 1, 1).to(unpacked_latents.device, unpacked_latents.dtype)
457
+
458
+ normalized_latents = unpacked_latents / l_std + l_mean
459
+ decoded_pixels = self.vae.decode(normalized_latents, return_dict=False)[0][:, :, 0]
460
+
461
+ return self.image_processor.postprocess(decoded_pixels, output_type=output_type)
462
+
463
+ @torch.no_grad()
464
+ @replace_example_docstring(MICO_EXAMPLE_DOCS)
465
+ def __call__(
466
+ self,
467
+ images: List[PipelineImageInput] = None,
468
+ prompt: Union[str, List[str]] = None,
469
+ negative_prompt: Union[str, List[str]] = None,
470
+ true_cfg_scale: float = 4.0,
471
+ height: Optional[int] = None,
472
+ width: Optional[int] = None,
473
+ num_inference_steps: int = 50,
474
+ sigmas: Optional[List[float]] = None,
475
+ guidance_scale: float = 1.0,
476
+ num_images_per_prompt: int = 1,
477
+ generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
478
+ latents: Optional[torch.Tensor] = None,
479
+ prompt_embeds: Optional[torch.Tensor] = None,
480
+ prompt_embeds_mask: Optional[torch.Tensor] = None,
481
+ negative_prompt_embeds: Optional[torch.Tensor] = None,
482
+ negative_prompt_embeds_mask: Optional[torch.Tensor] = None,
483
+ output_type: Optional[str] = "pil",
484
+ return_dict: bool = True,
485
+ attention_kwargs: Optional[Dict[str, Any]] = None,
486
+ callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = None,
487
+ callback_on_step_end_tensor_inputs: List[str] = ["latents"],
488
+ max_sequence_length: int = 512,
489
+ ):
490
+ r"""
491
+ Executes the multi-modal editing sequence based on the provided vision inputs and textual instructions.
492
+
493
+ Args:
494
+ images: Source vision data to be modified.
495
+ prompt: Textual instructions dictating the structural or stylistic changes.
496
+ negative_prompt: Elements or styles to actively suppress during generation.
497
+ num_inference_steps: Number of denoising iterations (default: 50).
498
+ guidance_scale: Modifier for classifier-free guidance adherence.
499
+ output_type: Format of the returned vision data ('pil' or 'latent').
500
+ return_dict: Whether to wrap outputs in a pipeline standard dictionary.
501
+
502
+ Examples:
503
+
504
+ Returns:
505
+ `QwenImagePipelineOutput` or `tuple`: Contains the generated vision data.
506
+ """
507
+ # 1. Image preparation & Alignment
508
+ images = [images] if not isinstance(images, (list, tuple)) else images
509
+ images, derived_w, derived_h = self._prepare_vision_inputs(images)
510
+
511
+ width, height = width or derived_w, height or derived_h
512
+ padding_base = self.vae_scale_factor * 2
513
+ width = (width // padding_base) * padding_base
514
+ height = (height // padding_base) * padding_base
515
+
516
+ # 2. Input Sanity Checks
517
+ self.check_inputs(
518
+ prompt, height, width, negative_prompt, prompt_embeds,
519
+ negative_prompt_embeds, prompt_embeds_mask, negative_prompt_embeds_mask,
520
+ callback_on_step_end_tensor_inputs, max_sequence_length
521
+ )
522
+
523
+ self._guidance_scale = guidance_scale
524
+ self._attention_kwargs = attention_kwargs or {}
525
+ self._current_timestep = None
526
+ self._interrupt = False
527
+
528
+ batch_size = len(prompt) if isinstance(prompt, list) else (prompt_embeds.shape[0] if prompt_embeds is not None else 1)
529
+ device = self._execution_device
530
+
531
+ # 3. Instruction Encoding
532
+ scaled_vision = [img.resize((round(img.width * 28 / 32), round(img.height * 28 / 32))) for img in images]
533
+ processed_images = [self.image_processor.preprocess(img, img.height, img.width).unsqueeze(2) for img in images]
534
+
535
+ cfg_eligible = true_cfg_scale > 1.0 and (negative_prompt is not None or negative_prompt_embeds is not None)
536
+
537
+ prompt_embeds, prompt_embeds_mask = self.encode_prompt(
538
+ images=scaled_vision, prompt=prompt, prompt_embeds=prompt_embeds,
539
+ prompt_embeds_mask=prompt_embeds_mask, device=device,
540
+ num_images_per_prompt=num_images_per_prompt, max_sequence_length=max_sequence_length,
541
+ )
542
+
543
+ if cfg_eligible:
544
+ negative_prompt_embeds, negative_prompt_embeds_mask = self.encode_prompt(
545
+ images=scaled_vision, prompt=negative_prompt, prompt_embeds=negative_prompt_embeds,
546
+ prompt_embeds_mask=negative_prompt_embeds_mask, device=device,
547
+ num_images_per_prompt=num_images_per_prompt, max_sequence_length=max_sequence_length,
548
+ )
549
+
550
+ # 4. Latent Space Injection
551
+ v_channels = self.transformer.config.in_channels // 4
552
+ latents, img_latents = self.prepare_latents(
553
+ processed_images, batch_size * num_images_per_prompt, v_channels,
554
+ height, width, prompt_embeds.dtype, device, generator, latents
555
+ )
556
+
557
+ img_shapes = [[(1, height // padding_base, width // padding_base)] + [(1, img.shape[-2] // padding_base, img.shape[-1] // padding_base) for img in processed_images]] * batch_size
558
+
559
+ # 5. Scheduling Alignment
560
+ sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas
561
+ shift_mu = _MICoPipelineUtils.compute_temporal_shift(
562
+ latents.shape[1],
563
+ self.scheduler.config.get("base_image_seq_len", 256),
564
+ self.scheduler.config.get("max_image_seq_len", 4096),
565
+ self.scheduler.config.get("base_shift", 0.5),
566
+ self.scheduler.config.get("max_shift", 1.15),
567
+ )
568
+ timesteps, num_inference_steps = _MICoPipelineUtils.fetch_timesteps(self.scheduler, num_inference_steps, device, sigmas=sigmas, mu=shift_mu)
569
+ num_warmup_steps = max(len(timesteps) - num_inference_steps * self.scheduler.order, 0)
570
+ self._num_timesteps = len(timesteps)
571
+
572
+ guidance_vec = torch.full([1], guidance_scale, device=device, dtype=torch.float32).expand(latents.shape[0]) if self.transformer.config.guidance_embeds else None
573
+
574
+ txt_lens = prompt_embeds_mask.sum(dim=1).tolist() if prompt_embeds_mask is not None else None
575
+ neg_txt_lens = negative_prompt_embeds_mask.sum(dim=1).tolist() if negative_prompt_embeds_mask is not None else None
576
+
577
+ # 6. Target Denoising Loop execution
578
+ self.scheduler.set_begin_index(0)
579
+ with self.progress_bar(total=num_inference_steps) as progress_bar:
580
+ for i, t in enumerate(timesteps):
581
+ if self.interrupt: continue
582
+ self._current_timestep = t
583
+ t_expanded = t.expand(latents.shape[0]).to(latents.dtype)
584
+ combined_states = torch.cat([latents] + img_latents, dim=1)
585
+
586
+ with self.transformer.cache_context("cond"):
587
+ pred_noise = self.transformer(
588
+ hidden_states=combined_states, timestep=t_expanded / 1000, guidance=guidance_vec,
589
+ encoder_hidden_states_mask=prompt_embeds_mask, encoder_hidden_states=prompt_embeds,
590
+ img_shapes=img_shapes, txt_seq_lens=txt_lens, attention_kwargs=self.attention_kwargs, return_dict=False
591
+ )[0][:, : latents.size(1)]
592
+
593
+ if cfg_eligible:
594
+ with self.transformer.cache_context("uncond"):
595
+ neg_pred_noise = self.transformer(
596
+ hidden_states=combined_states, timestep=t_expanded / 1000, guidance=guidance_vec,
597
+ encoder_hidden_states_mask=negative_prompt_embeds_mask, encoder_hidden_states=negative_prompt_embeds,
598
+ img_shapes=img_shapes, txt_seq_lens=neg_txt_lens, attention_kwargs=self.attention_kwargs, return_dict=False
599
+ )[0][:, : latents.size(1)]
600
+
601
+ blended_pred = neg_pred_noise + true_cfg_scale * (pred_noise - neg_pred_noise)
602
+ cond_norm, blend_norm = torch.norm(pred_noise, dim=-1, keepdim=True), torch.norm(blended_pred, dim=-1, keepdim=True)
603
+ pred_noise = blended_pred * (cond_norm / blend_norm)
604
+
605
+ base_dtype = latents.dtype
606
+ latents = self.scheduler.step(pred_noise, t, latents, return_dict=False)[0]
607
+
608
+ if latents.dtype != base_dtype:
609
+ latents = latents.to(base_dtype) if torch.backends.mps.is_available() else latents
610
+
611
+ if callback_on_step_end:
612
+ cb_kwargs = {k: locals()[k] for k in callback_on_step_end_tensor_inputs}
613
+ cb_outputs = callback_on_step_end(self, i, t, cb_kwargs)
614
+ latents = cb_outputs.pop("latents", latents)
615
+ prompt_embeds = cb_outputs.pop("prompt_embeds", prompt_embeds)
616
+
617
+ if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):
618
+ progress_bar.update()
619
+
620
+ if XLA_AVAILABLE: xm.mark_step()
621
+
622
+ self._current_timestep = None
623
+
624
+ # 7. Final Output Resolution
625
+ final_image = self._decode_latents_to_image(latents, height, width, output_type)
626
+ self.maybe_free_model_hooks()
627
+
628
+ return QwenImagePipelineOutput(images=final_image) if return_dict else (final_image,)