someone-in-the-world commited on
Commit
e1efec5
·
verified ·
1 Parent(s): e4ff2ed

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/10.jpg filter=lfs diff=lfs merge=lfs -text
37
+ examples/11.png filter=lfs diff=lfs merge=lfs -text
38
+ examples/6.jpg filter=lfs diff=lfs merge=lfs -text
39
+ examples/7.webp filter=lfs diff=lfs merge=lfs -text
40
+ examples/8.jpg filter=lfs diff=lfs merge=lfs -text
41
+ examples/9.png filter=lfs diff=lfs merge=lfs -text
LICENSE.txt ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md CHANGED
@@ -1,13 +1,83 @@
1
  ---
2
  title: FireRed Image Edit 1.0 Fast
3
- emoji: 💻
4
- colorFrom: blue
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: FireRed Image Edit 1.0 Fast
3
+ emoji: 🔥
4
+ colorFrom: red
5
+ colorTo: yellow
6
  sdk: gradio
7
+ sdk_version: "5.0.0"
 
8
  app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # **[FireRed-Image-Edit-1.0-Fast](https://huggingface.co/spaces/prithivMLmods/FireRed-Image-Edit-1.0-Fast)**
13
+
14
+ FireRed-Image-Edit-1.0-Fast is a high-performance, AI-driven image editing application that utilizes advanced diffusers and the QwenImageEditPlusPipeline for precise, prompt-based image modifications. Incorporating rapid Transformer configurations, the application provides an interactive Gradio web interface with a custom Soft Blue theme for an aesthetically pleasing user experience. Users can leverage powerful flow match euler discrete schedulers to seamlessly edit visual content by submitting an original image alongside descriptive textual instructions. The application operates entirely in Python, efficiently utilizing CUDA capabilities for accelerated machine learning computations, and serves as a fast, state-of-the-art solution for automated, text-guided image manipulation without complex manual editing software.
15
+
16
+ <img width="1918" height="1753" alt="Screenshot 2026-03-21 at 15-27-14 FireRed Image Edit 1 0 Fast - a Hugging Face Space by prithivMLmods" src="https://github.com/user-attachments/assets/c88a82b6-f877-4312-94e3-fd3119b03318" />
17
+
18
+ ## Features
19
+
20
+ * **Advanced Diffusers Pipeline:** Utilizes the QwenImageEditPlusPipeline integrated with FlowMatchEulerDiscreteScheduler for high-fidelity image editing based on user prompts.
21
+ * **Rapid AI Architecture:** Employs optimized transformer structures designed for fast inference, providing quick iterations and real-time responsiveness.
22
+ * **Custom Themed Interface:** Provides an interactive, user-friendly Gradio web interface styled with a custom Soft OrangeRed theme for an optimal visual layout.
23
+ * **Hardware Acceleration:** Automatically identifies and leverages CUDA-compatible devices for optimal computational performance, rendering complex edits rapidly.
24
+
25
+ ## Installation
26
+
27
+ ### 1. Clone the Repository
28
+
29
+ ```bash
30
+ git clone https://github.com/PRITHIVSAKTHIUR/FireRed-Image-Edit-1.0-Fast.git
31
+ cd FireRed-Image-Edit-1.0-Fast
32
+ ```
33
+
34
+ ### 2. Install Pre-requirements
35
+
36
+ Certain system-level or structural dependencies must be configured before setting up the main python environment:
37
+
38
+ ```bash
39
+ pip install -r pre-requirements.txt
40
+ ```
41
+
42
+ ### 3. Install Standard Dependencies
43
+
44
+ Install the core Python packages, which include critical modules like Diffusers, Accelerate, PEFT, and Gradio:
45
+
46
+ ```bash
47
+ pip install -r requirements.txt
48
+ ```
49
+
50
+ ## How to Run
51
+
52
+ To start the application and load the local server, run the main Python script:
53
+
54
+ ```bash
55
+ python app.py
56
+ ```
57
+
58
+ Once the model weights are successfully loaded into your device's memory and the server starts, the terminal will provide a local URL (typically `http://127.0.0.1:7860`). Open this link in your web browser to interact with the visual interface.
59
+
60
+ ## Project Structure
61
+
62
+ * `app.py`: The main entry point script containing the custom Gradio interface setup, pipeline initialization, and inference logic.
63
+ * `qwenimage/`: Core directory housing the transformer and processor modules crucial for the underlying image manipulation techniques.
64
+ * `requirements.txt`: The primary file listing Python library requirements needed to operate the application correctly.
65
+ * `pre-requirements.txt`: A list containing earlier or auxiliary dependency specifications.
66
+ * `examples/`: Directory dedicated to storing sample images and expected outputs to verify application functionality.
67
+ * `LICENSE.txt`: The legal text detailing the licensing constraints and permissions.
68
+
69
+ ## Workflow
70
+
71
+ 1. Navigate to the local server URL provided after executing the application.
72
+ 2. Upload a source image that you wish to edit into the input module.
73
+ 3. Provide a clear, detailed text prompt describing the exact modifications you want the AI to perform on the image.
74
+ 4. The system executes the QwenImageEditPlusPipeline via the underlying rapid transformers to compute the altered visual output.
75
+ 5. Retrieve and save the edited image directly from the interface.
76
+
77
+ ## License
78
+
79
+ This project is open-source. For detailed terms and conditions, refer to the included `LICENSE.txt` file within the repository.
80
+
81
+ ## Contributing
82
+
83
+ Community contributions are encouraged. Please submit an issue for bug reports or create a Pull Request to propose features, optimize inference times, or improve the user interface.
app.py ADDED
@@ -0,0 +1,1158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gc
3
+ import gradio as gr
4
+ import numpy as np
5
+ import spaces
6
+ import torch
7
+ import random
8
+ import base64
9
+ import json
10
+ import html as html_lib
11
+ from io import BytesIO
12
+ from PIL import Image
13
+
14
+ MAX_SEED = np.iinfo(np.int32).max
15
+ LANCZOS = getattr(Image, "Resampling", Image).LANCZOS
16
+
17
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
18
+
19
+ print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES"))
20
+ print("torch.__version__ =", torch.__version__)
21
+ print("Using device:", device)
22
+
23
+ from diffusers import FlowMatchEulerDiscreteScheduler
24
+ from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline
25
+ from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel
26
+ from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3
27
+
28
+ dtype = torch.bfloat16
29
+
30
+ pipe = QwenImageEditPlusPipeline.from_pretrained(
31
+ "FireRedTeam/FireRed-Image-Edit-1.1",
32
+ transformer=QwenImageTransformer2DModel.from_pretrained(
33
+ "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19",
34
+ torch_dtype=dtype,
35
+ device_map="cuda",
36
+ ),
37
+ torch_dtype=dtype,
38
+ ).to(device)
39
+
40
+ try:
41
+ pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3())
42
+ print("Flash Attention 3 Processor set successfully.")
43
+ except Exception as e:
44
+ print(f"Warning: Could not set FA3 processor: {e}")
45
+
46
+ EXAMPLES_CONFIG = [
47
+ {
48
+ "images": ["examples/1.jpg"],
49
+ "prompt": "cinematic polaroid with soft grain subtle vignette gentle lighting white frame handwritten photographed 'Fire-Edit' preserving realistic texture and details.",
50
+ },
51
+ {
52
+ "images": ["examples/2.jpg"],
53
+ "prompt": "Transform the image into a dotted cartoon style.",
54
+ },
55
+ {
56
+ "images": ["examples/3.jpeg"],
57
+ "prompt": "Convert it to black and white.",
58
+ },
59
+ {
60
+ "images": ["examples/4.jpg", "examples/5.jpg"],
61
+ "prompt": "Replace her glasses with the new glasses from image 1.",
62
+ },
63
+ {
64
+ "images": ["examples/8.jpg", "examples/9.png"],
65
+ "prompt": "Replace the current clothing with the clothing from the reference image 2. Keep the person's face, hairstyle, body pose, background, lighting, and camera angle unchanged. Ensure the new outfit fits naturally with realistic fabric texture, proper shadows, folds, and accurate proportions. Match the lighting, color tone, and overall style for a seamless and high-quality result.",
66
+ },
67
+ {
68
+ "images": ["examples/10.jpg", "examples/11.png"],
69
+ "prompt": "Replace the current clothing with the clothing from the reference image 2. Keep the person's face, hairstyle, body pose, background, lighting, and camera angle unchanged. Ensure the new outfit fits naturally with realistic fabric texture, proper shadows, folds, and accurate proportions. Match the lighting, color tone, and overall style for a seamless and high-quality result.",
70
+ },
71
+ ]
72
+
73
+
74
+ def make_thumb_b64(path, max_dim=220):
75
+ if not os.path.exists(path):
76
+ return ""
77
+ try:
78
+ img = Image.open(path).convert("RGB")
79
+ img.thumbnail((max_dim, max_dim), LANCZOS)
80
+ buf = BytesIO()
81
+ img.save(buf, format="JPEG", quality=65)
82
+ return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"
83
+ except Exception as e:
84
+ print(f"Thumbnail error for {path}: {e}")
85
+ return ""
86
+
87
+
88
+ def encode_full_image(path):
89
+ if not os.path.exists(path):
90
+ return ""
91
+ try:
92
+ with open(path, "rb") as f:
93
+ data = f.read()
94
+ ext = path.rsplit(".", 1)[-1].lower()
95
+ mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp"}.get(ext, "image/jpeg")
96
+ return f"data:{mime};base64,{base64.b64encode(data).decode()}"
97
+ except Exception as e:
98
+ print(f"Encode error for {path}: {e}")
99
+ return ""
100
+
101
+
102
+ def build_example_cards_html():
103
+ cards = ""
104
+ for i, ex in enumerate(EXAMPLES_CONFIG):
105
+ thumbs_html = ""
106
+ for path in ex["images"]:
107
+ thumb = make_thumb_b64(path)
108
+ if thumb:
109
+ thumbs_html += f'<img src="{thumb}" alt="">'
110
+ else:
111
+ thumbs_html += '<div class="example-thumb-placeholder">Preview</div>'
112
+ n = len(ex["images"])
113
+ badge = f'{n} image{"s" if n > 1 else ""}'
114
+ prompt_short = html_lib.escape(ex["prompt"][:90])
115
+ if len(ex["prompt"]) > 90:
116
+ prompt_short += "..."
117
+ cards += f'''<div class="example-card" data-idx="{i}">
118
+ <div class="example-thumbs">{thumbs_html}</div>
119
+ <div class="example-meta"><span class="example-badge">{badge}</span></div>
120
+ <div class="example-prompt-text">{prompt_short}</div>
121
+ </div>'''
122
+ return cards
123
+
124
+
125
+ def load_example_data(idx_str):
126
+ try:
127
+ idx = int(float(idx_str)) if idx_str and idx_str.strip() else -1
128
+ except (ValueError, TypeError):
129
+ idx = -1
130
+ if idx < 0 or idx >= len(EXAMPLES_CONFIG):
131
+ return json.dumps({"images": [], "prompt": "", "names": [], "status": "error"})
132
+ ex = EXAMPLES_CONFIG[idx]
133
+ b64_list, names = [], []
134
+ for path in ex["images"]:
135
+ b64 = encode_full_image(path)
136
+ if b64:
137
+ b64_list.append(b64)
138
+ names.append(os.path.basename(path))
139
+ return json.dumps({"images": b64_list, "prompt": ex["prompt"], "names": names, "status": "ok"})
140
+
141
+
142
+ print("Building example thumbnails...")
143
+ EXAMPLE_CARDS_HTML = build_example_cards_html()
144
+ print(f"Built {len(EXAMPLES_CONFIG)} example cards.")
145
+
146
+
147
+ def b64_to_pil_list(b64_json_str):
148
+ if not b64_json_str or b64_json_str.strip() in ("", "[]"):
149
+ return []
150
+ try:
151
+ b64_list = json.loads(b64_json_str)
152
+ except Exception:
153
+ return []
154
+ pil_images = []
155
+ for b64_str in b64_list:
156
+ if not b64_str or not isinstance(b64_str, str):
157
+ continue
158
+ try:
159
+ if b64_str.startswith("data:image"):
160
+ _, data = b64_str.split(",", 1)
161
+ else:
162
+ data = b64_str
163
+ image_data = base64.b64decode(data)
164
+ pil_images.append(Image.open(BytesIO(image_data)).convert("RGB"))
165
+ except Exception as e:
166
+ print(f"Error decoding image: {e}")
167
+ return pil_images
168
+
169
+
170
+ def update_dimensions_on_upload(image):
171
+ if image is None:
172
+ return 1024, 1024
173
+ w, h = image.size
174
+ if w > h:
175
+ nw = 1024
176
+ nh = int(nw * h / w)
177
+ else:
178
+ nh = 1024
179
+ nw = int(nh * w / h)
180
+ return (nw // 8) * 8, (nh // 8) * 8
181
+
182
+
183
+ @spaces.GPU
184
+ def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps, progress=gr.Progress(track_tqdm=True)):
185
+ gc.collect()
186
+ torch.cuda.empty_cache()
187
+ pil_images = b64_to_pil_list(images_b64_json)
188
+ if not pil_images:
189
+ raise gr.Error("Please upload at least one image to edit.")
190
+ if not prompt or prompt.strip() == "":
191
+ raise gr.Error("Please enter an edit prompt.")
192
+ if randomize_seed:
193
+ seed = random.randint(0, MAX_SEED)
194
+ generator = torch.Generator(device=device).manual_seed(seed)
195
+ negative_prompt = "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry"
196
+ width, height = update_dimensions_on_upload(pil_images[0])
197
+ try:
198
+ result_image = pipe(
199
+ image=pil_images, prompt=prompt, negative_prompt=negative_prompt,
200
+ height=height, width=width, num_inference_steps=steps,
201
+ generator=generator, true_cfg_scale=guidance_scale,
202
+ ).images[0]
203
+ return result_image, seed
204
+ except Exception as e:
205
+ raise e
206
+ finally:
207
+ gc.collect()
208
+ torch.cuda.empty_cache()
209
+
210
+
211
+ css = r"""
212
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap');
213
+ *{box-sizing:border-box;margin:0;padding:0}
214
+ body,.gradio-container{
215
+ background:#0f0f13!important;font-family:'Inter',system-ui,-apple-system,sans-serif!important;
216
+ font-size:14px!important;color:#e4e4e7!important;min-height:100vh;
217
+ }
218
+ .dark body,.dark .gradio-container{background:#0f0f13!important;color:#e4e4e7!important}
219
+ footer{display:none!important}
220
+ .hidden-input{display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important}
221
+
222
+ #example-load-btn{
223
+ position:absolute!important;left:-9999px!important;top:-9999px!important;
224
+ width:1px!important;height:1px!important;opacity:0.01!important;
225
+ pointer-events:none!important;overflow:hidden!important;
226
+ }
227
+ #gradio-run-btn{
228
+ position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;
229
+ opacity:0.01;pointer-events:none;overflow:hidden;
230
+ }
231
+
232
+ .app-shell{
233
+ background:#18181b;border:1px solid #27272a;border-radius:16px;
234
+ margin:12px auto;max-width:1400px;overflow:hidden;
235
+ box-shadow:0 25px 50px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.03);
236
+ }
237
+ .app-header{
238
+ background:linear-gradient(135deg,#18181b,#1e1e24);border-bottom:1px solid #27272a;
239
+ padding:14px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px;
240
+ }
241
+ .app-header-left{display:flex;align-items:center;gap:12px}
242
+ .app-logo{
243
+ width:36px;height:36px;background:linear-gradient(135deg,#1E90FF,#47A3FF,#7CB8FF);
244
+ border-radius:10px;display:flex;align-items:center;justify-content:center;
245
+ box-shadow:0 4px 12px rgba(30,144,255,.35);
246
+ }
247
+ .app-logo svg{width:20px;height:20px;fill:#fff;flex-shrink:0}
248
+ .app-title{
249
+ font-size:18px;font-weight:700;background:linear-gradient(135deg,#e4e4e7,#a1a1aa);
250
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.3px;
251
+ }
252
+ .app-badge{
253
+ font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px;
254
+ background:rgba(30,144,255,.15);color:#47A3FF;border:1px solid rgba(30,144,255,.25);letter-spacing:.3px;
255
+ }
256
+ .app-badge.fast{background:rgba(34,197,94,.12);color:#4ade80;border:1px solid rgba(34,197,94,.25)}
257
+
258
+ .app-toolbar{
259
+ background:#18181b;border-bottom:1px solid #27272a;padding:8px 16px;
260
+ display:flex;gap:4px;align-items:center;flex-wrap:wrap;
261
+ }
262
+ .tb-sep{width:1px;height:28px;background:#27272a;margin:0 8px}
263
+ .modern-tb-btn{
264
+ display:inline-flex;align-items:center;justify-content:center;gap:6px;
265
+ min-width:32px;height:34px;background:transparent;border:1px solid transparent;
266
+ border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;padding:0 12px;
267
+ font-family:'Inter',sans-serif;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;
268
+ transition:all .15s ease;
269
+ }
270
+ .modern-tb-btn:hover{background:rgba(30,144,255,.15);border-color:rgba(30,144,255,.3)}
271
+ .modern-tb-btn:active,.modern-tb-btn.active{background:rgba(30,144,255,.25);border-color:rgba(30,144,255,.45)}
272
+ .modern-tb-btn .tb-label{font-size:13px;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;font-weight:600}
273
+ .modern-tb-btn .tb-svg{width:15px;height:15px;flex-shrink:0;color:#ffffff!important}
274
+ .modern-tb-btn .tb-svg,
275
+ .modern-tb-btn .tb-svg *{stroke:#ffffff!important;fill:none!important}
276
+ .tb-info{font-family:'JetBrains Mono',monospace;font-size:12px;color:#71717a;padding:0 8px;display:flex;align-items:center}
277
+
278
+ body:not(.dark) .modern-tb-btn,body:not(.dark) .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
279
+ body:not(.dark) .modern-tb-btn .tb-svg,body:not(.dark) .modern-tb-btn .tb-svg *{stroke:#ffffff!important}
280
+ .dark .modern-tb-btn,.dark .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
281
+ .dark .modern-tb-btn .tb-svg,.dark .modern-tb-btn .tb-svg *{stroke:#ffffff!important}
282
+ .gradio-container .modern-tb-btn,.gradio-container .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
283
+ .gradio-container .modern-tb-btn .tb-svg,.gradio-container .modern-tb-btn .tb-svg *{stroke:#ffffff!important}
284
+
285
+ .app-main-row{display:flex;gap:0;flex:1;overflow:hidden}
286
+ .app-main-left{flex:1;display:flex;flex-direction:column;min-width:0;border-right:1px solid #27272a}
287
+ .app-main-right{width:420px;display:flex;flex-direction:column;flex-shrink:0;background:#18181b}
288
+
289
+ #gallery-drop-zone{position:relative;background:#09090b;min-height:440px;overflow:auto}
290
+ #gallery-drop-zone.drag-over{outline:2px solid #1E90FF;outline-offset:-2px;background:rgba(30,144,255,.04)}
291
+
292
+ .upload-prompt-modern{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:20}
293
+ .upload-click-area{
294
+ display:flex;flex-direction:column;align-items:center;justify-content:center;
295
+ cursor:pointer;padding:36px 52px;border:2px dashed #3f3f46;border-radius:16px;
296
+ background:rgba(30,144,255,.03);transition:all .2s ease;gap:8px;
297
+ }
298
+ .upload-click-area:hover{background:rgba(30,144,255,.08);border-color:#1E90FF;transform:scale(1.03)}
299
+ .upload-click-area:active{background:rgba(30,144,255,.12);transform:scale(.98)}
300
+ .upload-click-area svg{width:80px;height:80px}
301
+ .upload-main-text{color:#71717a;font-size:14px;font-weight:500;margin-top:4px}
302
+ .upload-sub-text{color:#52525b;font-size:12px}
303
+
304
+ .image-gallery-grid{
305
+ display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));
306
+ gap:12px;padding:16px;align-content:start;
307
+ }
308
+ .gallery-thumb{
309
+ position:relative;aspect-ratio:1;border-radius:10px;overflow:hidden;
310
+ cursor:pointer;border:2px solid #27272a;transition:all .2s ease;background:#18181b;
311
+ }
312
+ .gallery-thumb:hover{border-color:#3f3f46;transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.4)}
313
+ .gallery-thumb.selected{border-color:#1E90FF!important;box-shadow:0 0 0 3px rgba(30,144,255,.2)}
314
+ .gallery-thumb img{width:100%;height:100%;object-fit:cover}
315
+ .thumb-badge{
316
+ position:absolute;top:6px;left:6px;background:#1E90FF;color:#fff;
317
+ padding:2px 8px;border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:600;
318
+ }
319
+ .thumb-remove{
320
+ position:absolute;top:6px;right:6px;width:24px;height:24px;background:rgba(0,0,0,.75);
321
+ color:#fff;border:1px solid rgba(255,255,255,.15);border-radius:50%;cursor:pointer;
322
+ display:none;align-items:center;justify-content:center;font-size:12px;transition:all .15s;line-height:1;
323
+ }
324
+ .gallery-thumb:hover .thumb-remove{display:flex}
325
+ .thumb-remove:hover{background:#1E90FF;border-color:#1E90FF}
326
+ .gallery-add-card{
327
+ aspect-ratio:1;border-radius:10px;border:2px dashed #3f3f46;
328
+ display:flex;flex-direction:column;align-items:center;justify-content:center;
329
+ cursor:pointer;transition:all .2s ease;background:rgba(30,144,255,.03);gap:4px;
330
+ }
331
+ .gallery-add-card:hover{border-color:#1E90FF;background:rgba(30,144,255,.08)}
332
+ .gallery-add-card .add-icon{font-size:28px;color:#71717a;font-weight:300}
333
+ .gallery-add-card .add-text{font-size:12px;color:#71717a;font-weight:500}
334
+
335
+ .hint-bar{
336
+ background:rgba(30,144,255,.06);border-top:1px solid #27272a;border-bottom:1px solid #27272a;
337
+ padding:10px 20px;font-size:13px;color:#a1a1aa;line-height:1.7;
338
+ }
339
+ .hint-bar b{color:#7CB8FF;font-weight:600}
340
+ .hint-bar kbd{
341
+ display:inline-block;padding:1px 6px;background:#27272a;border:1px solid #3f3f46;
342
+ border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;color:#a1a1aa;
343
+ }
344
+
345
+ .suggestions-section{border-top:1px solid #27272a;padding:12px 16px}
346
+ .suggestions-title,.examples-title{
347
+ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;
348
+ letter-spacing:.8px;margin-bottom:10px;
349
+ }
350
+ .suggestions-wrap{display:flex;flex-wrap:wrap;gap:6px}
351
+ .suggestion-chip{
352
+ display:inline-flex;align-items:center;gap:4px;padding:5px 12px;
353
+ background:rgba(30,144,255,.08);border:1px solid rgba(30,144,255,.2);border-radius:20px;
354
+ color:#7CB8FF;font-size:12px;font-weight:500;font-family:'Inter',sans-serif;
355
+ cursor:pointer;transition:all .15s;white-space:nowrap;
356
+ }
357
+ .suggestion-chip:hover{background:rgba(30,144,255,.15);border-color:rgba(30,144,255,.35);color:#47A3FF;transform:translateY(-1px)}
358
+
359
+ .examples-section{border-top:1px solid #27272a;padding:12px 16px}
360
+ .examples-scroll{display:flex;gap:10px;overflow-x:auto;padding-bottom:8px}
361
+ .examples-scroll::-webkit-scrollbar{height:6px}
362
+ .examples-scroll::-webkit-scrollbar-track{background:#09090b;border-radius:3px}
363
+ .examples-scroll::-webkit-scrollbar-thumb{background:#27272a;border-radius:3px}
364
+ .examples-scroll::-webkit-scrollbar-thumb:hover{background:#3f3f46}
365
+ .example-card{
366
+ flex-shrink:0;width:210px;background:#09090b;border:1px solid #27272a;
367
+ border-radius:10px;overflow:hidden;cursor:pointer;transition:all .2s ease;
368
+ }
369
+ .example-card:hover{border-color:#1E90FF;transform:translateY(-2px);box-shadow:0 4px 12px rgba(30,144,255,.15)}
370
+ .example-card.loading{opacity:.5;pointer-events:none}
371
+ .example-thumbs{display:flex;height:110px;overflow:hidden;background:#18181b}
372
+ .example-thumbs img{flex:1;object-fit:cover;min-width:0;border-bottom:1px solid #27272a}
373
+ .example-thumb-placeholder{
374
+ flex:1;display:flex;align-items:center;justify-content:center;
375
+ background:#18181b;color:#3f3f46;font-size:11px;min-width:0;
376
+ }
377
+ .example-meta{padding:6px 10px;display:flex;align-items:center;gap:6px}
378
+ .example-badge{
379
+ display:inline-flex;padding:2px 7px;background:rgba(30,144,255,.1);border-radius:4px;
380
+ font-size:10px;font-weight:600;color:#47A3FF;font-family:'JetBrains Mono',monospace;white-space:nowrap;
381
+ }
382
+ .example-prompt-text{
383
+ padding:0 10px 8px;font-size:11px;color:#a1a1aa;line-height:1.4;
384
+ display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
385
+ }
386
+
387
+ .panel-card{border-bottom:1px solid #27272a}
388
+ .panel-card-title{
389
+ padding:12px 20px;font-size:12px;font-weight:600;color:#71717a;
390
+ text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6);
391
+ }
392
+ .panel-card-body{padding:16px 20px;display:flex;flex-direction:column;gap:8px}
393
+ .modern-label{font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block}
394
+ .modern-textarea{
395
+ width:100%;background:#09090b;border:1px solid #27272a;border-radius:8px;
396
+ padding:10px 14px;font-family:'Inter',sans-serif;font-size:14px;color:#e4e4e7;
397
+ resize:vertical;outline:none;min-height:42px;transition:border-color .2s;
398
+ }
399
+ .modern-textarea:focus{border-color:#1E90FF;box-shadow:0 0 0 3px rgba(30,144,255,.15)}
400
+ .modern-textarea::placeholder{color:#3f3f46}
401
+ .modern-textarea.error-flash{
402
+ border-color:#ef4444!important;box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;animation:shake .4s ease;
403
+ }
404
+ @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}}
405
+
406
+ .toast-notification{
407
+ position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-120%);
408
+ z-index:9999;padding:10px 24px;border-radius:10px;font-family:'Inter',sans-serif;
409
+ font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px;
410
+ box-shadow:0 8px 24px rgba(0,0,0,.5);
411
+ transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none;
412
+ }
413
+ .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}
414
+ .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)}
415
+ .toast-notification.warning{background:linear-gradient(135deg,#d97706,#b45309);color:#fff;border:1px solid rgba(255,255,255,.15)}
416
+ .toast-notification.info{background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;border:1px solid rgba(255,255,255,.15)}
417
+ .toast-notification .toast-icon{font-size:16px;line-height:1}
418
+ .toast-notification .toast-text{line-height:1.3}
419
+
420
+ .btn-run{
421
+ display:flex;align-items:center;justify-content:center;gap:8px;width:100%;
422
+ background:linear-gradient(135deg,#1E90FF,#1873CC);border:none;border-radius:10px;
423
+ padding:12px 24px;cursor:pointer;font-size:15px;font-weight:600;font-family:'Inter',sans-serif;
424
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;transition:all .2s ease;letter-spacing:-.2px;
425
+ box-shadow:0 4px 16px rgba(30,144,255,.3),inset 0 1px 0 rgba(255,255,255,.1);
426
+ }
427
+ .btn-run:hover{
428
+ background:linear-gradient(135deg,#47A3FF,#1E90FF);transform:translateY(-1px);
429
+ box-shadow:0 6px 24px rgba(30,144,255,.45),inset 0 1px 0 rgba(255,255,255,.15);
430
+ }
431
+ .btn-run:active{transform:translateY(0);box-shadow:0 2px 8px rgba(30,144,255,.3)}
432
+ .btn-run svg{width:18px;height:18px;fill:#ffffff!important}
433
+ .btn-run svg path{fill:#ffffff!important}
434
+ #custom-run-btn,#custom-run-btn *,#custom-run-btn span,#custom-run-btn svg,
435
+ #custom-run-btn svg path,#run-btn-label,.btn-run,.btn-run *{
436
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
437
+ }
438
+ body:not(.dark) .btn-run,body:not(.dark) .btn-run *,body:not(.dark) #custom-run-btn,
439
+ body:not(.dark) #custom-run-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important}
440
+ .dark .btn-run,.dark .btn-run *,.dark #custom-run-btn,.dark #custom-run-btn *{
441
+ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important;
442
+ }
443
+ .gradio-container .btn-run,.gradio-container .btn-run *,.gradio-container #custom-run-btn,
444
+ .gradio-container #custom-run-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important}
445
+
446
+ .output-frame{border-bottom:1px solid #27272a;display:flex;flex-direction:column;position:relative}
447
+ .output-frame .out-title{
448
+ padding:10px 20px;font-size:13px;font-weight:700;color:#ffffff!important;
449
+ -webkit-text-fill-color:#ffffff!important;text-transform:uppercase;letter-spacing:.8px;
450
+ border-bottom:1px solid rgba(39,39,42,.6);display:flex;align-items:center;justify-content:space-between;
451
+ }
452
+ .output-frame .out-title span{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important}
453
+ .output-frame .out-body{
454
+ flex:1;background:#09090b;display:flex;align-items:center;justify-content:center;
455
+ overflow:hidden;min-height:240px;position:relative;
456
+ }
457
+ .output-frame .out-body img{max-width:100%;max-height:460px;image-rendering:auto}
458
+ .output-frame .out-placeholder{color:#3f3f46;font-size:13px;text-align:center;padding:20px}
459
+ .out-download-btn{
460
+ display:none;align-items:center;justify-content:center;background:rgba(30,144,255,.1);
461
+ border:1px solid rgba(30,144,255,.2);border-radius:6px;cursor:pointer;padding:3px 10px;
462
+ font-size:11px;font-weight:500;color:#7CB8FF!important;gap:4px;height:24px;transition:all .15s;
463
+ }
464
+ .out-download-btn:hover{background:rgba(30,144,255,.2);border-color:rgba(30,144,255,.35);color:#ffffff!important}
465
+ .out-download-btn.visible{display:inline-flex}
466
+ .out-download-btn svg{width:12px;height:12px;fill:#7CB8FF}
467
+
468
+ .modern-loader{
469
+ display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(9,9,11,.92);
470
+ z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(4px);
471
+ }
472
+ .modern-loader.active{display:flex}
473
+ .modern-loader .loader-spinner{
474
+ width:36px;height:36px;border:3px solid #27272a;border-top-color:#1E90FF;
475
+ border-radius:50%;animation:spin .8s linear infinite;
476
+ }
477
+ @keyframes spin{to{transform:rotate(360deg)}}
478
+ .modern-loader .loader-text{font-size:13px;color:#a1a1aa;font-weight:500}
479
+ .loader-bar-track{width:200px;height:4px;background:#27272a;border-radius:2px;overflow:hidden}
480
+ .loader-bar-fill{
481
+ height:100%;background:linear-gradient(90deg,#1E90FF,#47A3FF,#1E90FF);
482
+ background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px;
483
+ }
484
+ @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}
485
+
486
+ .settings-group{border:1px solid #27272a;border-radius:10px;margin:12px 16px;padding:0;overflow:hidden}
487
+ .settings-group-title{
488
+ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px;
489
+ padding:10px 16px;border-bottom:1px solid #27272a;background:rgba(24,24,27,.5);
490
+ }
491
+ .settings-group-body{padding:14px 16px;display:flex;flex-direction:column;gap:12px}
492
+ .slider-row{display:flex;align-items:center;gap:10px;min-height:28px}
493
+ .slider-row label{font-size:13px;font-weight:500;color:#a1a1aa;min-width:72px;flex-shrink:0}
494
+ .slider-row input[type="range"]{
495
+ flex:1;-webkit-appearance:none;appearance:none;height:6px;background:#27272a;
496
+ border-radius:3px;outline:none;min-width:0;
497
+ }
498
+ .slider-row input[type="range"]::-webkit-slider-thumb{
499
+ -webkit-appearance:none;width:16px;height:16px;background:linear-gradient(135deg,#1E90FF,#1873CC);
500
+ border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(30,144,255,.4);transition:transform .15s;
501
+ }
502
+ .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)}
503
+ .slider-row input[type="range"]::-moz-range-thumb{
504
+ width:16px;height:16px;background:linear-gradient(135deg,#1E90FF,#1873CC);
505
+ border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(30,144,255,.4);
506
+ }
507
+ .slider-row .slider-val{
508
+ min-width:52px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:12px;
509
+ font-weight:500;padding:3px 8px;background:#09090b;border:1px solid #27272a;
510
+ border-radius:6px;color:#a1a1aa;flex-shrink:0;
511
+ }
512
+ .checkbox-row{display:flex;align-items:center;gap:8px;font-size:13px;color:#a1a1aa}
513
+ .checkbox-row input[type="checkbox"]{accent-color:#1E90FF;width:16px;height:16px;cursor:pointer}
514
+ .checkbox-row label{color:#a1a1aa;font-size:13px;cursor:pointer}
515
+
516
+ .app-statusbar{
517
+ background:#18181b;border-top:1px solid #27272a;padding:6px 20px;
518
+ display:flex;gap:12px;height:34px;align-items:center;font-size:12px;
519
+ }
520
+ .app-statusbar .sb-section{
521
+ padding:0 12px;flex:1;display:flex;align-items:center;font-family:'JetBrains Mono',monospace;
522
+ font-size:12px;color:#52525b;overflow:hidden;white-space:nowrap;
523
+ }
524
+ .app-statusbar .sb-section.sb-fixed{
525
+ flex:0 0 auto;min-width:90px;text-align:center;justify-content:center;
526
+ padding:3px 12px;background:rgba(30,144,255,.08);border-radius:6px;color:#47A3FF;font-weight:500;
527
+ }
528
+
529
+ .exp-note{padding:10px 20px;font-size:12px;color:#52525b;border-top:1px solid #27272a;text-align:center}
530
+ .exp-note a{color:#47A3FF;text-decoration:none}
531
+ .exp-note a:hover{text-decoration:underline}
532
+
533
+ .dark .app-shell{background:#18181b}
534
+ .dark .upload-prompt-modern{background:transparent}
535
+ .dark .panel-card{background:#18181b}
536
+ .dark .settings-group{background:#18181b}
537
+ .dark .output-frame .out-title{color:#ffffff!important}
538
+ .dark .output-frame .out-title span{color:#ffffff!important}
539
+ .dark .out-download-btn{color:#7CB8FF!important}
540
+ .dark .out-download-btn:hover{color:#ffffff!important}
541
+
542
+ ::-webkit-scrollbar{width:8px;height:8px}
543
+ ::-webkit-scrollbar-track{background:#09090b}
544
+ ::-webkit-scrollbar-thumb{background:#27272a;border-radius:4px}
545
+ ::-webkit-scrollbar-thumb:hover{background:#3f3f46}
546
+
547
+ @media(max-width:840px){
548
+ .app-main-row{flex-direction:column}
549
+ .app-main-right{width:100%}
550
+ .app-main-left{border-right:none;border-bottom:1px solid #27272a}
551
+ }
552
+ """
553
+
554
+ gallery_js = r"""
555
+ () => {
556
+ function init() {
557
+ if (window.__fireRedInitDone) return;
558
+
559
+ const galleryGrid = document.getElementById('image-gallery-grid');
560
+ const dropZone = document.getElementById('gallery-drop-zone');
561
+ const uploadPrompt = document.getElementById('upload-prompt');
562
+ const uploadClick = document.getElementById('upload-click-area');
563
+ const fileInput = document.getElementById('custom-file-input');
564
+ const btnUpload = document.getElementById('tb-upload');
565
+ const btnRemove = document.getElementById('tb-remove');
566
+ const btnClear = document.getElementById('tb-clear');
567
+ const promptInput = document.getElementById('custom-prompt-input');
568
+ const runBtnEl = document.getElementById('custom-run-btn');
569
+ const imgCountTb = document.getElementById('tb-image-count');
570
+ const imgCountSb = document.getElementById('sb-image-count');
571
+
572
+ if (!galleryGrid || !fileInput || !dropZone) {
573
+ setTimeout(init, 250);
574
+ return;
575
+ }
576
+
577
+ window.__fireRedInitDone = true;
578
+
579
+ let images = [];
580
+ window.__uploadedImages = images;
581
+ let selectedIdx = -1;
582
+ let toastTimer = null;
583
+
584
+ function showToast(message, type) {
585
+ let toast = document.getElementById('app-toast');
586
+ if (!toast) {
587
+ toast = document.createElement('div');
588
+ toast.id = 'app-toast';
589
+ toast.className = 'toast-notification';
590
+ toast.innerHTML = '<span class="toast-icon"></span><span class="toast-text"></span>';
591
+ document.body.appendChild(toast);
592
+ }
593
+ const icon = toast.querySelector('.toast-icon');
594
+ const text = toast.querySelector('.toast-text');
595
+ toast.className = 'toast-notification ' + (type || 'error');
596
+ if (type === 'warning') icon.textContent = '\u26A0';
597
+ else if (type === 'info') icon.textContent = '\u2139';
598
+ else icon.textContent = '\u2717';
599
+ text.textContent = message;
600
+ if (toastTimer) clearTimeout(toastTimer);
601
+ void toast.offsetWidth;
602
+ toast.classList.add('visible');
603
+ toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500);
604
+ }
605
+ window.__showToast = showToast;
606
+
607
+ function flashPromptError() {
608
+ if (!promptInput) return;
609
+ promptInput.classList.add('error-flash');
610
+ promptInput.focus();
611
+ setTimeout(() => promptInput.classList.remove('error-flash'), 800);
612
+ }
613
+
614
+ function setGradioValue(containerId, value) {
615
+ const container = document.getElementById(containerId);
616
+ if (!container) return;
617
+ container.querySelectorAll('input, textarea').forEach(el => {
618
+ if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return;
619
+ const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
620
+ const ns = Object.getOwnPropertyDescriptor(proto, 'value');
621
+ if (ns && ns.set) {
622
+ ns.set.call(el, value);
623
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
624
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
625
+ }
626
+ });
627
+ }
628
+ window.__setGradioValue = setGradioValue;
629
+
630
+ function syncImagesToGradio() {
631
+ window.__uploadedImages = images;
632
+ const b64Array = images.map(img => img.b64);
633
+ setGradioValue('hidden-images-b64', JSON.stringify(b64Array));
634
+ updateCounts();
635
+ }
636
+
637
+ function syncPromptToGradio() {
638
+ if (promptInput) setGradioValue('prompt-gradio-input', promptInput.value);
639
+ }
640
+
641
+ function updateCounts() {
642
+ const n = images.length;
643
+ const txt = n > 0 ? n + ' image' + (n > 1 ? 's' : '') : 'No images';
644
+ if (imgCountTb) imgCountTb.textContent = txt;
645
+ if (imgCountSb) imgCountSb.textContent = n > 0 ? txt + ' uploaded' : 'No images uploaded';
646
+ }
647
+
648
+ function addImage(b64, name) {
649
+ images.push({id: Date.now() + Math.random(), b64: b64, name: name});
650
+ renderGallery();
651
+ syncImagesToGradio();
652
+ }
653
+ window.__addImage = addImage;
654
+
655
+ function removeImage(idx) {
656
+ images.splice(idx, 1);
657
+ if (selectedIdx === idx) selectedIdx = -1;
658
+ else if (selectedIdx > idx) selectedIdx--;
659
+ renderGallery();
660
+ syncImagesToGradio();
661
+ }
662
+
663
+ function clearAll() {
664
+ images = [];
665
+ window.__uploadedImages = images;
666
+ selectedIdx = -1;
667
+ renderGallery();
668
+ syncImagesToGradio();
669
+ }
670
+ window.__clearAll = clearAll;
671
+
672
+ function selectImage(idx) {
673
+ selectedIdx = (selectedIdx === idx) ? -1 : idx;
674
+ renderGallery();
675
+ }
676
+
677
+ function renderGallery() {
678
+ if (images.length === 0) {
679
+ galleryGrid.innerHTML = '';
680
+ galleryGrid.style.display = 'none';
681
+ if (uploadPrompt) uploadPrompt.style.display = '';
682
+ return;
683
+ }
684
+ if (uploadPrompt) uploadPrompt.style.display = 'none';
685
+ galleryGrid.style.display = 'grid';
686
+
687
+ let html = '';
688
+ images.forEach((img, i) => {
689
+ const sel = i === selectedIdx ? ' selected' : '';
690
+ html += '<div class="gallery-thumb' + sel + '" data-idx="' + i + '">'
691
+ + '<img src="' + img.b64 + '" alt="' + (img.name||'image') + '">'
692
+ + '<span class="thumb-badge">#' + (i+1) + '</span>'
693
+ + '<button class="thumb-remove" data-remove="' + i + '">\u2715</button>'
694
+ + '</div>';
695
+ });
696
+ html += '<div class="gallery-add-card" id="gallery-add-card">'
697
+ + '<span class="add-icon">+</span>'
698
+ + '<span class="add-text">Add</span>'
699
+ + '</div>';
700
+ galleryGrid.innerHTML = html;
701
+
702
+ galleryGrid.querySelectorAll('.gallery-thumb').forEach(thumb => {
703
+ thumb.addEventListener('click', (e) => {
704
+ if (e.target.closest('.thumb-remove')) return;
705
+ selectImage(parseInt(thumb.dataset.idx));
706
+ });
707
+ });
708
+ galleryGrid.querySelectorAll('.thumb-remove').forEach(btn => {
709
+ btn.addEventListener('click', (e) => {
710
+ e.stopPropagation();
711
+ removeImage(parseInt(btn.dataset.remove));
712
+ });
713
+ });
714
+ const addCard = document.getElementById('gallery-add-card');
715
+ if (addCard) addCard.addEventListener('click', () => fileInput.click());
716
+ }
717
+
718
+ function processFiles(files) {
719
+ Array.from(files).forEach(file => {
720
+ if (!file.type.startsWith('image/')) return;
721
+ const reader = new FileReader();
722
+ reader.onload = (e) => addImage(e.target.result, file.name);
723
+ reader.readAsDataURL(file);
724
+ });
725
+ }
726
+
727
+ fileInput.addEventListener('change', (e) => { processFiles(e.target.files); e.target.value = ''; });
728
+ if (uploadClick) uploadClick.addEventListener('click', () => fileInput.click());
729
+ if (btnUpload) btnUpload.addEventListener('click', () => fileInput.click());
730
+ if (btnRemove) btnRemove.addEventListener('click', () => {
731
+ if (selectedIdx >= 0 && selectedIdx < images.length) removeImage(selectedIdx);
732
+ });
733
+ if (btnClear) btnClear.addEventListener('click', clearAll);
734
+
735
+ dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
736
+ dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); });
737
+ dropZone.addEventListener('drop', (e) => {
738
+ e.preventDefault(); dropZone.classList.remove('drag-over');
739
+ if (e.dataTransfer.files.length) processFiles(e.dataTransfer.files);
740
+ });
741
+
742
+ if (promptInput) promptInput.addEventListener('input', syncPromptToGradio);
743
+
744
+ window.__setPrompt = function(text) {
745
+ if (promptInput) { promptInput.value = text; syncPromptToGradio(); }
746
+ };
747
+
748
+ document.querySelectorAll('.example-card[data-idx]').forEach(card => {
749
+ card.addEventListener('click', () => {
750
+ const idx = card.getAttribute('data-idx');
751
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
752
+ card.classList.add('loading');
753
+ showToast('Loading example...', 'info');
754
+
755
+ setGradioValue('example-result-data', '');
756
+ setGradioValue('example-idx-input', idx);
757
+
758
+ setTimeout(() => {
759
+ const btn = document.getElementById('example-load-btn');
760
+ if (btn) {
761
+ const b = btn.querySelector('button');
762
+ if (b) b.click(); else btn.click();
763
+ }
764
+ }, 150);
765
+
766
+ setTimeout(() => card.classList.remove('loading'), 12000);
767
+ });
768
+ });
769
+
770
+ function syncSlider(customId, gradioId) {
771
+ const slider = document.getElementById(customId);
772
+ const valSpan = document.getElementById(customId + '-val');
773
+ if (!slider) return;
774
+ slider.addEventListener('input', () => {
775
+ if (valSpan) valSpan.textContent = slider.value;
776
+ const container = document.getElementById(gradioId);
777
+ if (!container) return;
778
+ container.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => {
779
+ const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
780
+ if (ns && ns.set) {
781
+ ns.set.call(el, slider.value);
782
+ el.dispatchEvent(new Event('input', {bubbles:true, composed:true}));
783
+ el.dispatchEvent(new Event('change', {bubbles:true, composed:true}));
784
+ }
785
+ });
786
+ });
787
+ }
788
+ syncSlider('custom-seed', 'gradio-seed');
789
+ syncSlider('custom-guidance', 'gradio-guidance');
790
+ syncSlider('custom-steps', 'gradio-steps');
791
+
792
+ const randCheck = document.getElementById('custom-randomize');
793
+ if (randCheck) {
794
+ randCheck.addEventListener('change', () => {
795
+ const container = document.getElementById('gradio-randomize');
796
+ if (!container) return;
797
+ const cb = container.querySelector('input[type="checkbox"]');
798
+ if (cb && cb.checked !== randCheck.checked) cb.click();
799
+ });
800
+ }
801
+
802
+ function showLoader() {
803
+ const l = document.getElementById('output-loader');
804
+ if (l) l.classList.add('active');
805
+ const sb = document.querySelector('.sb-fixed');
806
+ if (sb) sb.textContent = 'Processing...';
807
+ }
808
+ function hideLoader() {
809
+ const l = document.getElementById('output-loader');
810
+ if (l) l.classList.remove('active');
811
+ const sb = document.querySelector('.sb-fixed');
812
+ if (sb) sb.textContent = 'Done';
813
+ }
814
+ window.__showLoader = showLoader;
815
+ window.__hideLoader = hideLoader;
816
+
817
+ function validateBeforeRun() {
818
+ const promptVal = promptInput ? promptInput.value.trim() : '';
819
+ const hasImages = images.length > 0;
820
+ if (!hasImages && !promptVal) { showToast('Please upload an image and enter a prompt', 'error'); flashPromptError(); return false; }
821
+ if (!hasImages) { showToast('Please upload at least one image', 'error'); return false; }
822
+ if (!promptVal) { showToast('Please enter an edit prompt', 'warning'); flashPromptError(); return false; }
823
+ return true;
824
+ }
825
+
826
+ window.__clickGradioRunBtn = function() {
827
+ if (!validateBeforeRun()) return;
828
+ syncPromptToGradio(); syncImagesToGradio(); showLoader();
829
+ setTimeout(() => {
830
+ const gradioBtn = document.getElementById('gradio-run-btn');
831
+ if (!gradioBtn) return;
832
+ const btn = gradioBtn.querySelector('button');
833
+ if (btn) btn.click(); else gradioBtn.click();
834
+ }, 200);
835
+ };
836
+
837
+ if (runBtnEl) runBtnEl.addEventListener('click', () => window.__clickGradioRunBtn());
838
+
839
+ renderGallery();
840
+ updateCounts();
841
+ }
842
+ init();
843
+ }
844
+ """
845
+
846
+ wire_outputs_js = r"""
847
+ () => {
848
+ function watchOutputs() {
849
+ const resultContainer = document.getElementById('gradio-result');
850
+ const outBody = document.getElementById('output-image-container');
851
+ const outPh = document.getElementById('output-placeholder');
852
+ const dlBtn = document.getElementById('dl-btn-output');
853
+
854
+ if (!resultContainer || !outBody) { setTimeout(watchOutputs, 500); return; }
855
+
856
+ if (dlBtn) {
857
+ dlBtn.addEventListener('click', (e) => {
858
+ e.stopPropagation();
859
+ const img = outBody.querySelector('img.modern-out-img');
860
+ if (img && img.src) {
861
+ const a = document.createElement('a');
862
+ a.href = img.src; a.download = 'firered_output.png';
863
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
864
+ }
865
+ });
866
+ }
867
+
868
+ function syncImage() {
869
+ const resultImg = resultContainer.querySelector('img');
870
+ if (resultImg && resultImg.src) {
871
+ if (outPh) outPh.style.display = 'none';
872
+ let existing = outBody.querySelector('img.modern-out-img');
873
+ if (!existing) { existing = document.createElement('img'); existing.className = 'modern-out-img'; outBody.appendChild(existing); }
874
+ if (existing.src !== resultImg.src) {
875
+ existing.src = resultImg.src;
876
+ if (dlBtn) dlBtn.classList.add('visible');
877
+ if (window.__hideLoader) window.__hideLoader();
878
+ }
879
+ }
880
+ }
881
+ const observer = new MutationObserver(syncImage);
882
+ observer.observe(resultContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']});
883
+ setInterval(syncImage, 800);
884
+ }
885
+ watchOutputs();
886
+
887
+ function watchSeed() {
888
+ const seedContainer = document.getElementById('gradio-seed');
889
+ const seedSlider = document.getElementById('custom-seed');
890
+ const seedVal = document.getElementById('custom-seed-val');
891
+ if (!seedContainer || !seedSlider) { setTimeout(watchSeed, 500); return; }
892
+ function sync() {
893
+ const el = seedContainer.querySelector('input[type="range"],input[type="number"]');
894
+ if (el && el.value) { seedSlider.value = el.value; if (seedVal) seedVal.textContent = el.value; }
895
+ }
896
+ const obs = new MutationObserver(sync);
897
+ obs.observe(seedContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']});
898
+ setInterval(sync, 1000);
899
+ }
900
+ watchSeed();
901
+
902
+ function watchExampleResults() {
903
+ const container = document.getElementById('example-result-data');
904
+ if (!container) { setTimeout(watchExampleResults, 500); return; }
905
+
906
+ let lastProcessed = '';
907
+
908
+ function checkResult() {
909
+ const el = container.querySelector('textarea') || container.querySelector('input');
910
+ if (!el) return;
911
+ const val = el.value;
912
+ if (!val || val === lastProcessed || val.length < 20) return;
913
+
914
+ try {
915
+ const data = JSON.parse(val);
916
+ if (data.status === 'ok' && data.images && data.images.length > 0) {
917
+ lastProcessed = val;
918
+
919
+ if (window.__clearAll) window.__clearAll();
920
+ if (window.__setPrompt && data.prompt) window.__setPrompt(data.prompt);
921
+
922
+ data.images.forEach((b64, i) => {
923
+ if (b64 && window.__addImage) {
924
+ const name = (data.names && data.names[i]) ? data.names[i] : ('example_' + (i+1) + '.jpg');
925
+ window.__addImage(b64, name);
926
+ }
927
+ });
928
+
929
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
930
+ if (window.__showToast) window.__showToast('Example loaded — ' + data.images.length + ' image(s)', 'info');
931
+ } else if (data.status === 'error') {
932
+ document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading'));
933
+ if (window.__showToast) window.__showToast('Could not load example images', 'error');
934
+ }
935
+ } catch(e) {
936
+ console.error('Example parse error:', e);
937
+ }
938
+ }
939
+
940
+ const obs = new MutationObserver(checkResult);
941
+ obs.observe(container, {childList:true, subtree:true, characterData:true, attributes:true});
942
+ setInterval(checkResult, 500);
943
+ }
944
+ watchExampleResults();
945
+ }
946
+ """
947
+
948
+ DOWNLOAD_SVG = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 16l-5-5h3V4h4v7h3l-5 5z"/><path d="M20 18H4v2h16v-2z"/></svg>'
949
+
950
+ UPLOAD_SVG = '<svg class="tb-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>'
951
+
952
+ REMOVE_SVG = '<svg class="tb-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>'
953
+
954
+ CLEAR_SVG = '<svg class="tb-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>'
955
+
956
+ FIRE_LOGO_SVG = '<svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg"><path d="M12 23c-3.6 0-8-2.69-8-7.5 0-3.5 3-6.5 4.5-8 .27-.27.75-.08.75.28v2.44c0 .42.5.63.72.28C12.28 7.5 13 3 13 1c0-.42.48-.64.8-.35C18 4.5 20 9 20 12c0 5.5-3.5 11-8 11z"/></svg>'
957
+
958
+ with gr.Blocks() as demo:
959
+
960
+ hidden_images_b64 = gr.Textbox(value="[]", elem_id="hidden-images-b64", elem_classes="hidden-input", container=False)
961
+ prompt = gr.Textbox(value="", elem_id="prompt-gradio-input", elem_classes="hidden-input", container=False)
962
+ seed = gr.Slider(minimum=0, maximum=MAX_SEED, step=1, value=0, elem_id="gradio-seed", elem_classes="hidden-input", container=False)
963
+ randomize_seed = gr.Checkbox(value=True, elem_id="gradio-randomize", elem_classes="hidden-input", container=False)
964
+ guidance_scale = gr.Slider(minimum=1.0, maximum=10.0, step=0.1, value=1.0, elem_id="gradio-guidance", elem_classes="hidden-input", container=False)
965
+ steps = gr.Slider(minimum=1, maximum=50, step=1, value=4, elem_id="gradio-steps", elem_classes="hidden-input", container=False)
966
+ result = gr.Image(elem_id="gradio-result", elem_classes="hidden-input", container=False, format="png")
967
+
968
+ example_idx = gr.Textbox(value="", elem_id="example-idx-input", elem_classes="hidden-input", container=False)
969
+ example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False)
970
+ example_load_btn = gr.Button("Load Example", elem_id="example-load-btn")
971
+
972
+ gr.HTML(f"""
973
+ <div class="app-shell">
974
+
975
+ <div class="app-header">
976
+ <div class="app-header-left">
977
+ <div class="app-logo">{FIRE_LOGO_SVG}</div>
978
+ <span class="app-title">FireRed-Image-Edit</span>
979
+ <span class="app-badge">v1.1</span>
980
+ <span class="app-badge fast">4-Step Fast</span>
981
+ </div>
982
+ </div>
983
+
984
+ <div class="app-toolbar">
985
+ <button id="tb-upload" class="modern-tb-btn" title="Upload images">
986
+ {UPLOAD_SVG}<span class="tb-label">Upload</span>
987
+ </button>
988
+ <button id="tb-remove" class="modern-tb-btn" title="Remove selected image">
989
+ {REMOVE_SVG}<span class="tb-label">Remove</span>
990
+ </button>
991
+ <button id="tb-clear" class="modern-tb-btn" title="Clear all images">
992
+ {CLEAR_SVG}<span class="tb-label">Clear All</span>
993
+ </button>
994
+ <div class="tb-sep"></div>
995
+ <span id="tb-image-count" class="tb-info">No images</span>
996
+ </div>
997
+
998
+ <div class="app-main-row">
999
+ <div class="app-main-left">
1000
+ <div id="gallery-drop-zone">
1001
+ <div id="upload-prompt" class="upload-prompt-modern">
1002
+ <div id="upload-click-area" class="upload-click-area">
1003
+ <svg viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
1004
+ <rect x="8" y="14" width="64" height="52" rx="6" fill="none" stroke="#1E90FF" stroke-width="2" stroke-dasharray="4 3"/>
1005
+ <polygon points="12,62 30,40 42,50 54,34 68,62" fill="rgba(30,144,255,0.15)" stroke="#1E90FF" stroke-width="1.5"/>
1006
+ <circle cx="28" cy="30" r="6" fill="rgba(30,144,255,0.2)" stroke="#1E90FF" stroke-width="1.5"/>
1007
+ </svg>
1008
+ <span class="upload-main-text">Click or drag images here</span>
1009
+ <span class="upload-sub-text">Supports multiple images for reference-based editing and guided manipulation</span>
1010
+ </div>
1011
+ </div>
1012
+ <input id="custom-file-input" type="file" accept="image/*" multiple style="display:none;" />
1013
+ <div id="image-gallery-grid" class="image-gallery-grid" style="display:none;"></div>
1014
+ </div>
1015
+
1016
+ <div class="hint-bar">
1017
+ <b>Upload:</b> Click or drag to add images &nbsp;&middot;&nbsp;
1018
+ <b>Multi-image:</b> Upload multiple images for reference-based editing &nbsp;&middot;&nbsp;
1019
+ <kbd>Remove</kbd> deletes selected &nbsp;&middot;&nbsp;
1020
+ <kbd>Clear All</kbd> removes everything
1021
+ </div>
1022
+
1023
+ <div class="suggestions-section">
1024
+ <div class="suggestions-title">Quick Prompts</div>
1025
+ <div class="suggestions-wrap">
1026
+ <button class="suggestion-chip" onclick="window.__setPrompt('Transform the image into a dotted cartoon style.')">Cartoon Style</button>
1027
+ <button class="suggestion-chip" onclick="window.__setPrompt('Convert it to black and white.')">Black and White</button>
1028
+ <button class="suggestion-chip" onclick="window.__setPrompt('Add cinematic lighting with warm orange tones and film grain.')">Cinematic</button>
1029
+ <button class="suggestion-chip" onclick="window.__setPrompt('Transform into anime style illustration.')">Anime Style</button>
1030
+ <button class="suggestion-chip" onclick="window.__setPrompt('Apply oil painting effect with visible brush strokes.')">Oil Painting</button>
1031
+ <button class="suggestion-chip" onclick="window.__setPrompt('Enhance and upscale with more detail and clarity.')">Enhance</button>
1032
+ <button class="suggestion-chip" onclick="window.__setPrompt('Make it look like a watercolor painting with soft edges.')">Watercolor</button>
1033
+ <button class="suggestion-chip" onclick="window.__setPrompt('Add dramatic sunset sky and warm lighting.')">Sunset Glow</button>
1034
+ <button class="suggestion-chip" onclick="window.__setPrompt('Convert to detailed pencil sketch with cross-hatching and shading.')">Pencil Sketch</button>
1035
+ <button class="suggestion-chip" onclick="window.__setPrompt('Apply pop art style with bold colors and halftone patterns.')">Pop Art</button>
1036
+ <button class="suggestion-chip" onclick="window.__setPrompt('Apply a vintage retro film look with faded colors and light leaks.')">Vintage Retro</button>
1037
+ <button class="suggestion-chip" onclick="window.__setPrompt('Add neon glow effects with vibrant colors against a dark background.')">Neon Glow</button>
1038
+ <button class="suggestion-chip" onclick="window.__setPrompt('Convert to pixel art style with a retro 16-bit aesthetic.')">Pixel Art</button>
1039
+ <button class="suggestion-chip" onclick="window.__setPrompt('Simplify into a clean minimalist illustration with flat colors.')">Minimalist</button>
1040
+ <button class="suggestion-chip" onclick="window.__setPrompt('Convert to low poly 3D geometric art style.')">Low Poly 3D</button>
1041
+ <button class="suggestion-chip" onclick="window.__setPrompt('Transform into comic book style with bold outlines and cel shading.')">Comic Book</button>
1042
+ </div>
1043
+ </div>
1044
+
1045
+ <div class="examples-section">
1046
+ <div class="examples-title">Quick Examples</div>
1047
+ <div class="examples-scroll">
1048
+ {EXAMPLE_CARDS_HTML}
1049
+ </div>
1050
+ </div>
1051
+ </div>
1052
+
1053
+ <div class="app-main-right">
1054
+ <div class="panel-card">
1055
+ <div class="panel-card-title">Edit Instruction</div>
1056
+ <div class="panel-card-body">
1057
+ <label class="modern-label" for="custom-prompt-input">Prompt</label>
1058
+ <textarea id="custom-prompt-input" class="modern-textarea" rows="3" placeholder="e.g., transform into anime, upscale, change lighting..."></textarea>
1059
+ </div>
1060
+ </div>
1061
+
1062
+ <div style="padding:12px 20px;">
1063
+ <button id="custom-run-btn" class="btn-run">
1064
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 23c-3.6 0-8-2.69-8-7.5 0-3.5 3-6.5 4.5-8 .27-.27.75-.08.75.28v2.44c0 .42.5.63.72.28C12.28 7.5 13 3 13 1c0-.42.48-.64.8-.35C18 4.5 20 9 20 12c0 5.5-3.5 11-8 11z"/></svg>
1065
+ <span id="run-btn-label">Edit Image</span>
1066
+ </button>
1067
+ </div>
1068
+
1069
+ <div class="output-frame" style="flex:1">
1070
+ <div class="out-title">
1071
+ <span>Output</span>
1072
+ <span id="dl-btn-output" class="out-download-btn" title="Download">
1073
+ {DOWNLOAD_SVG} Save
1074
+ </span>
1075
+ </div>
1076
+ <div class="out-body" id="output-image-container">
1077
+ <div class="modern-loader" id="output-loader">
1078
+ <div class="loader-spinner"></div>
1079
+ <div class="loader-text">Processing image...</div>
1080
+ <div class="loader-bar-track"><div class="loader-bar-fill"></div></div>
1081
+ </div>
1082
+ <div class="out-placeholder" id="output-placeholder">Result will appear here</div>
1083
+ </div>
1084
+ </div>
1085
+
1086
+ <div class="settings-group">
1087
+ <div class="settings-group-title">Advanced Settings</div>
1088
+ <div class="settings-group-body">
1089
+ <div class="slider-row">
1090
+ <label>Seed</label>
1091
+ <input type="range" id="custom-seed" min="0" max="2147483647" step="1" value="0">
1092
+ <span class="slider-val" id="custom-seed-val">0</span>
1093
+ </div>
1094
+ <div class="checkbox-row">
1095
+ <input type="checkbox" id="custom-randomize" checked>
1096
+ <label for="custom-randomize">Randomize seed</label>
1097
+ </div>
1098
+ <div class="slider-row">
1099
+ <label>Guidance</label>
1100
+ <input type="range" id="custom-guidance" min="1" max="10" step="0.1" value="1.0">
1101
+ <span class="slider-val" id="custom-guidance-val">1.0</span>
1102
+ </div>
1103
+ <div class="slider-row">
1104
+ <label>Steps</label>
1105
+ <input type="range" id="custom-steps" min="1" max="50" step="1" value="4">
1106
+ <span class="slider-val" id="custom-steps-val">4</span>
1107
+ </div>
1108
+ </div>
1109
+ </div>
1110
+ </div>
1111
+ </div>
1112
+
1113
+ <div class="exp-note">
1114
+ Experimental Space for <a href="https://huggingface.co/FireRedTeam/FireRed-Image-Edit-1.1" target="_blank">FireRed-Image-Edit-1.1</a>
1115
+ &middot; Open on <a href="https://github.com/PRITHIVSAKTHIUR/FireRed-Image-Edit-1.0-Fast" target="_blank">GitHub</a>
1116
+ </div>
1117
+
1118
+ <div class="app-statusbar">
1119
+ <div class="sb-section" id="sb-image-count">No images uploaded</div>
1120
+ <div class="sb-section sb-fixed">Ready</div>
1121
+ </div>
1122
+ </div>
1123
+ """)
1124
+
1125
+ run_btn = gr.Button("Run", elem_id="gradio-run-btn")
1126
+
1127
+ demo.load(fn=None, js=gallery_js)
1128
+ demo.load(fn=None, js=wire_outputs_js)
1129
+
1130
+ run_btn.click(
1131
+ fn=infer,
1132
+ inputs=[hidden_images_b64, prompt, seed, randomize_seed, guidance_scale, steps],
1133
+ outputs=[result, seed],
1134
+ js=r"""(imgs, p, s, rs, gs, st) => {
1135
+ const images = window.__uploadedImages || [];
1136
+ const b64Array = images.map(img => img.b64);
1137
+ const imgsJson = JSON.stringify(b64Array);
1138
+ const promptEl = document.getElementById('custom-prompt-input');
1139
+ const promptVal = promptEl ? promptEl.value : p;
1140
+ return [imgsJson, promptVal, s, rs, gs, st];
1141
+ }""",
1142
+ )
1143
+
1144
+ example_load_btn.click(
1145
+ fn=load_example_data,
1146
+ inputs=[example_idx],
1147
+ outputs=[example_result],
1148
+ queue=False,
1149
+ )
1150
+
1151
+ if __name__ == "__main__":
1152
+ demo.queue(max_size=30).launch(
1153
+ css=css,
1154
+ mcp_server=True,
1155
+ ssr_mode=False,
1156
+ show_error=True,
1157
+ allowed_paths=["examples"],
1158
+ )
examples/1.jpg ADDED
examples/10.jpg ADDED

