Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- .gitattributes +6 -0
- LICENSE.txt +201 -0
- README.md +76 -6
- app.py +1158 -0
- examples/1.jpg +0 -0
- examples/10.jpg +3 -0
- examples/11.png +3 -0
- examples/2.jpg +0 -0
- examples/3.jpeg +0 -0
- examples/4.jpg +0 -0
- examples/5.jpg +0 -0
- examples/6.jpg +3 -0
- examples/7.webp +3 -0
- examples/8.jpg +3 -0
- examples/9.png +3 -0
- pre-requirements.txt +1 -0
- qwenimage/__init__.py +0 -0
- qwenimage/pipeline_qwenimage_edit_plus.py +891 -0
- qwenimage/qwen_fa3_processor.py +142 -0
- qwenimage/transformer_qwenimage.py +642 -0
- requirements.txt +14 -0
.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:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ·
|
| 1018 |
+
<b>Multi-image:</b> Upload multiple images for reference-based editing ·
|
| 1019 |
+
<kbd>Remove</kbd> deletes selected ·
|
| 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 |
+
· 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
|
examples/11.png
ADDED
|
Git LFS Details
|
examples/2.jpg
ADDED
|
examples/3.jpeg
ADDED
|
examples/4.jpg
ADDED
|
examples/5.jpg
ADDED
|
examples/6.jpg
ADDED
|
Git LFS Details
|
examples/7.webp
ADDED
|
Git LFS Details
|
examples/8.jpg
ADDED
|
Git LFS Details
|
examples/9.png
ADDED
|
Git LFS Details
|
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
|