Git LFS Details

  • SHA256: 31fd1d52ee41559dcda55e304aef19df2767ff1c76295480d838210868fb63a5
  • Pointer size: 131 Bytes
  • Size of remote file: 166 kB
examples/11.png ADDED

Git LFS Details

  • SHA256: 1488547f1f4ebb91e5ec06a9bad862268984b714e0286ffed1ed02deb9d128f5
  • Pointer size: 131 Bytes
  • Size of remote file: 304 kB
examples/2.jpg ADDED
examples/3.jpeg ADDED
examples/4.jpg ADDED
examples/5.jpg ADDED
examples/6.jpg ADDED

Git LFS Details

  • SHA256: 639a45e4efb19a34d5db474ef1e5219c538ba7cfabfa306dc401a3c2cba156da
  • Pointer size: 131 Bytes
  • Size of remote file: 266 kB
examples/7.webp ADDED

Git LFS Details

  • SHA256: b72dcded9b67fb7a6c94b81c4dda447cfc2ced617875d3bcea2d89c76fe8f092
  • Pointer size: 131 Bytes
  • Size of remote file: 110 kB
examples/8.jpg ADDED

Git LFS Details

  • SHA256: c17bc45b822ba2b5b74a8cf29d0f4eaeda638a9be4bd27838dc7012f1012a9ad
  • Pointer size: 131 Bytes
  • Size of remote file: 326 kB
examples/9.png ADDED

Git LFS Details

  • SHA256: 63e08f439fb83b39b3405d389b8f06481e39e99c9549990f5147f8e9e0c20383
  • Pointer size: 131 Bytes
  • Size of remote file: 199 kB
pre-requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ pip>=23.0.0
qwenimage/__init__.py ADDED
File without changes
qwenimage/pipeline_qwenimage_edit_plus.py ADDED
@@ -0,0 +1,891 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Qwen-Image Team and The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import inspect
16
+ import math
17
+ from typing import Any, Callable, Dict, List, Optional, Union
18
+
19
+ import numpy as np
20
+ import torch
21
+ from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, Qwen2VLProcessor
22
+
23
+ from diffusers.image_processor import PipelineImageInput, VaeImageProcessor
24
+ from diffusers.loaders import QwenImageLoraLoaderMixin
25
+ from diffusers.models import AutoencoderKLQwenImage, QwenImageTransformer2DModel
26
+ from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
27
+ from diffusers.utils import is_torch_xla_available, logging, replace_example_docstring
28
+ from diffusers.utils.torch_utils import randn_tensor
29
+ from diffusers.pipelines.pipeline_utils import DiffusionPipeline
30
+ from diffusers.pipelines.qwenimage.pipeline_output import QwenImagePipelineOutput
31
+
32
+
33
+ if is_torch_xla_available():
34
+ import torch_xla.core.xla_model as xm
35
+
36
+ XLA_AVAILABLE = True
37
+ else:
38
+ XLA_AVAILABLE = False
39
+
40
+
41
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
42
+
43
+ EXAMPLE_DOC_STRING = """
44
+ Examples:
45
+ ```py
46
+ >>> import torch
47
+ >>> from PIL import Image
48
+ >>> from diffusers import QwenImageEditPlusPipeline
49
+ >>> from diffusers.utils import load_image
50
+
51
+ >>> pipe = QwenImageEditPlusPipeline.from_pretrained("Qwen/Qwen-Image-Edit-2509", torch_dtype=torch.bfloat16)
52
+ >>> pipe.to("cuda")
53
+ >>> image = load_image(
54
+ ... "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/yarn-art-pikachu.png"
55
+ ... ).convert("RGB")
56
+ >>> prompt = (
57
+ ... "Make Pikachu hold a sign that says 'Qwen Edit is awesome', yarn art style, detailed, vibrant colors"
58
+ ... )
59
+ >>> # Depending on the variant being used, the pipeline call will slightly vary.
60
+ >>> # Refer to the pipeline documentation for more details.
61
+ >>> image = pipe(image, prompt, num_inference_steps=50).images[0]
62
+ >>> image.save("qwenimage_edit_plus.png")
63
+ ```
64
+ """
65
+
66
+ CONDITION_IMAGE_SIZE = 384 * 384
67
+ VAE_IMAGE_SIZE = 1024 * 1024
68
+
69
+
70
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.calculate_shift
71
+ def calculate_shift(
72
+ image_seq_len,
73
+ base_seq_len: int = 256,
74
+ max_seq_len: int = 4096,
75
+ base_shift: float = 0.5,
76
+ max_shift: float = 1.15,
77
+ ):
78
+ m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
79
+ b = base_shift - m * base_seq_len
80
+ mu = image_seq_len * m + b
81
+ return mu
82
+
83
+
84
+ # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.retrieve_timesteps
85
+ def retrieve_timesteps(
86
+ scheduler,
87
+ num_inference_steps: Optional[int] = None,
88
+ device: Optional[Union[str, torch.device]] = None,
89
+ timesteps: Optional[List[int]] = None,
90
+ sigmas: Optional[List[float]] = None,
91
+ **kwargs,
92
+ ):
93
+ r"""
94
+ Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
95
+ custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
96
+
97
+ Args:
98
+ scheduler (`SchedulerMixin`):
99
+ The scheduler to get timesteps from.
100
+ num_inference_steps (`int`):
101
+ The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
102
+ must be `None`.
103
+ device (`str` or `torch.device`, *optional*):
104
+ The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
105
+ timesteps (`List[int]`, *optional*):
106
+ Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
107
+ `num_inference_steps` and `sigmas` must be `None`.
108
+ sigmas (`List[float]`, *optional*):
109
+ Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
110
+ `num_inference_steps` and `timesteps` must be `None`.
111
+
112
+ Returns:
113
+ `Tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
114
+ second element is the number of inference steps.
115
+ """
116
+ if timesteps is not None and sigmas is not None:
117
+ raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")
118
+ if timesteps is not None:
119
+ accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
120
+ if not accepts_timesteps:
121
+ raise ValueError(
122
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
123
+ f" timestep schedules. Please check whether you are using the correct scheduler."
124
+ )
125
+ scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
126
+ timesteps = scheduler.timesteps
127
+ num_inference_steps = len(timesteps)
128
+ elif sigmas is not None:
129
+ accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
130
+ if not accept_sigmas:
131
+ raise ValueError(
132
+ f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
133
+ f" sigmas schedules. Please check whether you are using the correct scheduler."
134
+ )
135
+ scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
136
+ timesteps = scheduler.timesteps
137
+ num_inference_steps = len(timesteps)
138
+ else:
139
+ scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
140
+ timesteps = scheduler.timesteps
141
+ return timesteps, num_inference_steps
142
+
143
+
144
+ # Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents
145
+ def retrieve_latents(
146
+ encoder_output: torch.Tensor, generator: Optional[torch.Generator] = None, sample_mode: str = "sample"
147
+ ):
148
+ if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
149
+ return encoder_output.latent_dist.sample(generator)
150
+ elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
151
+ return encoder_output.latent_dist.mode()
152
+ elif hasattr(encoder_output, "latents"):
153
+ return encoder_output.latents
154
+ else:
155
+ raise AttributeError("Could not access latents of provided encoder_output")
156
+
157
+
158
+ def calculate_dimensions(target_area, ratio):
159
+ width = math.sqrt(target_area * ratio)
160
+ height = width / ratio
161
+
162
+ width = round(width / 32) * 32
163
+ height = round(height / 32) * 32
164
+
165
+ return width, height
166
+
167
+
168
+ class QwenImageEditPlusPipeline(DiffusionPipeline, QwenImageLoraLoaderMixin):
169
+ r"""
170
+ The Qwen-Image-Edit pipeline for image editing.
171
+
172
+ Args:
173
+ transformer ([`QwenImageTransformer2DModel`]):
174
+ Conditional Transformer (MMDiT) architecture to denoise the encoded image latents.
175
+ scheduler ([`FlowMatchEulerDiscreteScheduler`]):
176
+ A scheduler to be used in combination with `transformer` to denoise the encoded image latents.
177
+ vae ([`AutoencoderKL`]):
178
+ Variational Auto-Encoder (VAE) Model to encode and decode images to and from latent representations.
179
+ text_encoder ([`Qwen2.5-VL-7B-Instruct`]):
180
+ [Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct), specifically the
181
+ [Qwen2.5-VL-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct) variant.
182
+ tokenizer (`QwenTokenizer`):
183
+ Tokenizer of class
184
+ [CLIPTokenizer](https://huggingface.co/docs/transformers/en/model_doc/clip#transformers.CLIPTokenizer).
185
+ """
186
+
187
+ model_cpu_offload_seq = "text_encoder->transformer->vae"
188
+ _callback_tensor_inputs = ["latents", "prompt_embeds"]
189
+
190
+ def __init__(
191
+ self,
192
+ scheduler: FlowMatchEulerDiscreteScheduler,
193
+ vae: AutoencoderKLQwenImage,
194
+ text_encoder: Qwen2_5_VLForConditionalGeneration,
195
+ tokenizer: Qwen2Tokenizer,
196
+ processor: Qwen2VLProcessor,
197
+ transformer: QwenImageTransformer2DModel,
198
+ ):
199
+ super().__init__()
200
+
201
+ self.register_modules(
202
+ vae=vae,
203
+ text_encoder=text_encoder,
204
+ tokenizer=tokenizer,
205
+ processor=processor,
206
+ transformer=transformer,
207
+ scheduler=scheduler,
208
+ )
209
+ self.vae_scale_factor = 2 ** len(self.vae.temperal_downsample) if getattr(self, "vae", None) else 8
210
+ self.latent_channels = self.vae.config.z_dim if getattr(self, "vae", None) else 16
211
+ # QwenImage latents are turned into 2x2 patches and packed. This means the latent width and height has to be divisible
212
+ # by the patch size. So the vae scale factor is multiplied by the patch size to account for this
213
+ self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * 2)
214
+ self.tokenizer_max_length = 1024
215
+
216
+ self.prompt_template_encode = "<|im_start|>system\nDescribe the key features of the input image (color, shape, size, texture, objects, background), then explain how the user's text instruction should alter or modify the image. Generate a new image that meets the user's requirements while maintaining consistency with the original input where appropriate.<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n<|im_start|>assistant\n"
217
+ self.prompt_template_encode_start_idx = 64
218
+ self.default_sample_size = 128
219
+
220
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.QwenImagePipeline._extract_masked_hidden
221
+ def _extract_masked_hidden(self, hidden_states: torch.Tensor, mask: torch.Tensor):
222
+ bool_mask = mask.bool()
223
+ valid_lengths = bool_mask.sum(dim=1)
224
+ selected = hidden_states[bool_mask]
225
+ split_result = torch.split(selected, valid_lengths.tolist(), dim=0)
226
+
227
+ return split_result
228
+
229
+ def _get_qwen_prompt_embeds(
230
+ self,
231
+ prompt: Union[str, List[str]] = None,
232
+ image: Optional[torch.Tensor] = None,
233
+ device: Optional[torch.device] = None,
234
+ dtype: Optional[torch.dtype] = None,
235
+ ):
236
+ device = device or self._execution_device
237
+ dtype = dtype or self.text_encoder.dtype
238
+
239
+ prompt = [prompt] if isinstance(prompt, str) else prompt
240
+ img_prompt_template = "Picture {}: <|vision_start|><|image_pad|><|vision_end|>"
241
+ if isinstance(image, list):
242
+ base_img_prompt = ""
243
+ for i, img in enumerate(image):
244
+ base_img_prompt += img_prompt_template.format(i + 1)
245
+ elif image is not None:
246
+ base_img_prompt = img_prompt_template.format(1)
247
+ else:
248
+ base_img_prompt = ""
249
+
250
+ template = self.prompt_template_encode
251
+
252
+ drop_idx = self.prompt_template_encode_start_idx
253
+ txt = [template.format(base_img_prompt + e) for e in prompt]
254
+
255
+ model_inputs = self.processor(
256
+ text=txt,
257
+ images=image,
258
+ padding=True,
259
+ return_tensors="pt",
260
+ ).to(device)
261
+
262
+ outputs = self.text_encoder(
263
+ input_ids=model_inputs.input_ids,
264
+ attention_mask=model_inputs.attention_mask,
265
+ pixel_values=model_inputs.pixel_values,
266
+ image_grid_thw=model_inputs.image_grid_thw,
267
+ output_hidden_states=True,
268
+ )
269
+
270
+ hidden_states = outputs.hidden_states[-1]
271
+ split_hidden_states = self._extract_masked_hidden(hidden_states, model_inputs.attention_mask)
272
+ split_hidden_states = [e[drop_idx:] for e in split_hidden_states]
273
+ attn_mask_list = [torch.ones(e.size(0), dtype=torch.long, device=e.device) for e in split_hidden_states]
274
+ max_seq_len = max([e.size(0) for e in split_hidden_states])
275
+ prompt_embeds = torch.stack(
276
+ [torch.cat([u, u.new_zeros(max_seq_len - u.size(0), u.size(1))]) for u in split_hidden_states]
277
+ )
278
+ encoder_attention_mask = torch.stack(
279
+ [torch.cat([u, u.new_zeros(max_seq_len - u.size(0))]) for u in attn_mask_list]
280
+ )
281
+
282
+ prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
283
+
284
+ return prompt_embeds, encoder_attention_mask
285
+
286
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage_edit.QwenImageEditPipeline.encode_prompt
287
+ def encode_prompt(
288
+ self,
289
+ prompt: Union[str, List[str]],
290
+ image: Optional[torch.Tensor] = None,
291
+ device: Optional[torch.device] = None,
292
+ num_images_per_prompt: int = 1,
293
+ prompt_embeds: Optional[torch.Tensor] = None,
294
+ prompt_embeds_mask: Optional[torch.Tensor] = None,
295
+ max_sequence_length: int = 1024,
296
+ ):
297
+ r"""
298
+
299
+ Args:
300
+ prompt (`str` or `List[str]`, *optional*):
301
+ prompt to be encoded
302
+ image (`torch.Tensor`, *optional*):
303
+ image to be encoded
304
+ device: (`torch.device`):
305
+ torch device
306
+ num_images_per_prompt (`int`):
307
+ number of images that should be generated per prompt
308
+ prompt_embeds (`torch.Tensor`, *optional*):
309
+ Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
310
+ provided, text embeddings will be generated from `prompt` input argument.
311
+ """
312
+ device = device or self._execution_device
313
+
314
+ prompt = [prompt] if isinstance(prompt, str) else prompt
315
+ batch_size = len(prompt) if prompt_embeds is None else prompt_embeds.shape[0]
316
+
317
+ if prompt_embeds is None:
318
+ prompt_embeds, prompt_embeds_mask = self._get_qwen_prompt_embeds(prompt, image, device)
319
+
320
+ _, seq_len, _ = prompt_embeds.shape
321
+ prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)
322
+ prompt_embeds = prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)
323
+ prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_images_per_prompt, 1)
324
+ prompt_embeds_mask = prompt_embeds_mask.view(batch_size * num_images_per_prompt, seq_len)
325
+
326
+ return prompt_embeds, prompt_embeds_mask
327
+
328
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage_edit.QwenImageEditPipeline.check_inputs
329
+ def check_inputs(
330
+ self,
331
+ prompt,
332
+ height,
333
+ width,
334
+ negative_prompt=None,
335
+ prompt_embeds=None,
336
+ negative_prompt_embeds=None,
337
+ prompt_embeds_mask=None,
338
+ negative_prompt_embeds_mask=None,
339
+ callback_on_step_end_tensor_inputs=None,
340
+ max_sequence_length=None,
341
+ ):
342
+ if height % (self.vae_scale_factor * 2) != 0 or width % (self.vae_scale_factor * 2) != 0:
343
+ logger.warning(
344
+ f"`height` and `width` have to be divisible by {self.vae_scale_factor * 2} but are {height} and {width}. Dimensions will be resized accordingly"
345
+ )
346
+
347
+ if callback_on_step_end_tensor_inputs is not None and not all(
348
+ k in self._callback_tensor_inputs for k in callback_on_step_end_tensor_inputs
349
+ ):
350
+ raise ValueError(
351
+ f"`callback_on_step_end_tensor_inputs` has to be in {self._callback_tensor_inputs}, but found {[k for k in callback_on_step_end_tensor_inputs if k not in self._callback_tensor_inputs]}"
352
+ )
353
+
354
+ if prompt is not None and prompt_embeds is not None:
355
+ raise ValueError(
356
+ f"Cannot forward both `prompt`: {prompt} and `prompt_embeds`: {prompt_embeds}. Please make sure to"
357
+ " only forward one of the two."
358
+ )
359
+ elif prompt is None and prompt_embeds is None:
360
+ raise ValueError(
361
+ "Provide either `prompt` or `prompt_embeds`. Cannot leave both `prompt` and `prompt_embeds` undefined."
362
+ )
363
+ elif prompt is not None and (not isinstance(prompt, str) and not isinstance(prompt, list)):
364
+ raise ValueError(f"`prompt` has to be of type `str` or `list` but is {type(prompt)}")
365
+
366
+ if negative_prompt is not None and negative_prompt_embeds is not None:
367
+ raise ValueError(
368
+ f"Cannot forward both `negative_prompt`: {negative_prompt} and `negative_prompt_embeds`:"
369
+ f" {negative_prompt_embeds}. Please make sure to only forward one of the two."
370
+ )
371
+
372
+ if prompt_embeds is not None and prompt_embeds_mask is None:
373
+ raise ValueError(
374
+ "If `prompt_embeds` are provided, `prompt_embeds_mask` also have to be passed. Make sure to generate `prompt_embeds_mask` from the same text encoder that was used to generate `prompt_embeds`."
375
+ )
376
+ if negative_prompt_embeds is not None and negative_prompt_embeds_mask is None:
377
+ raise ValueError(
378
+ "If `negative_prompt_embeds` are provided, `negative_prompt_embeds_mask` also have to be passed. Make sure to generate `negative_prompt_embeds_mask` from the same text encoder that was used to generate `negative_prompt_embeds`."
379
+ )
380
+
381
+ if max_sequence_length is not None and max_sequence_length > 1024:
382
+ raise ValueError(f"`max_sequence_length` cannot be greater than 1024 but is {max_sequence_length}")
383
+
384
+ @staticmethod
385
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.QwenImagePipeline._pack_latents
386
+ def _pack_latents(latents, batch_size, num_channels_latents, height, width):
387
+ latents = latents.view(batch_size, num_channels_latents, height // 2, 2, width // 2, 2)
388
+ latents = latents.permute(0, 2, 4, 1, 3, 5)
389
+ latents = latents.reshape(batch_size, (height // 2) * (width // 2), num_channels_latents * 4)
390
+
391
+ return latents
392
+
393
+ @staticmethod
394
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage.QwenImagePipeline._unpack_latents
395
+ def _unpack_latents(latents, height, width, vae_scale_factor):
396
+ batch_size, num_patches, channels = latents.shape
397
+
398
+ # VAE applies 8x compression on images but we must also account for packing which requires
399
+ # latent height and width to be divisible by 2.
400
+ height = 2 * (int(height) // (vae_scale_factor * 2))
401
+ width = 2 * (int(width) // (vae_scale_factor * 2))
402
+
403
+ latents = latents.view(batch_size, height // 2, width // 2, channels // 4, 2, 2)
404
+ latents = latents.permute(0, 3, 1, 4, 2, 5)
405
+
406
+ latents = latents.reshape(batch_size, channels // (2 * 2), 1, height, width)
407
+
408
+ return latents
409
+
410
+ # Copied from diffusers.pipelines.qwenimage.pipeline_qwenimage_edit.QwenImageEditPipeline._encode_vae_image
411
+ def _encode_vae_image(self, image: torch.Tensor, generator: torch.Generator):
412
+ if isinstance(generator, list):
413
+ image_latents = [
414
+ retrieve_latents(self.vae.encode(image[i : i + 1]), generator=generator[i], sample_mode="argmax")
415
+ for i in range(image.shape[0])
416
+ ]
417
+ image_latents = torch.cat(image_latents, dim=0)
418
+ else:
419
+ image_latents = retrieve_latents(self.vae.encode(image), generator=generator, sample_mode="argmax")
420
+ latents_mean = (
421
+ torch.tensor(self.vae.config.latents_mean)
422
+ .view(1, self.latent_channels, 1, 1, 1)
423
+ .to(image_latents.device, image_latents.dtype)
424
+ )
425
+ latents_std = (
426
+ torch.tensor(self.vae.config.latents_std)
427
+ .view(1, self.latent_channels, 1, 1, 1)
428
+ .to(image_latents.device, image_latents.dtype)
429
+ )
430
+ image_latents = (image_latents - latents_mean) / latents_std
431
+
432
+ return image_latents
433
+
434
+ def prepare_latents(
435
+ self,
436
+ images,
437
+ batch_size,
438
+ num_channels_latents,
439
+ height,
440
+ width,
441
+ dtype,
442
+ device,
443
+ generator,
444
+ latents=None,
445
+ ):
446
+ # VAE applies 8x compression on images but we must also account for packing which requires
447
+ # latent height and width to be divisible by 2.
448
+ height = 2 * (int(height) // (self.vae_scale_factor * 2))
449
+ width = 2 * (int(width) // (self.vae_scale_factor * 2))
450
+
451
+ shape = (batch_size, 1, num_channels_latents, height, width)
452
+
453
+ image_latents = None
454
+ if images is not None:
455
+ if not isinstance(images, list):
456
+ images = [images]
457
+ all_image_latents = []
458
+ for image in images:
459
+ image = image.to(device=device, dtype=dtype)
460
+ if image.shape[1] != self.latent_channels:
461
+ image_latents = self._encode_vae_image(image=image, generator=generator)
462
+ else:
463
+ image_latents = image
464
+ if batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] == 0:
465
+ # expand init_latents for batch_size
466
+ additional_image_per_prompt = batch_size // image_latents.shape[0]
467
+ image_latents = torch.cat([image_latents] * additional_image_per_prompt, dim=0)
468
+ elif batch_size > image_latents.shape[0] and batch_size % image_latents.shape[0] != 0:
469
+ raise ValueError(
470
+ f"Cannot duplicate `image` of batch size {image_latents.shape[0]} to {batch_size} text prompts."
471
+ )
472
+ else:
473
+ image_latents = torch.cat([image_latents], dim=0)
474
+
475
+ image_latent_height, image_latent_width = image_latents.shape[3:]
476
+ image_latents = self._pack_latents(
477
+ image_latents, batch_size, num_channels_latents, image_latent_height, image_latent_width
478
+ )
479
+ all_image_latents.append(image_latents)
480
+ image_latents = torch.cat(all_image_latents, dim=1)
481
+
482
+ if isinstance(generator, list) and len(generator) != batch_size:
483
+ raise ValueError(
484
+ f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
485
+ f" size of {batch_size}. Make sure the batch size matches the length of the generators."
486
+ )
487
+ if latents is None:
488
+ latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
489
+ latents = self._pack_latents(latents, batch_size, num_channels_latents, height, width)
490
+ else:
491
+ latents = latents.to(device=device, dtype=dtype)
492
+
493
+ return latents, image_latents
494
+
495
+ @property
496
+ def guidance_scale(self):
497
+ return self._guidance_scale
498
+
499
+ @property
500
+ def attention_kwargs(self):
501
+ return self._attention_kwargs
502
+
503
+ @property
504
+ def num_timesteps(self):
505
+ return self._num_timesteps
506
+
507
+ @property
508
+ def current_timestep(self):
509
+ return self._current_timestep
510
+
511
+ @property
512
+ def interrupt(self):
513
+ return self._interrupt
514
+
515
+ @torch.no_grad()
516
+ @replace_example_docstring(EXAMPLE_DOC_STRING)
517
+ def __call__(
518
+ self,
519
+ image: Optional[PipelineImageInput] = None,
520
+ prompt: Union[str, List[str]] = None,
521
+ negative_prompt: Union[str, List[str]] = None,
522
+ true_cfg_scale: float = 4.0,
523
+ height: Optional[int] = None,
524
+ width: Optional[int] = None,
525
+ num_inference_steps: int = 50,
526
+ sigmas: Optional[List[float]] = None,
527
+ guidance_scale: Optional[float] = None,
528
+ num_images_per_prompt: int = 1,
529
+ generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
530
+ latents: Optional[torch.Tensor] = None,
531
+ prompt_embeds: Optional[torch.Tensor] = None,
532
+ prompt_embeds_mask: Optional[torch.Tensor] = None,
533
+ negative_prompt_embeds: Optional[torch.Tensor] = None,
534
+ negative_prompt_embeds_mask: Optional[torch.Tensor] = None,
535
+ output_type: Optional[str] = "pil",
536
+ return_dict: bool = True,
537
+ attention_kwargs: Optional[Dict[str, Any]] = None,
538
+ callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = None,
539
+ callback_on_step_end_tensor_inputs: List[str] = ["latents"],
540
+ max_sequence_length: int = 512,
541
+ ):
542
+ r"""
543
+ Function invoked when calling the pipeline for generation.
544
+
545
+ Args:
546
+ image (`torch.Tensor`, `PIL.Image.Image`, `np.ndarray`, `List[torch.Tensor]`, `List[PIL.Image.Image]`, or `List[np.ndarray]`):
547
+ `Image`, numpy array or tensor representing an image batch to be used as the starting point. For both
548
+ numpy array and pytorch tensor, the expected value range is between `[0, 1]` If it's a tensor or a list
549
+ or tensors, the expected shape should be `(B, C, H, W)` or `(C, H, W)`. If it is a numpy array or a
550
+ list of arrays, the expected shape should be `(B, H, W, C)` or `(H, W, C)` It can also accept image
551
+ latents as `image`, but if passing latents directly it is not encoded again.
552
+ prompt (`str` or `List[str]`, *optional*):
553
+ The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`.
554
+ instead.
555
+ negative_prompt (`str` or `List[str]`, *optional*):
556
+ The prompt or prompts not to guide the image generation. If not defined, one has to pass
557
+ `negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `true_cfg_scale` is
558
+ not greater than `1`).
559
+ true_cfg_scale (`float`, *optional*, defaults to 1.0):
560
+ true_cfg_scale (`float`, *optional*, defaults to 1.0): Guidance scale as defined in [Classifier-Free
561
+ Diffusion Guidance](https://huggingface.co/papers/2207.12598). `true_cfg_scale` is defined as `w` of
562
+ equation 2. of [Imagen Paper](https://huggingface.co/papers/2205.11487). Classifier-free guidance is
563
+ enabled by setting `true_cfg_scale > 1` and a provided `negative_prompt`. Higher guidance scale
564
+ encourages to generate images that are closely linked to the text `prompt`, usually at the expense of
565
+ lower image quality.
566
+ height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
567
+ The height in pixels of the generated image. This is set to 1024 by default for the best results.
568
+ width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
569
+ The width in pixels of the generated image. This is set to 1024 by default for the best results.
570
+ num_inference_steps (`int`, *optional*, defaults to 50):
571
+ The number of denoising steps. More denoising steps usually lead to a higher quality image at the
572
+ expense of slower inference.
573
+ sigmas (`List[float]`, *optional*):
574
+ Custom sigmas to use for the denoising process with schedulers which support a `sigmas` argument in
575
+ their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is passed
576
+ will be used.
577
+ guidance_scale (`float`, *optional*, defaults to None):
578
+ A guidance scale value for guidance distilled models. Unlike the traditional classifier-free guidance
579
+ where the guidance scale is applied during inference through noise prediction rescaling, guidance
580
+ distilled models take the guidance scale directly as an input parameter during forward pass. Guidance
581
+ scale is enabled by setting `guidance_scale > 1`. Higher guidance scale encourages to generate images
582
+ that are closely linked to the text `prompt`, usually at the expense of lower image quality. This
583
+ parameter in the pipeline is there to support future guidance-distilled models when they come up. It is
584
+ ignored when not using guidance distilled models. To enable traditional classifier-free guidance,
585
+ please pass `true_cfg_scale > 1.0` and `negative_prompt` (even an empty negative prompt like " " should
586
+ enable classifier-free guidance computations).
587
+ num_images_per_prompt (`int`, *optional*, defaults to 1):
588
+ The number of images to generate per prompt.
589
+ generator (`torch.Generator` or `List[torch.Generator]`, *optional*):
590
+ One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)
591
+ to make generation deterministic.
592
+ latents (`torch.Tensor`, *optional*):
593
+ Pre-generated noisy latents, sampled from a Gaussian distribution, to be used as inputs for image
594
+ generation. Can be used to tweak the same generation with different prompts. If not provided, a latents
595
+ tensor will be generated by sampling using the supplied random `generator`.
596
+ prompt_embeds (`torch.Tensor`, *optional*):
597
+ Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
598
+ provided, text embeddings will be generated from `prompt` input argument.
599
+ negative_prompt_embeds (`torch.Tensor`, *optional*):
600
+ Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt
601
+ weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input
602
+ argument.
603
+ output_type (`str`, *optional*, defaults to `"pil"`):
604
+ The output format of the generate image. Choose between
605
+ [PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`.
606
+ return_dict (`bool`, *optional*, defaults to `True`):
607
+ Whether or not to return a [`~pipelines.qwenimage.QwenImagePipelineOutput`] instead of a plain tuple.
608
+ attention_kwargs (`dict`, *optional*):
609
+ A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under
610
+ `self.processor` in
611
+ [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py).
612
+ callback_on_step_end (`Callable`, *optional*):
613
+ A function that calls at the end of each denoising steps during the inference. The function is called
614
+ with the following arguments: `callback_on_step_end(self: DiffusionPipeline, step: int, timestep: int,
615
+ callback_kwargs: Dict)`. `callback_kwargs` will include a list of all tensors as specified by
616
+ `callback_on_step_end_tensor_inputs`.
617
+ callback_on_step_end_tensor_inputs (`List`, *optional*):
618
+ The list of tensor inputs for the `callback_on_step_end` function. The tensors specified in the list
619
+ will be passed as `callback_kwargs` argument. You will only be able to include variables listed in the
620
+ `._callback_tensor_inputs` attribute of your pipeline class.
621
+ max_sequence_length (`int` defaults to 512): Maximum sequence length to use with the `prompt`.
622
+
623
+ Examples:
624
+
625
+ Returns:
626
+ [`~pipelines.qwenimage.QwenImagePipelineOutput`] or `tuple`:
627
+ [`~pipelines.qwenimage.QwenImagePipelineOutput`] if `return_dict` is True, otherwise a `tuple`. When
628
+ returning a tuple, the first element is a list with the generated images.
629
+ """
630
+ image_size = image[-1].size if isinstance(image, list) else image.size
631
+ calculated_width, calculated_height = calculate_dimensions(1024 * 1024, image_size[0] / image_size[1])
632
+ height = height or calculated_height
633
+ width = width or calculated_width
634
+
635
+ multiple_of = self.vae_scale_factor * 2
636
+ width = width // multiple_of * multiple_of
637
+ height = height // multiple_of * multiple_of
638
+
639
+ # 1. Check inputs. Raise error if not correct
640
+ self.check_inputs(
641
+ prompt,
642
+ height,
643
+ width,
644
+ negative_prompt=negative_prompt,
645
+ prompt_embeds=prompt_embeds,
646
+ negative_prompt_embeds=negative_prompt_embeds,
647
+ prompt_embeds_mask=prompt_embeds_mask,
648
+ negative_prompt_embeds_mask=negative_prompt_embeds_mask,
649
+ callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs,
650
+ max_sequence_length=max_sequence_length,
651
+ )
652
+
653
+ self._guidance_scale = guidance_scale
654
+ self._attention_kwargs = attention_kwargs
655
+ self._current_timestep = None
656
+ self._interrupt = False
657
+
658
+ # 2. Define call parameters
659
+ if prompt is not None and isinstance(prompt, str):
660
+ batch_size = 1
661
+ elif prompt is not None and isinstance(prompt, list):
662
+ batch_size = len(prompt)
663
+ else:
664
+ batch_size = prompt_embeds.shape[0]
665
+
666
+ device = self._execution_device
667
+ # 3. Preprocess image
668
+ if image is not None and not (isinstance(image, torch.Tensor) and image.size(1) == self.latent_channels):
669
+ if not isinstance(image, list):
670
+ image = [image]
671
+ condition_image_sizes = []
672
+ condition_images = []
673
+ vae_image_sizes = []
674
+ vae_images = []
675
+ for img in image:
676
+ image_width, image_height = img.size
677
+ condition_width, condition_height = calculate_dimensions(
678
+ CONDITION_IMAGE_SIZE, image_width / image_height
679
+ )
680
+ vae_width, vae_height = calculate_dimensions(VAE_IMAGE_SIZE, image_width / image_height)
681
+ condition_image_sizes.append((condition_width, condition_height))
682
+ vae_image_sizes.append((vae_width, vae_height))
683
+ condition_images.append(self.image_processor.resize(img, condition_height, condition_width))
684
+ vae_images.append(self.image_processor.preprocess(img, vae_height, vae_width).unsqueeze(2))
685
+
686
+ has_neg_prompt = negative_prompt is not None or (
687
+ negative_prompt_embeds is not None and negative_prompt_embeds_mask is not None
688
+ )
689
+
690
+ if true_cfg_scale > 1 and not has_neg_prompt:
691
+ logger.warning(
692
+ f"true_cfg_scale is passed as {true_cfg_scale}, but classifier-free guidance is not enabled since no negative_prompt is provided."
693
+ )
694
+ elif true_cfg_scale <= 1 and has_neg_prompt:
695
+ logger.warning(
696
+ " negative_prompt is passed but classifier-free guidance is not enabled since true_cfg_scale <= 1"
697
+ )
698
+
699
+ do_true_cfg = true_cfg_scale > 1 and has_neg_prompt
700
+ prompt_embeds, prompt_embeds_mask = self.encode_prompt(
701
+ image=condition_images,
702
+ prompt=prompt,
703
+ prompt_embeds=prompt_embeds,
704
+ prompt_embeds_mask=prompt_embeds_mask,
705
+ device=device,
706
+ num_images_per_prompt=num_images_per_prompt,
707
+ max_sequence_length=max_sequence_length,
708
+ )
709
+ if do_true_cfg:
710
+ negative_prompt_embeds, negative_prompt_embeds_mask = self.encode_prompt(
711
+ image=condition_images,
712
+ prompt=negative_prompt,
713
+ prompt_embeds=negative_prompt_embeds,
714
+ prompt_embeds_mask=negative_prompt_embeds_mask,
715
+ device=device,
716
+ num_images_per_prompt=num_images_per_prompt,
717
+ max_sequence_length=max_sequence_length,
718
+ )
719
+
720
+ # 4. Prepare latent variables
721
+ num_channels_latents = self.transformer.config.in_channels // 4
722
+ latents, image_latents = self.prepare_latents(
723
+ vae_images,
724
+ batch_size * num_images_per_prompt,
725
+ num_channels_latents,
726
+ height,
727
+ width,
728
+ prompt_embeds.dtype,
729
+ device,
730
+ generator,
731
+ latents,
732
+ )
733
+ img_shapes = [
734
+ [
735
+ (1, height // self.vae_scale_factor // 2, width // self.vae_scale_factor // 2),
736
+ *[
737
+ (1, vae_height // self.vae_scale_factor // 2, vae_width // self.vae_scale_factor // 2)
738
+ for vae_width, vae_height in vae_image_sizes
739
+ ],
740
+ ]
741
+ ] * batch_size
742
+
743
+ # 5. Prepare timesteps
744
+ sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas
745
+ image_seq_len = latents.shape[1]
746
+ mu = calculate_shift(
747
+ image_seq_len,
748
+ self.scheduler.config.get("base_image_seq_len", 256),
749
+ self.scheduler.config.get("max_image_seq_len", 4096),
750
+ self.scheduler.config.get("base_shift", 0.5),
751
+ self.scheduler.config.get("max_shift", 1.15),
752
+ )
753
+ timesteps, num_inference_steps = retrieve_timesteps(
754
+ self.scheduler,
755
+ num_inference_steps,
756
+ device,
757
+ sigmas=sigmas,
758
+ mu=mu,
759
+ )
760
+ num_warmup_steps = max(len(timesteps) - num_inference_steps * self.scheduler.order, 0)
761
+ self._num_timesteps = len(timesteps)
762
+
763
+ # handle guidance
764
+ if self.transformer.config.guidance_embeds and guidance_scale is None:
765
+ raise ValueError("guidance_scale is required for guidance-distilled model.")
766
+ elif self.transformer.config.guidance_embeds:
767
+ guidance = torch.full([1], guidance_scale, device=device, dtype=torch.float32)
768
+ guidance = guidance.expand(latents.shape[0])
769
+ elif not self.transformer.config.guidance_embeds and guidance_scale is not None:
770
+ logger.warning(
771
+ f"guidance_scale is passed as {guidance_scale}, but ignored since the model is not guidance-distilled."
772
+ )
773
+ guidance = None
774
+ elif not self.transformer.config.guidance_embeds and guidance_scale is None:
775
+ guidance = None
776
+
777
+ if self.attention_kwargs is None:
778
+ self._attention_kwargs = {}
779
+
780
+ txt_seq_lens = prompt_embeds_mask.sum(dim=1).tolist() if prompt_embeds_mask is not None else None
781
+
782
+ image_rotary_emb = self.transformer.pos_embed(img_shapes, txt_seq_lens, device=latents.device)
783
+ if do_true_cfg:
784
+ negative_txt_seq_lens = (
785
+ negative_prompt_embeds_mask.sum(dim=1).tolist()
786
+ if negative_prompt_embeds_mask is not None
787
+ else None
788
+ )
789
+ uncond_image_rotary_emb = self.transformer.pos_embed(
790
+ img_shapes, negative_txt_seq_lens, device=latents.device
791
+ )
792
+ else:
793
+ uncond_image_rotary_emb = None
794
+
795
+ # 6. Denoising loop
796
+ self.scheduler.set_begin_index(0)
797
+ with self.progress_bar(total=num_inference_steps) as progress_bar:
798
+ for i, t in enumerate(timesteps):
799
+ if self.interrupt:
800
+ continue
801
+
802
+ self._current_timestep = t
803
+
804
+ latent_model_input = latents
805
+ if image_latents is not None:
806
+ latent_model_input = torch.cat([latents, image_latents], dim=1)
807
+
808
+ # broadcast to batch dimension in a way that's compatible with ONNX/Core ML
809
+ timestep = t.expand(latents.shape[0]).to(latents.dtype)
810
+ with self.transformer.cache_context("cond"):
811
+ noise_pred = self.transformer(
812
+ hidden_states=latent_model_input,
813
+ timestep=timestep / 1000,
814
+ guidance=guidance,
815
+ encoder_hidden_states_mask=prompt_embeds_mask,
816
+ encoder_hidden_states=prompt_embeds,
817
+ image_rotary_emb=image_rotary_emb,
818
+ attention_kwargs=self.attention_kwargs,
819
+ return_dict=False,
820
+ )[0]
821
+ noise_pred = noise_pred[:, : latents.size(1)]
822
+
823
+ if do_true_cfg:
824
+ with self.transformer.cache_context("uncond"):
825
+ neg_noise_pred = self.transformer(
826
+ hidden_states=latent_model_input,
827
+ timestep=timestep / 1000,
828
+ guidance=guidance,
829
+ encoder_hidden_states_mask=negative_prompt_embeds_mask,
830
+ encoder_hidden_states=negative_prompt_embeds,
831
+ image_rotary_emb=uncond_image_rotary_emb,
832
+ attention_kwargs=self.attention_kwargs,
833
+ return_dict=False,
834
+ )[0]
835
+ neg_noise_pred = neg_noise_pred[:, : latents.size(1)]
836
+ comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
837
+
838
+ cond_norm = torch.norm(noise_pred, dim=-1, keepdim=True)
839
+ noise_norm = torch.norm(comb_pred, dim=-1, keepdim=True)
840
+ noise_pred = comb_pred * (cond_norm / noise_norm)
841
+
842
+ # compute the previous noisy sample x_t -> x_t-1
843
+ latents_dtype = latents.dtype
844
+ latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
845
+
846
+ if latents.dtype != latents_dtype:
847
+ if torch.backends.mps.is_available():
848
+ # some platforms (eg. apple mps) misbehave due to a pytorch bug: https://github.com/pytorch/pytorch/pull/99272
849
+ latents = latents.to(latents_dtype)
850
+
851
+ if callback_on_step_end is not None:
852
+ callback_kwargs = {}
853
+ for k in callback_on_step_end_tensor_inputs:
854
+ callback_kwargs[k] = locals()[k]
855
+ callback_outputs = callback_on_step_end(self, i, t, callback_kwargs)
856
+
857
+ latents = callback_outputs.pop("latents", latents)
858
+ prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds)
859
+
860
+ # call the callback, if provided
861
+ if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):
862
+ progress_bar.update()
863
+
864
+ if XLA_AVAILABLE:
865
+ xm.mark_step()
866
+
867
+ self._current_timestep = None
868
+ if output_type == "latent":
869
+ image = latents
870
+ else:
871
+ latents = self._unpack_latents(latents, height, width, self.vae_scale_factor)
872
+ latents = latents.to(self.vae.dtype)
873
+ latents_mean = (
874
+ torch.tensor(self.vae.config.latents_mean)
875
+ .view(1, self.vae.config.z_dim, 1, 1, 1)
876
+ .to(latents.device, latents.dtype)
877
+ )
878
+ latents_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(1, self.vae.config.z_dim, 1, 1, 1).to(
879
+ latents.device, latents.dtype
880
+ )
881
+ latents = latents / latents_std + latents_mean
882
+ image = self.vae.decode(latents, return_dict=False)[0][:, :, 0]
883
+ image = self.image_processor.postprocess(image, output_type=output_type)
884
+
885
+ # Offload all models
886
+ self.maybe_free_model_hooks()
887
+
888
+ if not return_dict:
889
+ return (image,)
890
+
891
+ return QwenImagePipelineOutput(images=image)
qwenimage/qwen_fa3_processor.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Paired with a good language model. Thanks!
3
+ """
4
+
5
+ import torch
6
+ from typing import Optional, Tuple
7
+ from diffusers.models.transformers.transformer_qwenimage import apply_rotary_emb_qwen
8
+
9
+ try:
10
+ from kernels import get_kernel
11
+ _k = get_kernel("kernels-community/vllm-flash-attn3")
12
+ _flash_attn_func = _k.flash_attn_func
13
+ except Exception as e:
14
+ _flash_attn_func = None
15
+ _kernels_err = e
16
+
17
+
18
+ def _ensure_fa3_available():
19
+ if _flash_attn_func is None:
20
+ raise ImportError(
21
+ "FlashAttention-3 via Hugging Face `kernels` is required. "
22
+ "Tried `get_kernel('kernels-community/vllm-flash-attn3')` and failed with:\n"
23
+ f"{_kernels_err}"
24
+ )
25
+
26
+ @torch.library.custom_op("flash::flash_attn_func", mutates_args=())
27
+ def flash_attn_func(
28
+ q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, causal: bool = False
29
+ ) -> torch.Tensor:
30
+ outputs, lse = _flash_attn_func(q, k, v, causal=causal)
31
+ return outputs
32
+
33
+ @flash_attn_func.register_fake
34
+ def _(q, k, v, **kwargs):
35
+ # two outputs:
36
+ # 1. output: (batch, seq_len, num_heads, head_dim)
37
+ # 2. softmax_lse: (batch, num_heads, seq_len) with dtype=torch.float32
38
+ meta_q = torch.empty_like(q).contiguous()
39
+ return meta_q #, q.new_empty((q.size(0), q.size(2), q.size(1)), dtype=torch.float32)
40
+
41
+
42
+ class QwenDoubleStreamAttnProcessorFA3:
43
+ """
44
+ FA3-based attention processor for Qwen double-stream architecture.
45
+ Computes joint attention over concatenated [text, image] streams using vLLM FlashAttention-3
46
+ accessed via Hugging Face `kernels`.
47
+
48
+ Notes / limitations:
49
+ - General attention masks are not supported here (FA3 path). `is_causal=False` and no arbitrary mask.
50
+ - Optional windowed attention / sink tokens / softcap can be plumbed through if you use those features.
51
+ - Expects an available `apply_rotary_emb_qwen` in scope (same as your non-FA3 processor).
52
+ """
53
+
54
+ _attention_backend = "fa3" # for parity with your other processors, not used internally
55
+
56
+ def __init__(self):
57
+ _ensure_fa3_available()
58
+
59
+ @torch.no_grad()
60
+ def __call__(
61
+ self,
62
+ attn, # Attention module with to_q/to_k/to_v/add_*_proj, norms, to_out, to_add_out, and .heads
63
+ hidden_states: torch.FloatTensor, # (B, S_img, D_model) image stream
64
+ encoder_hidden_states: torch.FloatTensor = None, # (B, S_txt, D_model) text stream
65
+ encoder_hidden_states_mask: torch.FloatTensor = None, # unused in FA3 path
66
+ attention_mask: Optional[torch.FloatTensor] = None, # unused in FA3 path
67
+ image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, # (img_freqs, txt_freqs)
68
+ ) -> Tuple[torch.FloatTensor, torch.FloatTensor]:
69
+ if encoder_hidden_states is None:
70
+ raise ValueError("QwenDoubleStreamAttnProcessorFA3 requires encoder_hidden_states (text stream).")
71
+ if attention_mask is not None:
72
+ # FA3 kernel path here does not consume arbitrary masks; fail fast to avoid silent correctness issues.
73
+ raise NotImplementedError("attention_mask is not supported in this FA3 implementation.")
74
+
75
+ _ensure_fa3_available()
76
+
77
+ B, S_img, _ = hidden_states.shape
78
+ S_txt = encoder_hidden_states.shape[1]
79
+
80
+ # ---- QKV projections (image/sample stream) ----
81
+ img_q = attn.to_q(hidden_states) # (B, S_img, D)
82
+ img_k = attn.to_k(hidden_states)
83
+ img_v = attn.to_v(hidden_states)
84
+
85
+ # ---- QKV projections (text/context stream) ----
86
+ txt_q = attn.add_q_proj(encoder_hidden_states) # (B, S_txt, D)
87
+ txt_k = attn.add_k_proj(encoder_hidden_states)
88
+ txt_v = attn.add_v_proj(encoder_hidden_states)
89
+
90
+ # ---- Reshape to (B, S, H, D_h) ----
91
+ H = attn.heads
92
+ img_q = img_q.unflatten(-1, (H, -1))
93
+ img_k = img_k.unflatten(-1, (H, -1))
94
+ img_v = img_v.unflatten(-1, (H, -1))
95
+
96
+ txt_q = txt_q.unflatten(-1, (H, -1))
97
+ txt_k = txt_k.unflatten(-1, (H, -1))
98
+ txt_v = txt_v.unflatten(-1, (H, -1))
99
+
100
+ # ---- Q/K normalization (per your module contract) ----
101
+ if getattr(attn, "norm_q", None) is not None:
102
+ img_q = attn.norm_q(img_q)
103
+ if getattr(attn, "norm_k", None) is not None:
104
+ img_k = attn.norm_k(img_k)
105
+ if getattr(attn, "norm_added_q", None) is not None:
106
+ txt_q = attn.norm_added_q(txt_q)
107
+ if getattr(attn, "norm_added_k", None) is not None:
108
+ txt_k = attn.norm_added_k(txt_k)
109
+
110
+ # ---- RoPE (Qwen variant) ----
111
+ if image_rotary_emb is not None:
112
+ img_freqs, txt_freqs = image_rotary_emb
113
+ # expects tensors shaped (B, S, H, D_h)
114
+ img_q = apply_rotary_emb_qwen(img_q, img_freqs, use_real=False)
115
+ img_k = apply_rotary_emb_qwen(img_k, img_freqs, use_real=False)
116
+ txt_q = apply_rotary_emb_qwen(txt_q, txt_freqs, use_real=False)
117
+ txt_k = apply_rotary_emb_qwen(txt_k, txt_freqs, use_real=False)
118
+
119
+ # ---- Joint attention over [text, image] along sequence axis ----
120
+ # Shapes: (B, S_total, H, D_h)
121
+ q = torch.cat([txt_q, img_q], dim=1)
122
+ k = torch.cat([txt_k, img_k], dim=1)
123
+ v = torch.cat([txt_v, img_v], dim=1)
124
+
125
+ # FlashAttention-3 path expects (B, S, H, D_h) and returns (out, softmax_lse)
126
+ out = flash_attn_func(q, k, v, causal=False) # out: (B, S_total, H, D_h)
127
+
128
+ # ---- Back to (B, S, D_model) ----
129
+ out = out.flatten(2, 3).to(q.dtype)
130
+
131
+ # Split back to text / image segments
132
+ txt_attn_out = out[:, :S_txt, :]
133
+ img_attn_out = out[:, S_txt:, :]
134
+
135
+ # ---- Output projections ----
136
+ img_attn_out = attn.to_out[0](img_attn_out)
137
+ if len(attn.to_out) > 1:
138
+ img_attn_out = attn.to_out[1](img_attn_out) # dropout if present
139
+
140
+ txt_attn_out = attn.to_add_out(txt_attn_out)
141
+
142
+ return img_attn_out, txt_attn_out
qwenimage/transformer_qwenimage.py ADDED
@@ -0,0 +1,642 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 Qwen-Image Team, The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import functools
16
+ import math
17
+ from typing import Any, Dict, List, Optional, Tuple, Union
18
+
19
+ import torch
20
+ import torch.nn as nn
21
+ import torch.nn.functional as F
22
+
23
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
24
+ from diffusers.loaders import FromOriginalModelMixin, PeftAdapterMixin
25
+ from diffusers.utils import USE_PEFT_BACKEND, logging, scale_lora_layers, unscale_lora_layers
26
+ from diffusers.utils.torch_utils import maybe_allow_in_graph
27
+ from diffusers.models.attention import FeedForward, AttentionMixin
28
+ from diffusers.models.attention_dispatch import dispatch_attention_fn
29
+ from diffusers.models.attention_processor import Attention
30
+ from diffusers.models.cache_utils import CacheMixin
31
+ from diffusers.models.embeddings import TimestepEmbedding, Timesteps
32
+ from diffusers.models.modeling_outputs import Transformer2DModelOutput
33
+ from diffusers.models.modeling_utils import ModelMixin
34
+ from diffusers.models.normalization import AdaLayerNormContinuous, RMSNorm
35
+
36
+
37
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
38
+
39
+
40
+ def get_timestep_embedding(
41
+ timesteps: torch.Tensor,
42
+ embedding_dim: int,
43
+ flip_sin_to_cos: bool = False,
44
+ downscale_freq_shift: float = 1,
45
+ scale: float = 1,
46
+ max_period: int = 10000,
47
+ ) -> torch.Tensor:
48
+ """
49
+ This matches the implementation in Denoising Diffusion Probabilistic Models: Create sinusoidal timestep embeddings.
50
+
51
+ Args
52
+ timesteps (torch.Tensor):
53
+ a 1-D Tensor of N indices, one per batch element. These may be fractional.
54
+ embedding_dim (int):
55
+ the dimension of the output.
56
+ flip_sin_to_cos (bool):
57
+ Whether the embedding order should be `cos, sin` (if True) or `sin, cos` (if False)
58
+ downscale_freq_shift (float):
59
+ Controls the delta between frequencies between dimensions
60
+ scale (float):
61
+ Scaling factor applied to the embeddings.
62
+ max_period (int):
63
+ Controls the maximum frequency of the embeddings
64
+ Returns
65
+ torch.Tensor: an [N x dim] Tensor of positional embeddings.
66
+ """
67
+ assert len(timesteps.shape) == 1, "Timesteps should be a 1d-array"
68
+
69
+ half_dim = embedding_dim // 2
70
+ exponent = -math.log(max_period) * torch.arange(
71
+ start=0, end=half_dim, dtype=torch.float32, device=timesteps.device
72
+ )
73
+ exponent = exponent / (half_dim - downscale_freq_shift)
74
+
75
+ emb = torch.exp(exponent).to(timesteps.dtype)
76
+ emb = timesteps[:, None].float() * emb[None, :]
77
+
78
+ # scale embeddings
79
+ emb = scale * emb
80
+
81
+ # concat sine and cosine embeddings
82
+ emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)
83
+
84
+ # flip sine and cosine embeddings
85
+ if flip_sin_to_cos:
86
+ emb = torch.cat([emb[:, half_dim:], emb[:, :half_dim]], dim=-1)
87
+
88
+ # zero pad
89
+ if embedding_dim % 2 == 1:
90
+ emb = torch.nn.functional.pad(emb, (0, 1, 0, 0))
91
+ return emb
92
+
93
+
94
+ def apply_rotary_emb_qwen(
95
+ x: torch.Tensor,
96
+ freqs_cis: Union[torch.Tensor, Tuple[torch.Tensor]],
97
+ use_real: bool = True,
98
+ use_real_unbind_dim: int = -1,
99
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
100
+ """
101
+ Apply rotary embeddings to input tensors using the given frequency tensor. This function applies rotary embeddings
102
+ to the given query or key 'x' tensors using the provided frequency tensor 'freqs_cis'. The input tensors are
103
+ reshaped as complex numbers, and the frequency tensor is reshaped for broadcasting compatibility. The resulting
104
+ tensors contain rotary embeddings and are returned as real tensors.
105
+
106
+ Args:
107
+ x (`torch.Tensor`):
108
+ Query or key tensor to apply rotary embeddings. [B, S, H, D] xk (torch.Tensor): Key tensor to apply
109
+ freqs_cis (`Tuple[torch.Tensor]`): Precomputed frequency tensor for complex exponentials. ([S, D], [S, D],)
110
+
111
+ Returns:
112
+ Tuple[torch.Tensor, torch.Tensor]: Tuple of modified query tensor and key tensor with rotary embeddings.
113
+ """
114
+ if use_real:
115
+ cos, sin = freqs_cis # [S, D]
116
+ cos = cos[None, None]
117
+ sin = sin[None, None]
118
+ cos, sin = cos.to(x.device), sin.to(x.device)
119
+
120
+ if use_real_unbind_dim == -1:
121
+ # Used for flux, cogvideox, hunyuan-dit
122
+ x_real, x_imag = x.reshape(*x.shape[:-1], -1, 2).unbind(-1) # [B, S, H, D//2]
123
+ x_rotated = torch.stack([-x_imag, x_real], dim=-1).flatten(3)
124
+ elif use_real_unbind_dim == -2:
125
+ # Used for Stable Audio, OmniGen, CogView4 and Cosmos
126
+ x_real, x_imag = x.reshape(*x.shape[:-1], 2, -1).unbind(-2) # [B, S, H, D//2]
127
+ x_rotated = torch.cat([-x_imag, x_real], dim=-1)
128
+ else:
129
+ raise ValueError(f"`use_real_unbind_dim={use_real_unbind_dim}` but should be -1 or -2.")
130
+
131
+ out = (x.float() * cos + x_rotated.float() * sin).to(x.dtype)
132
+
133
+ return out
134
+ else:
135
+ x_rotated = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
136
+ freqs_cis = freqs_cis.unsqueeze(1)
137
+ x_out = torch.view_as_real(x_rotated * freqs_cis).flatten(3)
138
+
139
+ return x_out.type_as(x)
140
+
141
+
142
+ class QwenTimestepProjEmbeddings(nn.Module):
143
+ def __init__(self, embedding_dim):
144
+ super().__init__()
145
+
146
+ self.time_proj = Timesteps(num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=0, scale=1000)
147
+ self.timestep_embedder = TimestepEmbedding(in_channels=256, time_embed_dim=embedding_dim)
148
+
149
+ def forward(self, timestep, hidden_states):
150
+ timesteps_proj = self.time_proj(timestep)
151
+ timesteps_emb = self.timestep_embedder(timesteps_proj.to(dtype=hidden_states.dtype)) # (N, D)
152
+
153
+ conditioning = timesteps_emb
154
+
155
+ return conditioning
156
+
157
+
158
+ class QwenEmbedRope(nn.Module):
159
+ def __init__(self, theta: int, axes_dim: List[int], scale_rope=False):
160
+ super().__init__()
161
+ self.theta = theta
162
+ self.axes_dim = axes_dim
163
+ pos_index = torch.arange(4096)
164
+ neg_index = torch.arange(4096).flip(0) * -1 - 1
165
+ self.pos_freqs = torch.cat(
166
+ [
167
+ self.rope_params(pos_index, self.axes_dim[0], self.theta),
168
+ self.rope_params(pos_index, self.axes_dim[1], self.theta),
169
+ self.rope_params(pos_index, self.axes_dim[2], self.theta),
170
+ ],
171
+ dim=1,
172
+ )
173
+ self.neg_freqs = torch.cat(
174
+ [
175
+ self.rope_params(neg_index, self.axes_dim[0], self.theta),
176
+ self.rope_params(neg_index, self.axes_dim[1], self.theta),
177
+ self.rope_params(neg_index, self.axes_dim[2], self.theta),
178
+ ],
179
+ dim=1,
180
+ )
181
+ self.rope_cache = {}
182
+
183
+ # DO NOT USING REGISTER BUFFER HERE, IT WILL CAUSE COMPLEX NUMBERS LOSE ITS IMAGINARY PART
184
+ self.scale_rope = scale_rope
185
+
186
+ def rope_params(self, index, dim, theta=10000):
187
+ """
188
+ Args:
189
+ index: [0, 1, 2, 3] 1D Tensor representing the position index of the token
190
+ """
191
+ assert dim % 2 == 0
192
+ freqs = torch.outer(index, 1.0 / torch.pow(theta, torch.arange(0, dim, 2).to(torch.float32).div(dim)))
193
+ freqs = torch.polar(torch.ones_like(freqs), freqs)
194
+ return freqs
195
+
196
+ def forward(self, video_fhw, txt_seq_lens, device):
197
+ """
198
+ Args: video_fhw: [frame, height, width] a list of 3 integers representing the shape of the video Args:
199
+ txt_length: [bs] a list of 1 integers representing the length of the text
200
+ """
201
+ if self.pos_freqs.device != device:
202
+ self.pos_freqs = self.pos_freqs.to(device)
203
+ self.neg_freqs = self.neg_freqs.to(device)
204
+
205
+ if isinstance(video_fhw, list):
206
+ video_fhw = video_fhw[0]
207
+ if not isinstance(video_fhw, list):
208
+ video_fhw = [video_fhw]
209
+
210
+ vid_freqs = []
211
+ max_vid_index = 0
212
+ for idx, fhw in enumerate(video_fhw):
213
+ frame, height, width = fhw
214
+ rope_key = f"{idx}_{height}_{width}"
215
+
216
+ if not torch.compiler.is_compiling():
217
+ if rope_key not in self.rope_cache:
218
+ self.rope_cache[rope_key] = self._compute_video_freqs(frame, height, width, idx)
219
+ video_freq = self.rope_cache[rope_key]
220
+ else:
221
+ video_freq = self._compute_video_freqs(frame, height, width, idx)
222
+ video_freq = video_freq.to(device)
223
+ vid_freqs.append(video_freq)
224
+
225
+ if self.scale_rope:
226
+ max_vid_index = max(height // 2, width // 2, max_vid_index)
227
+ else:
228
+ max_vid_index = max(height, width, max_vid_index)
229
+
230
+ max_len = max(txt_seq_lens)
231
+ txt_freqs = self.pos_freqs[max_vid_index : max_vid_index + max_len, ...]
232
+ vid_freqs = torch.cat(vid_freqs, dim=0)
233
+
234
+ return vid_freqs, txt_freqs
235
+
236
+ @functools.lru_cache(maxsize=None)
237
+ def _compute_video_freqs(self, frame, height, width, idx=0):
238
+ seq_lens = frame * height * width
239
+ freqs_pos = self.pos_freqs.split([x // 2 for x in self.axes_dim], dim=1)
240
+ freqs_neg = self.neg_freqs.split([x // 2 for x in self.axes_dim], dim=1)
241
+
242
+ freqs_frame = freqs_pos[0][idx : idx + frame].view(frame, 1, 1, -1).expand(frame, height, width, -1)
243
+ if self.scale_rope:
244
+ freqs_height = torch.cat([freqs_neg[1][-(height - height // 2) :], freqs_pos[1][: height // 2]], dim=0)
245
+ freqs_height = freqs_height.view(1, height, 1, -1).expand(frame, height, width, -1)
246
+ freqs_width = torch.cat([freqs_neg[2][-(width - width // 2) :], freqs_pos[2][: width // 2]], dim=0)
247
+ freqs_width = freqs_width.view(1, 1, width, -1).expand(frame, height, width, -1)
248
+ else:
249
+ freqs_height = freqs_pos[1][:height].view(1, height, 1, -1).expand(frame, height, width, -1)
250
+ freqs_width = freqs_pos[2][:width].view(1, 1, width, -1).expand(frame, height, width, -1)
251
+
252
+ freqs = torch.cat([freqs_frame, freqs_height, freqs_width], dim=-1).reshape(seq_lens, -1)
253
+ return freqs.clone().contiguous()
254
+
255
+
256
+ class QwenDoubleStreamAttnProcessor2_0:
257
+ """
258
+ Attention processor for Qwen double-stream architecture, matching DoubleStreamLayerMegatron logic. This processor
259
+ implements joint attention computation where text and image streams are processed together.
260
+ """
261
+
262
+ _attention_backend = None
263
+
264
+ def __init__(self):
265
+ if not hasattr(F, "scaled_dot_product_attention"):
266
+ raise ImportError(
267
+ "QwenDoubleStreamAttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0."
268
+ )
269
+
270
+ def __call__(
271
+ self,
272
+ attn: Attention,
273
+ hidden_states: torch.FloatTensor, # Image stream
274
+ encoder_hidden_states: torch.FloatTensor = None, # Text stream
275
+ encoder_hidden_states_mask: torch.FloatTensor = None,
276
+ attention_mask: Optional[torch.FloatTensor] = None,
277
+ image_rotary_emb: Optional[torch.Tensor] = None,
278
+ ) -> torch.FloatTensor:
279
+ if encoder_hidden_states is None:
280
+ raise ValueError("QwenDoubleStreamAttnProcessor2_0 requires encoder_hidden_states (text stream)")
281
+
282
+ seq_txt = encoder_hidden_states.shape[1]
283
+
284
+ # Compute QKV for image stream (sample projections)
285
+ img_query = attn.to_q(hidden_states)
286
+ img_key = attn.to_k(hidden_states)
287
+ img_value = attn.to_v(hidden_states)
288
+
289
+ # Compute QKV for text stream (context projections)
290
+ txt_query = attn.add_q_proj(encoder_hidden_states)
291
+ txt_key = attn.add_k_proj(encoder_hidden_states)
292
+ txt_value = attn.add_v_proj(encoder_hidden_states)
293
+
294
+ # Reshape for multi-head attention
295
+ img_query = img_query.unflatten(-1, (attn.heads, -1))
296
+ img_key = img_key.unflatten(-1, (attn.heads, -1))
297
+ img_value = img_value.unflatten(-1, (attn.heads, -1))
298
+
299
+ txt_query = txt_query.unflatten(-1, (attn.heads, -1))
300
+ txt_key = txt_key.unflatten(-1, (attn.heads, -1))
301
+ txt_value = txt_value.unflatten(-1, (attn.heads, -1))
302
+
303
+ # Apply QK normalization
304
+ if attn.norm_q is not None:
305
+ img_query = attn.norm_q(img_query)
306
+ if attn.norm_k is not None:
307
+ img_key = attn.norm_k(img_key)
308
+ if attn.norm_added_q is not None:
309
+ txt_query = attn.norm_added_q(txt_query)
310
+ if attn.norm_added_k is not None:
311
+ txt_key = attn.norm_added_k(txt_key)
312
+
313
+ # Apply RoPE
314
+ if image_rotary_emb is not None:
315
+ img_freqs, txt_freqs = image_rotary_emb
316
+ img_query = apply_rotary_emb_qwen(img_query, img_freqs, use_real=False)
317
+ img_key = apply_rotary_emb_qwen(img_key, img_freqs, use_real=False)
318
+ txt_query = apply_rotary_emb_qwen(txt_query, txt_freqs, use_real=False)
319
+ txt_key = apply_rotary_emb_qwen(txt_key, txt_freqs, use_real=False)
320
+
321
+ # Concatenate for joint attention
322
+ # Order: [text, image]
323
+ joint_query = torch.cat([txt_query, img_query], dim=1)
324
+ joint_key = torch.cat([txt_key, img_key], dim=1)
325
+ joint_value = torch.cat([txt_value, img_value], dim=1)
326
+
327
+ # Compute joint attention
328
+ joint_hidden_states = dispatch_attention_fn(
329
+ joint_query,
330
+ joint_key,
331
+ joint_value,
332
+ attn_mask=attention_mask,
333
+ dropout_p=0.0,
334
+ is_causal=False,
335
+ backend=self._attention_backend,
336
+ )
337
+
338
+ # Reshape back
339
+ joint_hidden_states = joint_hidden_states.flatten(2, 3)
340
+ joint_hidden_states = joint_hidden_states.to(joint_query.dtype)
341
+
342
+ # Split attention outputs back
343
+ txt_attn_output = joint_hidden_states[:, :seq_txt, :] # Text part
344
+ img_attn_output = joint_hidden_states[:, seq_txt:, :] # Image part
345
+
346
+ # Apply output projections
347
+ img_attn_output = attn.to_out[0](img_attn_output)
348
+ if len(attn.to_out) > 1:
349
+ img_attn_output = attn.to_out[1](img_attn_output) # dropout
350
+
351
+ txt_attn_output = attn.to_add_out(txt_attn_output)
352
+
353
+ return img_attn_output, txt_attn_output
354
+
355
+
356
+ @maybe_allow_in_graph
357
+ class QwenImageTransformerBlock(nn.Module):
358
+ def __init__(
359
+ self, dim: int, num_attention_heads: int, attention_head_dim: int, qk_norm: str = "rms_norm", eps: float = 1e-6
360
+ ):
361
+ super().__init__()
362
+
363
+ self.dim = dim
364
+ self.num_attention_heads = num_attention_heads
365
+ self.attention_head_dim = attention_head_dim
366
+
367
+ # Image processing modules
368
+ self.img_mod = nn.Sequential(
369
+ nn.SiLU(),
370
+ nn.Linear(dim, 6 * dim, bias=True), # For scale, shift, gate for norm1 and norm2
371
+ )
372
+ self.img_norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
373
+ self.attn = Attention(
374
+ query_dim=dim,
375
+ cross_attention_dim=None, # Enable cross attention for joint computation
376
+ added_kv_proj_dim=dim, # Enable added KV projections for text stream
377
+ dim_head=attention_head_dim,
378
+ heads=num_attention_heads,
379
+ out_dim=dim,
380
+ context_pre_only=False,
381
+ bias=True,
382
+ processor=QwenDoubleStreamAttnProcessor2_0(),
383
+ qk_norm=qk_norm,
384
+ eps=eps,
385
+ )
386
+ self.img_norm2 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
387
+ self.img_mlp = FeedForward(dim=dim, dim_out=dim, activation_fn="gelu-approximate")
388
+
389
+ # Text processing modules
390
+ self.txt_mod = nn.Sequential(
391
+ nn.SiLU(),
392
+ nn.Linear(dim, 6 * dim, bias=True), # For scale, shift, gate for norm1 and norm2
393
+ )
394
+ self.txt_norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
395
+ # Text doesn't need separate attention - it's handled by img_attn joint computation
396
+ self.txt_norm2 = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
397
+ self.txt_mlp = FeedForward(dim=dim, dim_out=dim, activation_fn="gelu-approximate")
398
+
399
+ def _modulate(self, x, mod_params):
400
+ """Apply modulation to input tensor"""
401
+ shift, scale, gate = mod_params.chunk(3, dim=-1)
402
+ return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1), gate.unsqueeze(1)
403
+
404
+ def forward(
405
+ self,
406
+ hidden_states: torch.Tensor,
407
+ encoder_hidden_states: torch.Tensor,
408
+ encoder_hidden_states_mask: torch.Tensor,
409
+ temb: torch.Tensor,
410
+ image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
411
+ joint_attention_kwargs: Optional[Dict[str, Any]] = None,
412
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
413
+ # Get modulation parameters for both streams
414
+ img_mod_params = self.img_mod(temb) # [B, 6*dim]
415
+ txt_mod_params = self.txt_mod(temb) # [B, 6*dim]
416
+
417
+ # Split modulation parameters for norm1 and norm2
418
+ img_mod1, img_mod2 = img_mod_params.chunk(2, dim=-1) # Each [B, 3*dim]
419
+ txt_mod1, txt_mod2 = txt_mod_params.chunk(2, dim=-1) # Each [B, 3*dim]
420
+
421
+ # Process image stream - norm1 + modulation
422
+ img_normed = self.img_norm1(hidden_states)
423
+ img_modulated, img_gate1 = self._modulate(img_normed, img_mod1)
424
+
425
+ # Process text stream - norm1 + modulation
426
+ txt_normed = self.txt_norm1(encoder_hidden_states)
427
+ txt_modulated, txt_gate1 = self._modulate(txt_normed, txt_mod1)
428
+
429
+ # Use QwenAttnProcessor2_0 for joint attention computation
430
+ # This directly implements the DoubleStreamLayerMegatron logic:
431
+ # 1. Computes QKV for both streams
432
+ # 2. Applies QK normalization and RoPE
433
+ # 3. Concatenates and runs joint attention
434
+ # 4. Splits results back to separate streams
435
+ joint_attention_kwargs = joint_attention_kwargs or {}
436
+ attn_output = self.attn(
437
+ hidden_states=img_modulated, # Image stream (will be processed as "sample")
438
+ encoder_hidden_states=txt_modulated, # Text stream (will be processed as "context")
439
+ encoder_hidden_states_mask=encoder_hidden_states_mask,
440
+ image_rotary_emb=image_rotary_emb,
441
+ **joint_attention_kwargs,
442
+ )
443
+
444
+ # QwenAttnProcessor2_0 returns (img_output, txt_output) when encoder_hidden_states is provided
445
+ img_attn_output, txt_attn_output = attn_output
446
+
447
+ # Apply attention gates and add residual (like in Megatron)
448
+ hidden_states = hidden_states + img_gate1 * img_attn_output
449
+ encoder_hidden_states = encoder_hidden_states + txt_gate1 * txt_attn_output
450
+
451
+ # Process image stream - norm2 + MLP
452
+ img_normed2 = self.img_norm2(hidden_states)
453
+ img_modulated2, img_gate2 = self._modulate(img_normed2, img_mod2)
454
+ img_mlp_output = self.img_mlp(img_modulated2)
455
+ hidden_states = hidden_states + img_gate2 * img_mlp_output
456
+
457
+ # Process text stream - norm2 + MLP
458
+ txt_normed2 = self.txt_norm2(encoder_hidden_states)
459
+ txt_modulated2, txt_gate2 = self._modulate(txt_normed2, txt_mod2)
460
+ txt_mlp_output = self.txt_mlp(txt_modulated2)
461
+ encoder_hidden_states = encoder_hidden_states + txt_gate2 * txt_mlp_output
462
+
463
+ # Clip to prevent overflow for fp16
464
+ if encoder_hidden_states.dtype == torch.float16:
465
+ encoder_hidden_states = encoder_hidden_states.clip(-65504, 65504)
466
+ if hidden_states.dtype == torch.float16:
467
+ hidden_states = hidden_states.clip(-65504, 65504)
468
+
469
+ return encoder_hidden_states, hidden_states
470
+
471
+
472
+ class QwenImageTransformer2DModel(ModelMixin, ConfigMixin, PeftAdapterMixin, FromOriginalModelMixin, CacheMixin, AttentionMixin):
473
+ """
474
+ The Transformer model introduced in Qwen.
475
+
476
+ Args:
477
+ patch_size (`int`, defaults to `2`):
478
+ Patch size to turn the input data into small patches.
479
+ in_channels (`int`, defaults to `64`):
480
+ The number of channels in the input.
481
+ out_channels (`int`, *optional*, defaults to `None`):
482
+ The number of channels in the output. If not specified, it defaults to `in_channels`.
483
+ num_layers (`int`, defaults to `60`):
484
+ The number of layers of dual stream DiT blocks to use.
485
+ attention_head_dim (`int`, defaults to `128`):
486
+ The number of dimensions to use for each attention head.
487
+ num_attention_heads (`int`, defaults to `24`):
488
+ The number of attention heads to use.
489
+ joint_attention_dim (`int`, defaults to `3584`):
490
+ The number of dimensions to use for the joint attention (embedding/channel dimension of
491
+ `encoder_hidden_states`).
492
+ guidance_embeds (`bool`, defaults to `False`):
493
+ Whether to use guidance embeddings for guidance-distilled variant of the model.
494
+ axes_dims_rope (`Tuple[int]`, defaults to `(16, 56, 56)`):
495
+ The dimensions to use for the rotary positional embeddings.
496
+ """
497
+
498
+ _supports_gradient_checkpointing = True
499
+ _no_split_modules = ["QwenImageTransformerBlock"]
500
+ _skip_layerwise_casting_patterns = ["pos_embed", "norm"]
501
+ _repeated_blocks = ["QwenImageTransformerBlock"]
502
+
503
+ @register_to_config
504
+ def __init__(
505
+ self,
506
+ patch_size: int = 2,
507
+ in_channels: int = 64,
508
+ out_channels: Optional[int] = 16,
509
+ num_layers: int = 60,
510
+ attention_head_dim: int = 128,
511
+ num_attention_heads: int = 24,
512
+ joint_attention_dim: int = 3584,
513
+ guidance_embeds: bool = False, # TODO: this should probably be removed
514
+ axes_dims_rope: Tuple[int, int, int] = (16, 56, 56),
515
+ ):
516
+ super().__init__()
517
+ self.out_channels = out_channels or in_channels
518
+ self.inner_dim = num_attention_heads * attention_head_dim
519
+
520
+ self.pos_embed = QwenEmbedRope(theta=10000, axes_dim=list(axes_dims_rope), scale_rope=True)
521
+
522
+ self.time_text_embed = QwenTimestepProjEmbeddings(embedding_dim=self.inner_dim)
523
+
524
+ self.txt_norm = RMSNorm(joint_attention_dim, eps=1e-6)
525
+
526
+ self.img_in = nn.Linear(in_channels, self.inner_dim)
527
+ self.txt_in = nn.Linear(joint_attention_dim, self.inner_dim)
528
+
529
+ self.transformer_blocks = nn.ModuleList(
530
+ [
531
+ QwenImageTransformerBlock(
532
+ dim=self.inner_dim,
533
+ num_attention_heads=num_attention_heads,
534
+ attention_head_dim=attention_head_dim,
535
+ )
536
+ for _ in range(num_layers)
537
+ ]
538
+ )
539
+
540
+ self.norm_out = AdaLayerNormContinuous(self.inner_dim, self.inner_dim, elementwise_affine=False, eps=1e-6)
541
+ self.proj_out = nn.Linear(self.inner_dim, patch_size * patch_size * self.out_channels, bias=True)
542
+
543
+ self.gradient_checkpointing = False
544
+
545
+ def forward(
546
+ self,
547
+ hidden_states: torch.Tensor,
548
+ encoder_hidden_states: torch.Tensor = None,
549
+ encoder_hidden_states_mask: torch.Tensor = None,
550
+ timestep: torch.LongTensor = None,
551
+ image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
552
+ guidance: torch.Tensor = None, # TODO: this should probably be removed
553
+ attention_kwargs: Optional[Dict[str, Any]] = None,
554
+ return_dict: bool = True,
555
+ ) -> Union[torch.Tensor, Transformer2DModelOutput]:
556
+ """
557
+ The [`QwenTransformer2DModel`] forward method.
558
+
559
+ Args:
560
+ hidden_states (`torch.Tensor` of shape `(batch_size, image_sequence_length, in_channels)`):
561
+ Input `hidden_states`.
562
+ encoder_hidden_states (`torch.Tensor` of shape `(batch_size, text_sequence_length, joint_attention_dim)`):
563
+ Conditional embeddings (embeddings computed from the input conditions such as prompts) to use.
564
+ encoder_hidden_states_mask (`torch.Tensor` of shape `(batch_size, text_sequence_length)`):
565
+ Mask of the input conditions.
566
+ timestep ( `torch.LongTensor`):
567
+ Used to indicate denoising step.
568
+ attention_kwargs (`dict`, *optional*):
569
+ A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under
570
+ `self.processor` in
571
+ [diffusers.models.attention_processor](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py).
572
+ return_dict (`bool`, *optional*, defaults to `True`):
573
+ Whether or not to return a [`~models.transformer_2d.Transformer2DModelOutput`] instead of a plain
574
+ tuple.
575
+
576
+ Returns:
577
+ If `return_dict` is True, an [`~models.transformer_2d.Transformer2DModelOutput`] is returned, otherwise a
578
+ `tuple` where the first element is the sample tensor.
579
+ """
580
+ if attention_kwargs is not None:
581
+ attention_kwargs = attention_kwargs.copy()
582
+ lora_scale = attention_kwargs.pop("scale", 1.0)
583
+ else:
584
+ lora_scale = 1.0
585
+
586
+ if USE_PEFT_BACKEND:
587
+ # weight the lora layers by setting `lora_scale` for each PEFT layer
588
+ scale_lora_layers(self, lora_scale)
589
+ else:
590
+ if attention_kwargs is not None and attention_kwargs.get("scale", None) is not None:
591
+ logger.warning(
592
+ "Passing `scale` via `joint_attention_kwargs` when not using the PEFT backend is ineffective."
593
+ )
594
+
595
+ hidden_states = self.img_in(hidden_states)
596
+
597
+ timestep = timestep.to(hidden_states.dtype)
598
+ encoder_hidden_states = self.txt_norm(encoder_hidden_states)
599
+ encoder_hidden_states = self.txt_in(encoder_hidden_states)
600
+
601
+ if guidance is not None:
602
+ guidance = guidance.to(hidden_states.dtype) * 1000
603
+
604
+ temb = (
605
+ self.time_text_embed(timestep, hidden_states)
606
+ if guidance is None
607
+ else self.time_text_embed(timestep, guidance, hidden_states)
608
+ )
609
+
610
+ for index_block, block in enumerate(self.transformer_blocks):
611
+ if torch.is_grad_enabled() and self.gradient_checkpointing:
612
+ encoder_hidden_states, hidden_states = self._gradient_checkpointing_func(
613
+ block,
614
+ hidden_states,
615
+ encoder_hidden_states,
616
+ encoder_hidden_states_mask,
617
+ temb,
618
+ image_rotary_emb,
619
+ )
620
+
621
+ else:
622
+ encoder_hidden_states, hidden_states = block(
623
+ hidden_states=hidden_states,
624
+ encoder_hidden_states=encoder_hidden_states,
625
+ encoder_hidden_states_mask=encoder_hidden_states_mask,
626
+ temb=temb,
627
+ image_rotary_emb=image_rotary_emb,
628
+ joint_attention_kwargs=attention_kwargs,
629
+ )
630
+
631
+ # Use only the image part (hidden_states) from the dual-stream blocks
632
+ hidden_states = self.norm_out(hidden_states, temb)
633
+ output = self.proj_out(hidden_states)
634
+
635
+ if USE_PEFT_BACKEND:
636
+ # remove `lora_scale` from each PEFT layer
637
+ unscale_lora_layers(self, lora_scale)
638
+
639
+ if not return_dict:
640
+ return (output,)
641
+
642
+ return Transformer2DModelOutput(sample=output)
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ git+https://github.com/huggingface/accelerate.git
2
+ git+https://github.com/huggingface/diffusers.git
3
+ git+https://github.com/huggingface/peft.git
4
+ transformers==4.57.6
5
+ huggingface_hub
6
+ sentencepiece
7
+ torchvision
8
+ kernels
9
+ spaces
10
+ hf_xet
11
+ gradio
12
+ torch
13
+ numpy
14
+ av