yijunwang2 commited on
Commit
004f54a
·
verified ·
1 Parent(s): 165101a

Upload private Krea 2 AnyPaint release candidate

Browse files
.gitattributes CHANGED
@@ -33,3 +33,16 @@ 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
+ LICENSE.pdf filter=lfs diff=lfs merge=lfs -text
37
+ showcase/02_watercolor_outpaint_result.webp filter=lfs diff=lfs merge=lfs -text
38
+ showcase/02_watercolor_outpaint_source.webp filter=lfs diff=lfs merge=lfs -text
39
+ showcase/03_3d_outpaint_result.webp filter=lfs diff=lfs merge=lfs -text
40
+ showcase/03_3d_outpaint_source.webp filter=lfs diff=lfs merge=lfs -text
41
+ showcase/04_portrait_mixed_result.webp filter=lfs diff=lfs merge=lfs -text
42
+ showcase/05_botanical_disconnected_result.webp filter=lfs diff=lfs merge=lfs -text
43
+ showcase/05_botanical_disconnected_source.webp filter=lfs diff=lfs merge=lfs -text
44
+ showcase/06_interior_boundary_result.webp filter=lfs diff=lfs merge=lfs -text
45
+ showcase/06_interior_boundary_source.webp filter=lfs diff=lfs merge=lfs -text
46
+ showcase/08_landscape_complex_result.webp filter=lfs diff=lfs merge=lfs -text
47
+ showcase/08_landscape_complex_source.webp filter=lfs diff=lfs merge=lfs -text
48
+ showcase/contact_sheet.webp filter=lfs diff=lfs merge=lfs -text
LICENSE.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b82a2805162bde714a4eb27b9063c4fc3345d08a30be055134a6160e5430ba74
3
+ size 137711
NOTICE ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Krea 2 AnyPaint
2
+
3
+ Krea 2 is licensed under the Krea 2 Community License Agreement.
4
+ For more information, visit https://krea.ai/krea-2-licensing.
5
+
6
+ This repository contains a modified derivative of Krea 2: Krea 2 AnyPaint, a
7
+ rank-32 LoRA and modified reference pipeline that add arbitrary-mask
8
+ inpainting, outpainting, and image editing.
9
+
10
+ Use, modification, and distribution of the model weights are subject to the
11
+ Krea 2 Community License Agreement included as LICENSE.pdf.
12
+
13
+ The included pipeline is derived from ostris/Krea2OstrisEdit and adds
14
+ destination-relative reference placement and timestep-matched known-region
15
+ preservation. Pipeline and helper code are distributed under Apache License
16
+ 2.0.
17
+
18
+ Krea 2: https://huggingface.co/krea/Krea-2-Turbo
19
+ Upstream edit pipeline: https://huggingface.co/ostris/Krea2OstrisEdit
20
+
21
+ This is an unofficial community release and is not endorsed by Krea or
22
+ Ostris. Training data is not included.
PIPELINE_LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: other
3
+ license_name: krea-2-community-license
4
+ license_link: https://krea.ai/krea-2-licensing
5
+ base_model:
6
+ - krea/Krea-2-Raw
7
+ - krea/Krea-2-Turbo
8
+ library_name: diffusers
9
+ pipeline_tag: image-to-image
10
+ tags:
11
+ - krea-2
12
+ - krea2
13
+ - anypaint
14
+ - lora
15
+ - inpainting
16
+ - outpainting
17
+ - arbitrary-mask
18
+ - image-editing
19
+ - image-to-image
20
+ - diffusers
21
+ ---
22
+
23
+ # Krea 2 AnyPaint: Arbitrary-Mask Inpainting, Outpainting, and Image Editing
24
+
25
+ > **Important:** The Hugging Face auto-generated Diffusers snippet and ordinary
26
+ > LoRA importers are not compatible with this functional adapter. Use the
27
+ > included `example.py`, `pipeline.py`, and `anypaint.py`.
28
+
29
+ **Krea 2 AnyPaint provides arbitrary-mask inpainting, outpainting, and image
30
+ editing for Krea 2 Turbo.** One runtime handles internal edits, canvas
31
+ extension, disconnected regions, narrow structures, and mixed masks that cross
32
+ the source boundary.
33
+
34
+ [Try the private/public Space](https://huggingface.co/spaces/yijunwang2/krea2-anypaint)
35
+ | [Runtime source](https://github.com/alexw5702-afk/krea2-anypaint)
36
+
37
+ ## Capabilities
38
+
39
+ - arbitrary-shape and freehand mask image editing;
40
+ - same-canvas inpainting;
41
+ - one-pass outpainting into a larger canvas;
42
+ - multiple disconnected edit regions;
43
+ - narrow, curved, and boundary-crossing masks;
44
+ - combined inpainting and outpainting in one request;
45
+ - preservation of unmasked regions through timestep-matched latent constraints.
46
+
47
+ White mask pixels are generated or edited. Black mask pixels are preserved.
48
+ The helper also marks every pixel outside the placed source as generated, so
49
+ the same input contract covers both inpainting and outpainting.
50
+
51
+ ## Showcase
52
+
53
+ All showcase sources are synthetic, generated from the unmodified Krea 2 base
54
+ model with original prompts. They contain no real-person identity, brand,
55
+ existing fictional character, or other third-party character IP. Each listed
56
+ result is the first output from its recorded prompt and fixed seed; unsuccessful
57
+ runs were not silently removed.
58
+
59
+ The release candidate includes a diverse fixed-seed set covering photographic,
60
+ illustrated, 3D, indoor, outdoor, object, and human-subject compositions. Every
61
+ case ships with its source, exact binary mask, output, and generation metadata
62
+ under `showcase/`.
63
+
64
+ ## Input contract
65
+
66
+ The runtime takes:
67
+
68
+ - one RGB source image;
69
+ - one binary mask, either source-sized or canvas-sized;
70
+ - an output canvas whose dimensions are multiples of 16;
71
+ - an optional source bounding box `(x0, y0, x1, y1)` in output pixels;
72
+ - a prompt describing the complete desired output image.
73
+
74
+ The source bounding box must stay inside the canvas and preserve the source
75
+ aspect ratio. A source-sized mask is resized and placed with the source. A
76
+ canvas-sized mask is used directly. Areas outside the source box are always
77
+ generated.
78
+
79
+ The complete masked canvas is encoded as one maximum-edge-384 reference whose
80
+ rotary coordinates cover the full target canvas. A full-resolution known image
81
+ and keep mask separately preserve reliable unmasked latent tokens after every
82
+ flow step. A 32-pixel boundary band is deliberately left for the model to
83
+ redraw and blend. The returned image is the raw decoded model output; there is
84
+ no hidden post-generation source composite.
85
+
86
+ ## Recommended settings
87
+
88
+ | Setting | Value |
89
+ |---|---:|
90
+ | Inference base | `krea/Krea-2-Turbo` |
91
+ | Steps | 8 |
92
+ | Guidance scale | 0.0 |
93
+ | LoRA scale | 1.0 |
94
+ | Reference maximum edge | 384 px |
95
+ | Boundary redraw band | 32 px |
96
+ | Reference VLM encoding | enabled |
97
+ | Reference K/V cache | enabled |
98
+
99
+ The adapter was trained at rank/alpha 32/32 against Krea 2 Raw and is intended
100
+ for distilled inference with Krea 2 Turbo.
101
+
102
+ ## Usage
103
+
104
+ ```bash
105
+ pip install -r requirements.txt
106
+
107
+ python example.py \
108
+ --source input.webp \
109
+ --mask mask.png \
110
+ --output result.webp \
111
+ --width 1536 \
112
+ --height 1024 \
113
+ --bbox 0 0 1024 1024 \
114
+ --prompt "a bright modern studio, complete coherent composition"
115
+ ```
116
+
117
+ For pure inpainting, omit `--bbox` and use a source image matching the output
118
+ canvas. For outpainting, provide the source location in the larger canvas. The
119
+ mask may additionally select any internal areas to regenerate.
120
+
121
+ ## How it works
122
+
123
+ 1. The source is placed without changing its aspect ratio.
124
+ 2. The out-of-source canvas and the user mask are unioned into one generated
125
+ region.
126
+ 3. Generated pixels in the semantic reference are replaced by the median color
127
+ of known pixels; the complete reference is encoded at a 384-pixel maximum
128
+ edge.
129
+ 4. The denoiser uses destination-relative reference coordinates and isolated
130
+ reference K/V caching.
131
+ 5. After every flow step, reliable known tokens are restored at the next noise
132
+ level while the mask boundary remains free for coherent blending.
133
+
134
+ ## Limitations
135
+
136
+ - Large generated fractions may introduce unrelated subjects or structures.
137
+ - Very thin mask features may be coarsened by VAE and patch tokenization.
138
+ - Ambiguous prompts can change more context near the 32-pixel redraw band.
139
+ - The model may reproduce semantic biases present in the Krea 2 base model.
140
+ - The portable BF16 example favors reproducibility over the fastest
141
+ hardware-specific quantized runtime.
142
+
143
+ ## Responsible use
144
+
145
+ Use reference images only when you have the subject's consent or another lawful
146
+ basis to use them. Do not use this model for deceptive impersonation, fraud,
147
+ harassment, non-consensual intimate imagery, biometric identification, or
148
+ misleading political or media content.
149
+
150
+ Deployments must follow the Krea 2 Acceptable Use Policy, implement appropriate
151
+ content moderation, and disclose AI-generated content where required.
152
+
153
+ ## License
154
+
155
+ The LoRA weights are a derivative of Krea 2 and are released under the Krea 2
156
+ Community License in `LICENSE.pdf`. Pipeline and helper code are distributed
157
+ under Apache License 2.0 in `PIPELINE_LICENSE`. This is an unofficial community
158
+ release and is not endorsed by Krea or Ostris. Training data is not included.
SHA256SUMS ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ b82a2805162bde714a4eb27b9063c4fc3345d08a30be055134a6160e5430ba74 LICENSE.pdf
2
+ 740282d896660e50f80db176f12b8ab1eff8262c1226372b7a21762c2fd28bf3 NOTICE
3
+ cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 PIPELINE_LICENSE
4
+ a7bd3899462fbf66838d3cc936ab9d4f1be7d7932643f91a3718765371ffb7c8 README.md
5
+ 848cd6f3616e7c59dc2dfe295525a81f659315a7095b8abf8a5c7fe3b6ce6f44 anypaint.py
6
+ f1222b7676d3edd33b5543426ea5955a5097a0a375877fd0338f3c374e1de81c example.py
7
+ 3a7d09f6b27f8ead160d340f2f59c11f4ee635c4a1ee87ffd8b1b9f9ba412f7a krea2_anypaint_rank32.safetensors
8
+ f74edb4026306afec92ec5a9d60beaa22fac74361531bb66a0dab13d745b6a2d pipeline.py
9
+ 15cf6d284f6a6737c140d89dde872e66374db24b9e6d383d9cd9fdc96d11136d requirements.txt
10
+ 7d27ee47ac736e14b41a2c0e91f80eb8c8a4c5e75732d37a7118ef0cc5a3d22e showcase/01_photo_internal_mask.png
11
+ 4ae364f61bbc20b2f1b12652c382e3734bfe654a654ac094db836f1cf4659947 showcase/01_photo_internal_result.webp
12
+ 0bef8e0e5ecb74ef789d8dc591fc204d89b7cd6996e1789dddfdfcffbc5c58fc showcase/01_photo_internal_source.webp
13
+ 69a6a3a1f8e4e9e9d705545dbe4fe849e4685aaade3c405a4a0caa17a14e11b8 showcase/02_watercolor_outpaint_mask.png
14
+ da9ce77585777ccfb8092643b1e9fe8e8e14173bdb8669e06c2b82afd43262fb showcase/02_watercolor_outpaint_result.webp
15
+ 4efe48474687d31d47f8b89b4c73e47a9369d37d47695741c225cc30dd55f437 showcase/02_watercolor_outpaint_source.webp
16
+ dc1612b94e9ec3b07bdc4abb1b7480e3068537e94f9bedd34758ec06dd4dbdae showcase/03_3d_outpaint_mask.png
17
+ e586b4c9105b1ab9f4d83c1ed200c067d6735e6084925ebc8ee3ce051ae3ab9a showcase/03_3d_outpaint_result.webp
18
+ 447be13e6afb4f534fb3520e8649ee380eba5da12cdb3e9538fdecc2bd5b8578 showcase/03_3d_outpaint_source.webp
19
+ 2f2ac67652f469418fc767cd53afee2e51c69d0a8ba88aaf5dcad08c519609f7 showcase/04_portrait_mixed_mask.png
20
+ 9d2d3c20ac925d9f6fb07e3793a7e988bef1340d9461e0a9e96b76927ad2f571 showcase/04_portrait_mixed_result.webp
21
+ aae5ff6ba55f6ed47bdb931c3a8af13f6e6c4ef9216160dc34c8bb2305405c69 showcase/04_portrait_mixed_source.webp
22
+ ce6b35714e994c8fd1ad3f3aafcf2389346f9a751863f0036756a7d020645ec2 showcase/05_botanical_disconnected_mask.png
23
+ 56c262a0c3a7b05dc4d1995f2acfd70853ddeaa87fff120b806754e3c72bcb30 showcase/05_botanical_disconnected_result.webp
24
+ e5b791b266899fb92e46ffc2c81d43f8c25294de63b1c876a9d200520f442f21 showcase/05_botanical_disconnected_source.webp
25
+ 97c499416ede55b54d133c6e327980cb51eb845ebdf82de0051060a85b3feb04 showcase/06_interior_boundary_mask.png
26
+ 2235d5edd1e8a39540e46a0f85497175ea26f83fc9ac6f06b352243449b74ac5 showcase/06_interior_boundary_result.webp
27
+ a76702fbe0d6aafa79b04fffdb372d4bc9fa5990a711093f0df865ad7d1d6223 showcase/06_interior_boundary_source.webp
28
+ ff93e38bd097083b609491bca7e102c5f47842644cba42c05730460ff2cbe9f3 showcase/07_product_narrow_mask.png
29
+ 140ccc45d4b818fb956cac316199c4c8b5145a62c5963f351c4c197da409231a showcase/07_product_narrow_result.webp
30
+ 5e80da9ec470e5d83197ec0a105d86ff500a84e180e7f5944e64d06067ac1e68 showcase/07_product_narrow_source.webp
31
+ 22d69caa07195da35b8cb9b46a82bfcccb2d722302dd6becac6c4438a56c7c06 showcase/08_landscape_complex_mask.png
32
+ f782f98d34c81e00d56a9ff5260cc2ef1fdffd0b826a4a0278f65ac82057ac55 showcase/08_landscape_complex_result.webp
33
+ 59eb9767eff575a1914a9405e0b58996191b76e7906ec2d862996bf6edb71f14 showcase/08_landscape_complex_source.webp
34
+ 8787f390fb834576b464a0669d6f4c5838a011d95549092f4c9fc7ec15c96d07 showcase/README.md
35
+ 0b65fceaa9f08e0d6ac01ca60462d8c29ceaf4dc1f65550448595c72e07ae041 showcase/contact_sheet.webp
36
+ 4ee61dad3c37917420231ce7cc6e72e4c94d645a4f1d156764446164699d6192 showcase/manifest.json
anypaint.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from PIL import Image, ImageOps
8
+
9
+
10
+ REFERENCE_MAX_EDGE = 384
11
+ SEAM_PX = 32
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class AnyPaintInput:
16
+ condition: Image.Image
17
+ known_image: Image.Image
18
+ keep_mask: Image.Image
19
+ generated_mask: Image.Image
20
+ canvas_size: tuple[int, int]
21
+ source_bbox: tuple[int, int, int, int]
22
+
23
+ @property
24
+ def reference_placement(self) -> dict[str, list[float]]:
25
+ return {"bbox_normalized": [0.0, 0.0, 1.0, 1.0]}
26
+
27
+
28
+ def _validate_canvas(canvas_size: tuple[int, int]) -> None:
29
+ width, height = canvas_size
30
+ if width < 16 or height < 16 or width % 16 or height % 16:
31
+ raise ValueError("Canvas dimensions must be positive multiples of 16")
32
+
33
+
34
+ def _resize_max_edge(image: Image.Image, max_edge: int) -> Image.Image:
35
+ if max(image.size) <= max_edge:
36
+ return image.copy()
37
+ scale = max_edge / max(image.size)
38
+ size = (max(1, round(image.width * scale)), max(1, round(image.height * scale)))
39
+ return image.resize(size, Image.Resampling.LANCZOS)
40
+
41
+
42
+ def _median_color(values: np.ndarray) -> np.ndarray:
43
+ if values.size == 0:
44
+ return np.array([127, 127, 127], dtype=np.uint8)
45
+ return np.median(values.reshape(-1, 3), axis=0).round().astype(np.uint8)
46
+
47
+
48
+ def _edge_aware_keep_mask(generated_mask: Image.Image, seam_px: int) -> Image.Image:
49
+ generated = np.where(
50
+ np.asarray(generated_mask.convert("L")) > 0,
51
+ 255,
52
+ 0,
53
+ ).astype(np.uint8)
54
+ if seam_px > 0:
55
+ kernel = cv2.getStructuringElement(
56
+ cv2.MORPH_RECT,
57
+ (seam_px * 2 + 1, seam_px * 2 + 1),
58
+ )
59
+ generated = cv2.dilate(generated, kernel)
60
+ return ImageOps.invert(Image.fromarray(generated, mode="L"))
61
+
62
+
63
+ def prepare_anypaint(
64
+ source: Image.Image,
65
+ generated_mask: Image.Image,
66
+ canvas_size: tuple[int, int],
67
+ source_bbox: tuple[int, int, int, int] | None = None,
68
+ *,
69
+ reference_max_edge: int = REFERENCE_MAX_EDGE,
70
+ seam_px: int = SEAM_PX,
71
+ ) -> AnyPaintInput:
72
+ """Prepare one arbitrary-mask inpainting/outpainting request.
73
+
74
+ White mask pixels are generated. Black mask pixels are preserved. Pixels
75
+ outside ``source_bbox`` are always generated, which makes the same helper
76
+ work for both inpainting and outpainting.
77
+ """
78
+ _validate_canvas(canvas_size)
79
+ width, height = canvas_size
80
+ source = source.convert("RGB")
81
+ if source_bbox is None:
82
+ source_bbox = (0, 0, width, height)
83
+ x0, y0, x1, y1 = source_bbox
84
+ if not (0 <= x0 < x1 <= width and 0 <= y0 < y1 <= height):
85
+ raise ValueError(f"Source bbox is outside the canvas: {source_bbox}")
86
+
87
+ box_width, box_height = x1 - x0, y1 - y0
88
+ source_ratio = source.width / source.height
89
+ box_ratio = box_width / box_height
90
+ tolerance = max(0.025, 2.0 / min(box_width, box_height))
91
+ if abs(box_ratio / source_ratio - 1.0) > tolerance:
92
+ raise ValueError("Source bbox must preserve the source image aspect ratio")
93
+
94
+ placed = source.resize((box_width, box_height), Image.Resampling.LANCZOS)
95
+ placed_values = np.asarray(placed, dtype=np.uint8)
96
+ fill = _median_color(placed_values)
97
+ known_values = np.empty((height, width, 3), dtype=np.uint8)
98
+ known_values[:] = fill
99
+ known_values[y0:y1, x0:x1] = placed_values
100
+ known_image = Image.fromarray(known_values, mode="RGB")
101
+
102
+ mask = generated_mask.convert("L")
103
+ if mask.size == source.size:
104
+ mask = mask.resize((box_width, box_height), Image.Resampling.NEAREST)
105
+ canvas_mask = Image.new("L", canvas_size, 255)
106
+ canvas_mask.paste(mask, (x0, y0))
107
+ mask = canvas_mask
108
+ elif mask.size != canvas_size:
109
+ raise ValueError("Mask must match either the source image or the output canvas")
110
+ mask = mask.point(lambda value: 255 if value > 127 else 0)
111
+
112
+ outside = Image.new("L", canvas_size, 255)
113
+ outside.paste(0, source_bbox)
114
+ generated = Image.fromarray(
115
+ np.maximum(np.asarray(mask, dtype=np.uint8), np.asarray(outside, dtype=np.uint8)),
116
+ mode="L",
117
+ )
118
+ generated_values = np.asarray(generated, dtype=np.uint8) > 0
119
+ if not generated_values.any():
120
+ raise ValueError("The generated mask has no white pixels")
121
+
122
+ condition_values = known_values.copy()
123
+ known_pixels = condition_values[~generated_values]
124
+ condition_values[generated_values] = _median_color(known_pixels)
125
+ condition = _resize_max_edge(
126
+ Image.fromarray(condition_values, mode="RGB"),
127
+ reference_max_edge,
128
+ )
129
+
130
+ return AnyPaintInput(
131
+ condition=condition,
132
+ known_image=known_image,
133
+ keep_mask=_edge_aware_keep_mask(generated, seam_px),
134
+ generated_mask=generated,
135
+ canvas_size=canvas_size,
136
+ source_bbox=source_bbox,
137
+ )
example.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ import torch
7
+ from diffusers import DiffusionPipeline
8
+ from PIL import Image
9
+
10
+ from anypaint import prepare_anypaint
11
+
12
+
13
+ REPO_ID = "yijunwang2/krea2-anypaint"
14
+ WEIGHT_NAME = "krea2_anypaint_rank32.safetensors"
15
+
16
+
17
+ def parse_args() -> argparse.Namespace:
18
+ parser = argparse.ArgumentParser(
19
+ description="Run arbitrary-mask Krea 2 inpainting and outpainting"
20
+ )
21
+ parser.add_argument("--source", type=Path, required=True)
22
+ parser.add_argument("--mask", type=Path, required=True)
23
+ parser.add_argument("--output", type=Path, required=True)
24
+ parser.add_argument("--width", type=int, required=True)
25
+ parser.add_argument("--height", type=int, required=True)
26
+ parser.add_argument(
27
+ "--bbox",
28
+ type=int,
29
+ nargs=4,
30
+ metavar=("X0", "Y0", "X1", "Y1"),
31
+ help="Source placement in output pixels; defaults to the full canvas",
32
+ )
33
+ parser.add_argument("--prompt", required=True)
34
+ parser.add_argument("--steps", type=int, default=8)
35
+ parser.add_argument("--seed", type=int, default=42)
36
+ parser.add_argument("--lora-scale", type=float, default=1.0)
37
+ return parser.parse_args()
38
+
39
+
40
+ def main() -> None:
41
+ args = parse_args()
42
+ prepared = prepare_anypaint(
43
+ Image.open(args.source),
44
+ Image.open(args.mask),
45
+ (args.width, args.height),
46
+ tuple(args.bbox) if args.bbox else None,
47
+ )
48
+
49
+ pipe = DiffusionPipeline.from_pretrained(
50
+ "krea/Krea-2-Turbo",
51
+ custom_pipeline=REPO_ID,
52
+ trust_remote_code=True,
53
+ torch_dtype=torch.bfloat16,
54
+ ).to("cuda")
55
+ pipe.load_lora_weights(REPO_ID, weight_name=WEIGHT_NAME, adapter_name="anypaint")
56
+ pipe.set_adapters(["anypaint"], adapter_weights=[args.lora_scale])
57
+
58
+ generator = torch.Generator(device="cpu").manual_seed(args.seed)
59
+ result = pipe(
60
+ prompt=args.prompt,
61
+ image=prepared.condition,
62
+ width=args.width,
63
+ height=args.height,
64
+ num_inference_steps=args.steps,
65
+ guidance_scale=0.0,
66
+ generator=generator,
67
+ reference_max_pixels=384 * 384,
68
+ reference_placements=[prepared.reference_placement],
69
+ vlm_reference_indices=[0],
70
+ encode_reference_in_prompt=False,
71
+ kv_cache=True,
72
+ known_image=prepared.known_image,
73
+ known_mask=prepared.keep_mask,
74
+ ).images[0]
75
+
76
+ args.output.parent.mkdir(parents=True, exist_ok=True)
77
+ result.save(args.output)
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
krea2_anypaint_rank32.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3a7d09f6b27f8ead160d340f2f59c11f4ee635c4a1ee87ffd8b1b9f9ba412f7a
3
+ size 228587752
pipeline.py ADDED
@@ -0,0 +1,1580 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 Ostris, LLC. All rights reserved.
2
+ #
3
+ # Portions of the Krea2Transformer2DModel implementation are adapted from
4
+ # huggingface/diffusers (Apache License, Version 2.0), Copyright 2026 Krea AI
5
+ # and The HuggingFace Team.
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ """Krea2OstrisEdit -- a self-contained Hugging Face community pipeline for Krea 2
19
+ with reference-image (edit) conditioning and Ostris AI-Toolkit LoRA loading.
20
+
21
+ Everything lives in this one file so it can be hosted as a hub community
22
+ pipeline (a model repo containing just this ``pipeline.py``):
23
+
24
+ ```python
25
+ import torch
26
+ from diffusers import DiffusionPipeline
27
+ from PIL import Image
28
+
29
+ pipe = DiffusionPipeline.from_pretrained(
30
+ "krea/Krea-2-Turbo",
31
+ custom_pipeline="ostris/Krea2OstrisEdit",
32
+ torch_dtype=torch.bfloat16,
33
+ )
34
+ pipe.to("cuda") # or pipe.enable_model_cpu_offload() on GPUs with < ~40 GB VRAM
35
+
36
+ # Load an AI-Toolkit (or already-diffusers-format) Krea 2 LoRA, e.g. the style
37
+ # reference LoRA (generates the prompt in the style of the reference images).
38
+ pipe.load_lora_weights(
39
+ "ostris/krea2_turbo_style_reference", weight_name="krea2_style_reference.safetensors"
40
+ )
41
+
42
+ image = pipe(
43
+ "a white yeti with horns reading a book",
44
+ image=Image.open("style_reference.png"), # one reference image or a list of them
45
+ num_inference_steps=8, # Turbo defaults; the base model wants 28 / 4.5
46
+ guidance_scale=0.0,
47
+ # kv_cache=True, # reference K/V computed once and reused every step; only for
48
+ # # LoRAs trained with AI-Toolkit's kv_cache model kwarg
49
+ ).images[0]
50
+ image.save("output.png")
51
+ ```
52
+
53
+ Reference images condition the model in two places, matching how the edit LoRAs
54
+ are trained with Ostris AI-Toolkit (and the ComfyUI-Krea2-Ostris-Edit nodes):
55
+
56
+ 1. through the Qwen3-VL text encoder: each image is embedded in the user message
57
+ ahead of the prompt via ``Picture N: <|vision_start|><|image_pad|><|vision_end|>``
58
+ placeholders, so the text embeddings "see" the references;
59
+ 2. as clean VAE latents appended after the noisy image tokens in the transformer
60
+ sequence. They keep the flow time ``t=0`` (they are never noised) and sit on
61
+ rotary-position frame axis ``i + 1`` -- the Kontext-style "index" placement.
62
+
63
+ Without ``image`` the pipeline is a plain Krea 2 text-to-image sampler.
64
+ """
65
+
66
+ import math
67
+ import os
68
+ import re
69
+ from dataclasses import dataclass
70
+ from typing import Any, Dict, List, Optional, Tuple, Union
71
+
72
+ import numpy as np
73
+ import PIL.Image
74
+ import torch
75
+ import torch.nn as nn
76
+ import torch.nn.functional as F
77
+
78
+ import diffusers
79
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
80
+ from diffusers.image_processor import VaeImageProcessor
81
+ from diffusers.loaders import PeftAdapterMixin
82
+ from diffusers.models import AutoencoderKLQwenImage
83
+ from diffusers.models.modeling_outputs import Transformer2DModelOutput
84
+ from diffusers.models.modeling_utils import ModelMixin
85
+ from diffusers.pipelines.pipeline_utils import DiffusionPipeline
86
+ from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
87
+ from diffusers.utils import USE_PEFT_BACKEND, BaseOutput, logging, scale_lora_layers, unscale_lora_layers
88
+ from diffusers.utils.torch_utils import randn_tensor
89
+
90
+ try:
91
+ from transformers import AutoTokenizer, Qwen3VLModel
92
+ except ImportError as e: # pragma: no cover
93
+ raise ImportError(
94
+ "Krea2OstrisEdit requires a transformers version that ships Qwen3-VL "
95
+ "(`transformers>=4.57`). Please upgrade transformers."
96
+ ) from e
97
+
98
+
99
+ logger = logging.get_logger(__name__) # pylint: disable=invalid-name
100
+
101
+ # torch>=2.5 supports grouped-query attention natively in SDPA; older versions
102
+ # need the key/value heads repeated to the query head count.
103
+ _SDPA_HAS_GQA = tuple(int(re.sub(r"\D.*", "", v) or 0) for v in torch.__version__.split(".")[:2]) >= (2, 5)
104
+
105
+
106
+ # ---------------------------------------------------------------------------
107
+ # Transformer (Krea 2 single-stream MMDiT)
108
+ #
109
+ # Module tree and state-dict keys match the `Krea2Transformer2DModel` checkpoint
110
+ # layout in the `transformer/` folder of the Krea 2 hub repos, so the sharded
111
+ # weights load directly. The forward pass additionally supports clean reference
112
+ # tokens appended after the image tokens (`ref_seq_len`), which are modulated at
113
+ # flow time t=0 while the text + noisy image tokens keep the real timestep.
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ class Krea2RMSNorm(nn.Module):
118
+ """RMSNorm with a zero-centered scale: the effective multiplier is ``1 + weight``,
119
+ matching the Krea 2 checkpoint format. Normalization runs in float32."""
120
+
121
+ def __init__(self, dim: int, eps: float = 1e-5) -> None:
122
+ super().__init__()
123
+ self.dim = dim
124
+ self.eps = eps
125
+ self.weight = nn.Parameter(torch.zeros(dim))
126
+
127
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
128
+ dtype = hidden_states.dtype
129
+ hidden_states = F.rms_norm(
130
+ hidden_states.float(), (self.dim,), weight=self.weight.float() + 1.0, eps=self.eps
131
+ )
132
+ return hidden_states.to(dtype)
133
+
134
+
135
+ def _apply_rotary_emb(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
136
+ """Rotate interleaved (even, odd) channel pairs. ``x`` is (B, H, S, D); ``cos``/``sin``
137
+ are (S, D) in the repeat-interleaved layout produced by ``Krea2RotaryPosEmbed``."""
138
+ x_f = x.float()
139
+ x_rot = torch.stack((-x_f[..., 1::2], x_f[..., 0::2]), dim=-1).flatten(-2)
140
+ return (x_f * cos + x_rot * sin).to(x.dtype)
141
+
142
+
143
+ class Krea2RotaryPosEmbed(nn.Module):
144
+ def __init__(self, theta: float, axes_dim: List[int]) -> None:
145
+ super().__init__()
146
+ self.theta = theta
147
+ self.axes_dim = axes_dim
148
+
149
+ def forward(self, ids: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
150
+ # ids: (seq_len, 3) rotary coordinates. Frequencies are computed in float64
151
+ # (float32 on backends without float64 support, e.g. MPS).
152
+ dtype = torch.float32 if ids.device.type == "mps" else torch.float64
153
+ angles = []
154
+ for i, dim in enumerate(self.axes_dim):
155
+ pos = ids[:, i].to(dtype)
156
+ freqs = 1.0 / (self.theta ** (torch.arange(0, dim, 2, dtype=dtype, device=ids.device) / dim))
157
+ angles.append(pos[:, None] * freqs[None, :])
158
+ angles = torch.cat(angles, dim=-1)
159
+ cos = angles.cos().repeat_interleave(2, dim=-1).float()
160
+ sin = angles.sin().repeat_interleave(2, dim=-1).float()
161
+ return cos, sin
162
+
163
+
164
+ class Krea2Attention(nn.Module):
165
+ """Self-attention with grouped-query projections, q/k RMSNorm, rotary embeddings
166
+ and a sigmoid output gate."""
167
+
168
+ def __init__(self, hidden_size: int, num_heads: int, num_kv_heads: Optional[int] = None, eps: float = 1e-5):
169
+ super().__init__()
170
+ self.num_heads = num_heads
171
+ self.num_kv_heads = num_kv_heads if num_kv_heads is not None else num_heads
172
+ self.head_dim = hidden_size // num_heads
173
+
174
+ self.to_q = nn.Linear(hidden_size, self.head_dim * self.num_heads, bias=False)
175
+ self.to_k = nn.Linear(hidden_size, self.head_dim * self.num_kv_heads, bias=False)
176
+ self.to_v = nn.Linear(hidden_size, self.head_dim * self.num_kv_heads, bias=False)
177
+ self.to_gate = nn.Linear(hidden_size, hidden_size, bias=False)
178
+ self.norm_q = Krea2RMSNorm(self.head_dim, eps=eps)
179
+ self.norm_k = Krea2RMSNorm(self.head_dim, eps=eps)
180
+ self.to_out = nn.ModuleList([nn.Linear(hidden_size, hidden_size, bias=False), nn.Dropout(0.0)])
181
+
182
+ def forward(
183
+ self,
184
+ hidden_states: torch.Tensor,
185
+ attention_mask: Optional[torch.Tensor] = None,
186
+ image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
187
+ kv_capture: Optional[list] = None,
188
+ kv_cache: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
189
+ ) -> torch.Tensor:
190
+ query = self.to_q(hidden_states).unflatten(-1, (self.num_heads, self.head_dim)).transpose(1, 2)
191
+ key = self.to_k(hidden_states).unflatten(-1, (self.num_kv_heads, self.head_dim)).transpose(1, 2)
192
+ value = self.to_v(hidden_states).unflatten(-1, (self.num_kv_heads, self.head_dim)).transpose(1, 2)
193
+ gate = self.to_gate(hidden_states)
194
+
195
+ query = self.norm_q(query)
196
+ key = self.norm_k(key)
197
+
198
+ if image_rotary_emb is not None:
199
+ cos, sin = image_rotary_emb
200
+ query = _apply_rotary_emb(query, cos, sin)
201
+ key = _apply_rotary_emb(key, cos, sin)
202
+
203
+ if kv_capture is not None:
204
+ kv_capture.append((key, value))
205
+ if kv_cache is not None:
206
+ # Cached reference K/V, already rotary-embedded at their original positions.
207
+ key = torch.cat([key, kv_cache[0].to(key.dtype)], dim=2)
208
+ value = torch.cat([value, kv_cache[1].to(value.dtype)], dim=2)
209
+
210
+ is_gqa = self.num_heads != self.num_kv_heads
211
+ if is_gqa and not _SDPA_HAS_GQA:
212
+ key = key.repeat_interleave(self.num_heads // self.num_kv_heads, dim=1)
213
+ value = value.repeat_interleave(self.num_heads // self.num_kv_heads, dim=1)
214
+ sdpa_kwargs = {"enable_gqa": True} if (is_gqa and _SDPA_HAS_GQA) else {}
215
+ hidden_states = F.scaled_dot_product_attention(query, key, value, attn_mask=attention_mask, **sdpa_kwargs)
216
+
217
+ hidden_states = hidden_states.transpose(1, 2).flatten(2)
218
+ hidden_states = hidden_states * torch.sigmoid(gate)
219
+ return self.to_out[0](hidden_states)
220
+
221
+
222
+ class Krea2SwiGLU(nn.Module):
223
+ def __init__(self, dim: int, hidden_dim: int) -> None:
224
+ super().__init__()
225
+ self.gate = nn.Linear(dim, hidden_dim, bias=False)
226
+ self.up = nn.Linear(dim, hidden_dim, bias=False)
227
+ self.down = nn.Linear(hidden_dim, dim, bias=False)
228
+
229
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
230
+ return self.down(F.silu(self.gate(hidden_states)) * self.up(hidden_states))
231
+
232
+
233
+ class Krea2TextFusionBlock(nn.Module):
234
+ """Pre-norm transformer block (no rotary embeddings, no time modulation) used by
235
+ the text fusion stage."""
236
+
237
+ def __init__(self, dim: int, num_heads: int, num_kv_heads: int, intermediate_size: int, eps: float) -> None:
238
+ super().__init__()
239
+ self.norm1 = Krea2RMSNorm(dim, eps=eps)
240
+ self.norm2 = Krea2RMSNorm(dim, eps=eps)
241
+ self.attn = Krea2Attention(dim, num_heads, num_kv_heads, eps=eps)
242
+ self.ff = Krea2SwiGLU(dim, intermediate_size)
243
+
244
+ def forward(self, hidden_states: torch.Tensor, attention_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
245
+ hidden_states = hidden_states + self.attn(self.norm1(hidden_states), attention_mask=attention_mask)
246
+ hidden_states = hidden_states + self.ff(self.norm2(hidden_states))
247
+ return hidden_states
248
+
249
+
250
+ class Krea2TextFusion(nn.Module):
251
+ """Fuses the stack of tapped text-encoder hidden states into one text sequence:
252
+ ``layerwise_blocks`` attend across the layer axis per token, a linear ``projector``
253
+ collapses that axis, and ``refiner_blocks`` attend across the token sequence."""
254
+
255
+ def __init__(
256
+ self,
257
+ num_text_layers: int,
258
+ dim: int,
259
+ num_heads: int,
260
+ num_kv_heads: int,
261
+ intermediate_size: int,
262
+ num_layerwise_blocks: int,
263
+ num_refiner_blocks: int,
264
+ eps: float,
265
+ ) -> None:
266
+ super().__init__()
267
+ self.layerwise_blocks = nn.ModuleList(
268
+ [
269
+ Krea2TextFusionBlock(dim, num_heads, num_kv_heads, intermediate_size, eps)
270
+ for _ in range(num_layerwise_blocks)
271
+ ]
272
+ )
273
+ self.projector = nn.Linear(num_text_layers, 1, bias=False)
274
+ self.refiner_blocks = nn.ModuleList(
275
+ [
276
+ Krea2TextFusionBlock(dim, num_heads, num_kv_heads, intermediate_size, eps)
277
+ for _ in range(num_refiner_blocks)
278
+ ]
279
+ )
280
+
281
+ def forward(self, encoder_hidden_states: torch.Tensor, attention_mask: Optional[torch.Tensor] = None):
282
+ batch_size, seq_len, num_text_layers, dim = encoder_hidden_states.shape
283
+
284
+ hidden_states = encoder_hidden_states.reshape(batch_size * seq_len, num_text_layers, dim)
285
+ for block in self.layerwise_blocks:
286
+ hidden_states = block(hidden_states.contiguous())
287
+
288
+ hidden_states = hidden_states.reshape(batch_size, seq_len, num_text_layers, dim).permute(0, 1, 3, 2)
289
+ hidden_states = self.projector(hidden_states).squeeze(-1)
290
+
291
+ for block in self.refiner_blocks:
292
+ hidden_states = block(hidden_states, attention_mask=attention_mask)
293
+
294
+ return hidden_states
295
+
296
+
297
+ class Krea2TransformerBlock(nn.Module):
298
+ def __init__(
299
+ self, hidden_size: int, intermediate_size: int, num_heads: int, num_kv_heads: int, norm_eps: float
300
+ ) -> None:
301
+ super().__init__()
302
+ self.scale_shift_table = nn.Parameter(torch.zeros(6, hidden_size))
303
+ self.norm1 = Krea2RMSNorm(hidden_size, eps=norm_eps)
304
+ self.norm2 = Krea2RMSNorm(hidden_size, eps=norm_eps)
305
+ self.attn = Krea2Attention(hidden_size, num_heads, num_kv_heads, eps=norm_eps)
306
+ self.ff = Krea2SwiGLU(hidden_size, intermediate_size)
307
+
308
+ def forward(
309
+ self,
310
+ hidden_states: torch.Tensor,
311
+ temb: Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor, int]],
312
+ image_rotary_emb: Tuple[torch.Tensor, torch.Tensor],
313
+ attention_mask: Optional[torch.Tensor] = None,
314
+ kv_capture: Optional[list] = None,
315
+ kv_cache: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
316
+ ) -> torch.Tensor:
317
+ # ``temb`` is the (B, 1, 6 * hidden_size) modulation input, or a tuple
318
+ # ``(temb, ref_temb, split)`` for reference-image conditioning: tokens
319
+ # ``[:split]`` (text + noisy image) are modulated with the real timestep
320
+ # while tokens ``[split:]`` (clean reference tokens) use the t=0 embedding.
321
+ if isinstance(temb, tuple):
322
+ temb, ref_temb, split = temb
323
+ m = (temb.unflatten(-1, (6, -1)) + self.scale_shift_table).unbind(-2)
324
+ r = (ref_temb.unflatten(-1, (6, -1)) + self.scale_shift_table).unbind(-2)
325
+
326
+ def modulate(h, scale_idx, shift_idx):
327
+ return torch.cat(
328
+ (
329
+ (1.0 + m[scale_idx]) * h[:, :split] + m[shift_idx],
330
+ (1.0 + r[scale_idx]) * h[:, split:] + r[shift_idx],
331
+ ),
332
+ dim=1,
333
+ )
334
+
335
+ def gate(h, gate_idx):
336
+ return torch.cat((m[gate_idx] * h[:, :split], r[gate_idx] * h[:, split:]), dim=1)
337
+
338
+ attn_out = self.attn(
339
+ modulate(self.norm1(hidden_states), 0, 1),
340
+ attention_mask=attention_mask,
341
+ image_rotary_emb=image_rotary_emb,
342
+ kv_capture=kv_capture,
343
+ kv_cache=kv_cache,
344
+ )
345
+ hidden_states = hidden_states + gate(attn_out, 2)
346
+ ff_out = self.ff(modulate(self.norm2(hidden_states), 3, 4))
347
+ hidden_states = hidden_states + gate(ff_out, 5)
348
+ return hidden_states
349
+
350
+ modulation = temb.unflatten(-1, (6, -1)) + self.scale_shift_table
351
+ prescale, preshift, pregate, postscale, postshift, postgate = modulation.unbind(-2)
352
+
353
+ attn_out = self.attn(
354
+ (1.0 + prescale) * self.norm1(hidden_states) + preshift,
355
+ attention_mask=attention_mask,
356
+ image_rotary_emb=image_rotary_emb,
357
+ kv_capture=kv_capture,
358
+ kv_cache=kv_cache,
359
+ )
360
+ hidden_states = hidden_states + pregate * attn_out
361
+ ff_out = self.ff((1.0 + postscale) * self.norm2(hidden_states) + postshift)
362
+ hidden_states = hidden_states + postgate * ff_out
363
+ return hidden_states
364
+
365
+
366
+ class Krea2TimestepEmbedding(nn.Module):
367
+ """Sinusoidal flow-time embedding (cos-first, input scaled by 1000) followed by a
368
+ two-layer MLP. Keeps the sequence dimension at size 1 so per-block modulations
369
+ broadcast over tokens."""
370
+
371
+ def __init__(self, embed_dim: int, hidden_size: int) -> None:
372
+ super().__init__()
373
+ self.embed_dim = embed_dim
374
+ self.linear_1 = nn.Linear(embed_dim, hidden_size, bias=True)
375
+ self.linear_2 = nn.Linear(hidden_size, hidden_size, bias=True)
376
+
377
+ def forward(self, timestep: torch.Tensor, dtype: torch.dtype) -> torch.Tensor:
378
+ half = self.embed_dim // 2
379
+ freqs = torch.exp(-math.log(1e4) * torch.arange(half, dtype=torch.float32, device=timestep.device) / half)
380
+ args = (timestep.float() * 1e3)[:, None, None] * freqs
381
+ emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1).to(dtype)
382
+ return self.linear_2(F.gelu(self.linear_1(emb), approximate="tanh"))
383
+
384
+
385
+ class Krea2TextProjection(nn.Module):
386
+ """Projects the fused text features into the transformer width."""
387
+
388
+ def __init__(self, text_dim: int, hidden_size: int, eps: float) -> None:
389
+ super().__init__()
390
+ self.norm = Krea2RMSNorm(text_dim, eps=eps)
391
+ self.linear_1 = nn.Linear(text_dim, hidden_size, bias=True)
392
+ self.linear_2 = nn.Linear(hidden_size, hidden_size, bias=True)
393
+
394
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
395
+ hidden_states = self.linear_1(self.norm(hidden_states))
396
+ return self.linear_2(F.gelu(hidden_states, approximate="tanh"))
397
+
398
+
399
+ class Krea2FinalLayer(nn.Module):
400
+ """Final adaptive RMSNorm and output projection."""
401
+
402
+ def __init__(self, hidden_size: int, out_channels: int, eps: float) -> None:
403
+ super().__init__()
404
+ self.scale_shift_table = nn.Parameter(torch.zeros(2, hidden_size))
405
+ self.norm = Krea2RMSNorm(hidden_size, eps=eps)
406
+ self.linear = nn.Linear(hidden_size, out_channels, bias=True)
407
+
408
+ def forward(self, hidden_states: torch.Tensor, temb: torch.Tensor) -> torch.Tensor:
409
+ modulation = temb + self.scale_shift_table
410
+ scale, shift = modulation.chunk(2, dim=1)
411
+ hidden_states = (1.0 + scale) * self.norm(hidden_states) + shift
412
+ return self.linear(hidden_states)
413
+
414
+
415
+ class Krea2Transformer2DModel(ModelMixin, ConfigMixin, PeftAdapterMixin):
416
+ r"""
417
+ The Krea 2 single-stream MMDiT flow-matching backbone, extended with support for
418
+ clean reference-image tokens ("edit" conditioning).
419
+
420
+ Text conditioning enters as a stack of hidden states tapped from several layers of
421
+ the Qwen3-VL text encoder. A small text-fusion transformer collapses the layer axis
422
+ and refines the token sequence; the result is concatenated with the patchified
423
+ image latents (and, optionally, packed reference latents) into a single
424
+ ``[text, image, refs]`` sequence processed by the transformer blocks.
425
+
426
+ When ``ref_seq_len > 0``, the last ``ref_seq_len`` tokens of ``hidden_states`` are
427
+ clean reference tokens: they are modulated with the t=0 timestep embedding
428
+ (Kontext-style "index_timestep_zero") and excluded from the returned velocity.
429
+ """
430
+
431
+ _supports_gradient_checkpointing = True
432
+ _no_split_modules = ["Krea2TransformerBlock", "Krea2TextFusionBlock", "Krea2FinalLayer"]
433
+ _keep_in_fp32_modules = ["norm", "norm1", "norm2", "norm_q", "norm_k"]
434
+ _skip_layerwise_casting_patterns = ["time_embed", "norm"]
435
+
436
+ @register_to_config
437
+ def __init__(
438
+ self,
439
+ in_channels: int = 64,
440
+ num_layers: int = 28,
441
+ attention_head_dim: int = 128,
442
+ num_attention_heads: int = 48,
443
+ num_key_value_heads: int = 12,
444
+ intermediate_size: int = 16384,
445
+ timestep_embed_dim: int = 256,
446
+ text_hidden_dim: int = 2560,
447
+ num_text_layers: int = 12,
448
+ text_num_attention_heads: int = 20,
449
+ text_num_key_value_heads: int = 20,
450
+ text_intermediate_size: int = 6912,
451
+ num_layerwise_text_blocks: int = 2,
452
+ num_refiner_text_blocks: int = 2,
453
+ axes_dims_rope: Tuple[int, int, int] = (32, 48, 48),
454
+ rope_theta: float = 1000.0,
455
+ norm_eps: float = 1e-5,
456
+ ) -> None:
457
+ super().__init__()
458
+
459
+ hidden_size = attention_head_dim * num_attention_heads
460
+ if sum(axes_dims_rope) != attention_head_dim:
461
+ raise ValueError(
462
+ f"sum(axes_dims_rope)={sum(axes_dims_rope)} must equal attention_head_dim={attention_head_dim}"
463
+ )
464
+
465
+ self.in_channels = in_channels
466
+ self.out_channels = in_channels
467
+ self.hidden_size = hidden_size
468
+ self.gradient_checkpointing = False
469
+
470
+ self.img_in = nn.Linear(in_channels, hidden_size, bias=True)
471
+ self.time_embed = Krea2TimestepEmbedding(timestep_embed_dim, hidden_size)
472
+ self.time_mod_proj = nn.Linear(hidden_size, 6 * hidden_size, bias=True)
473
+ self.text_fusion = Krea2TextFusion(
474
+ num_text_layers=num_text_layers,
475
+ dim=text_hidden_dim,
476
+ num_heads=text_num_attention_heads,
477
+ num_kv_heads=text_num_key_value_heads,
478
+ intermediate_size=text_intermediate_size,
479
+ num_layerwise_blocks=num_layerwise_text_blocks,
480
+ num_refiner_blocks=num_refiner_text_blocks,
481
+ eps=norm_eps,
482
+ )
483
+ self.txt_in = Krea2TextProjection(text_hidden_dim, hidden_size, eps=norm_eps)
484
+ self.rotary_emb = Krea2RotaryPosEmbed(theta=rope_theta, axes_dim=list(axes_dims_rope))
485
+
486
+ self.transformer_blocks = nn.ModuleList(
487
+ [
488
+ Krea2TransformerBlock(
489
+ hidden_size=hidden_size,
490
+ intermediate_size=intermediate_size,
491
+ num_heads=num_attention_heads,
492
+ num_kv_heads=num_key_value_heads,
493
+ norm_eps=norm_eps,
494
+ )
495
+ for _ in range(num_layers)
496
+ ]
497
+ )
498
+
499
+ self.final_layer = Krea2FinalLayer(hidden_size, out_channels=in_channels, eps=norm_eps)
500
+
501
+ def precompute_ref_kv(
502
+ self,
503
+ hidden_states: torch.Tensor,
504
+ position_ids: torch.Tensor,
505
+ attention_kwargs: Optional[Dict[str, Any]] = None,
506
+ ) -> List[Tuple[torch.Tensor, torch.Tensor]]:
507
+ r"""
508
+ Run only the packed clean reference tokens through the transformer blocks at t=0
509
+ and return each block's rotary-embedded key/value pair.
510
+
511
+ Only valid for adapters trained with AI-Toolkit's ``kv_cache`` model kwarg,
512
+ where reference tokens attend solely to each other: their per-block K/V are
513
+ then independent of the timestep and of the rest of the sequence, so this
514
+ single pass serves every denoising step. Pass the result to
515
+ ``forward(..., ref_kv_cache=...)`` with the reference tokens dropped from
516
+ ``hidden_states`` and ``position_ids``.
517
+
518
+ Args:
519
+ hidden_states (`torch.Tensor` of shape `(batch_size, ref_seq_len, in_channels)`):
520
+ Packed reference latents (see the pipeline's `_pack_reference_latents`).
521
+ position_ids (`torch.Tensor` of shape `(ref_seq_len, 3)`):
522
+ The reference tokens' rotary coordinates.
523
+ attention_kwargs (`dict`, *optional*):
524
+ When it contains a `scale` entry, sets the LoRA scale, matching `forward`.
525
+
526
+ Returns:
527
+ A list with one `(key, value)` tuple per transformer block, each of shape
528
+ `(batch_size, num_key_value_heads, ref_seq_len, attention_head_dim)`.
529
+ """
530
+ lora_scale = 1.0
531
+ if attention_kwargs is not None:
532
+ lora_scale = attention_kwargs.get("scale", 1.0)
533
+ if USE_PEFT_BACKEND and lora_scale != 1.0:
534
+ scale_lora_layers(self, lora_scale)
535
+
536
+ # Clean reference tokens are always conditioned at flow time t=0.
537
+ timestep = torch.zeros(hidden_states.shape[0], device=hidden_states.device)
538
+ temb = self.time_embed(timestep, dtype=hidden_states.dtype)
539
+ temb_mod = self.time_mod_proj(F.gelu(temb, approximate="tanh"))
540
+
541
+ hidden_states = self.img_in(hidden_states)
542
+ image_rotary_emb = self.rotary_emb(position_ids)
543
+
544
+ ref_kv = []
545
+ for block in self.transformer_blocks:
546
+ captured = []
547
+ hidden_states = block(hidden_states, temb_mod, image_rotary_emb, kv_capture=captured)
548
+ ref_kv.append(captured[0])
549
+
550
+ if USE_PEFT_BACKEND and lora_scale != 1.0:
551
+ unscale_lora_layers(self, lora_scale)
552
+ return ref_kv
553
+
554
+ def forward(
555
+ self,
556
+ hidden_states: torch.Tensor,
557
+ encoder_hidden_states: torch.Tensor,
558
+ timestep: torch.Tensor,
559
+ position_ids: torch.Tensor,
560
+ encoder_attention_mask: Optional[torch.Tensor] = None,
561
+ ref_seq_len: int = 0,
562
+ ref_kv_cache: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None,
563
+ attention_kwargs: Optional[Dict[str, Any]] = None,
564
+ return_dict: bool = True,
565
+ ) -> Union[Transformer2DModelOutput, Tuple[torch.Tensor]]:
566
+ r"""
567
+ Predict the flow-matching velocity for the (noisy) image tokens.
568
+
569
+ Args:
570
+ hidden_states (`torch.Tensor` of shape `(batch_size, image_seq_len + ref_seq_len, in_channels)`):
571
+ Packed (patchified) noisy image latents, with any packed clean reference
572
+ latents appended at the end.
573
+ encoder_hidden_states (`torch.Tensor` of shape `(batch_size, text_seq_len, num_text_layers, text_hidden_dim)`):
574
+ Stack of tapped text-encoder hidden states per token.
575
+ timestep (`torch.Tensor` of shape `(batch_size,)`):
576
+ Flow-matching time in `[0, 1]` (1 is pure noise, 0 is clean data).
577
+ position_ids (`torch.Tensor` of shape `(text_seq_len + image_seq_len + ref_seq_len, 3)`):
578
+ `(t, h, w)` rotary coordinates for the combined sequence. Text rows are
579
+ all-zero; image rows hold the latent-grid coordinates; the i-th
580
+ reference image sits on frame axis `i + 1` with its own grid.
581
+ encoder_attention_mask (`torch.Tensor` of shape `(batch_size, text_seq_len)`, *optional*):
582
+ Boolean mask marking valid text tokens.
583
+ ref_seq_len (`int`, defaults to 0):
584
+ Number of trailing reference tokens in `hidden_states`. They receive the
585
+ t=0 modulation and are excluded from the output.
586
+ ref_kv_cache (`list[tuple[torch.Tensor, torch.Tensor]]`, *optional*):
587
+ Per-block reference K/V from [`~Krea2Transformer2DModel.precompute_ref_kv`].
588
+ When given, `hidden_states` / `position_ids` must not contain the
589
+ reference tokens (`ref_seq_len == 0`); each block's attention appends the
590
+ cached K/V as extra keys instead. Only valid for adapters trained with
591
+ AI-Toolkit's ``kv_cache`` model kwarg (isolated reference attention).
592
+ attention_kwargs (`dict`, *optional*):
593
+ When it contains a `scale` entry, sets the LoRA scale applied to this
594
+ transformer's adapters for the duration of the forward pass.
595
+
596
+ Returns:
597
+ The velocity tensor of shape `(batch_size, image_seq_len, in_channels)`.
598
+ """
599
+ if position_ids.ndim != 2 or position_ids.shape[-1] != 3:
600
+ raise ValueError(f"`position_ids` must have shape (sequence_length, 3), got {tuple(position_ids.shape)}.")
601
+ if ref_kv_cache is not None and ref_seq_len > 0:
602
+ raise ValueError(
603
+ "`ref_kv_cache` replaces the reference tokens; do not also append them to "
604
+ "`hidden_states` (`ref_seq_len` must be 0)."
605
+ )
606
+
607
+ lora_scale = 1.0
608
+ if attention_kwargs is not None:
609
+ attention_kwargs = attention_kwargs.copy()
610
+ lora_scale = attention_kwargs.pop("scale", 1.0)
611
+ if USE_PEFT_BACKEND and lora_scale != 1.0:
612
+ scale_lora_layers(self, lora_scale)
613
+
614
+ batch_size, image_seq_len, _ = hidden_states.shape # includes ref tokens
615
+ text_seq_len = encoder_hidden_states.shape[1]
616
+
617
+ temb = self.time_embed(timestep, dtype=hidden_states.dtype)
618
+ temb_mod = self.time_mod_proj(F.gelu(temb, approximate="tanh"))
619
+
620
+ block_temb = temb_mod
621
+ if ref_seq_len > 0:
622
+ # Clean reference tokens are conditioned at t=0; everything else keeps t.
623
+ temb_zero = self.time_embed(torch.zeros_like(timestep), dtype=hidden_states.dtype)
624
+ ref_temb_mod = self.time_mod_proj(F.gelu(temb_zero, approximate="tanh"))
625
+ block_temb = (temb_mod, ref_temb_mod, text_seq_len + image_seq_len - ref_seq_len)
626
+
627
+ # An all-True mask (no padded text tokens, e.g. any batch-of-1 call) is
628
+ # equivalent to no mask; passing None keeps SDPA on its fast, low-memory
629
+ # (flash) path instead of a mask-materializing fallback.
630
+ if encoder_attention_mask is not None and bool(encoder_attention_mask.all()):
631
+ encoder_attention_mask = None
632
+
633
+ text_attention_mask = None
634
+ attention_mask = None
635
+ if encoder_attention_mask is not None:
636
+ # Key-padding masks of shape (B, 1, 1, L): padded text tokens are excluded
637
+ # as attention keys everywhere; their own (garbage) lanes are never read
638
+ # back and are dropped at the output slice.
639
+ text_attention_mask = encoder_attention_mask[:, None, None, :]
640
+ image_mask = encoder_attention_mask.new_ones((batch_size, image_seq_len))
641
+ attention_mask = torch.cat([encoder_attention_mask, image_mask], dim=1)[:, None, None, :]
642
+ if ref_kv_cache is not None:
643
+ # Cached reference K/V are appended as extra (always-valid) keys.
644
+ ref_mask = attention_mask.new_ones((batch_size, 1, 1, ref_kv_cache[0][0].shape[2]))
645
+ attention_mask = torch.cat([attention_mask, ref_mask], dim=-1)
646
+
647
+ encoder_hidden_states = self.text_fusion(encoder_hidden_states, attention_mask=text_attention_mask)
648
+ encoder_hidden_states = self.txt_in(encoder_hidden_states)
649
+
650
+ hidden_states = self.img_in(hidden_states)
651
+ hidden_states = torch.cat([encoder_hidden_states, hidden_states], dim=1)
652
+
653
+ image_rotary_emb = self.rotary_emb(position_ids)
654
+
655
+ for i, block in enumerate(self.transformer_blocks):
656
+ if torch.is_grad_enabled() and self.gradient_checkpointing:
657
+ ckpt_func = getattr(self, "_gradient_checkpointing_func", None)
658
+ if ckpt_func is None:
659
+ hidden_states = torch.utils.checkpoint.checkpoint(
660
+ block, hidden_states, block_temb, image_rotary_emb, attention_mask, use_reentrant=False
661
+ )
662
+ else:
663
+ hidden_states = ckpt_func(block, hidden_states, block_temb, image_rotary_emb, attention_mask)
664
+ else:
665
+ hidden_states = block(
666
+ hidden_states,
667
+ block_temb,
668
+ image_rotary_emb,
669
+ attention_mask,
670
+ kv_cache=ref_kv_cache[i] if ref_kv_cache is not None else None,
671
+ )
672
+
673
+ hidden_states = hidden_states[:, text_seq_len : text_seq_len + image_seq_len - ref_seq_len]
674
+ output = self.final_layer(hidden_states, temb)
675
+
676
+ if USE_PEFT_BACKEND and lora_scale != 1.0:
677
+ unscale_lora_layers(self, lora_scale)
678
+
679
+ if not return_dict:
680
+ return (output,)
681
+ return Transformer2DModelOutput(sample=output)
682
+
683
+
684
+ # The Krea 2 hub repos' `model_index.json` points the `transformer` component at
685
+ # `["diffusers", "Krea2Transformer2DModel"]`. Registering the vendored class into the
686
+ # diffusers namespace lets `DiffusionPipeline.from_pretrained` resolve it on diffusers
687
+ # releases that don't ship Krea 2 yet, and guarantees the loaded transformer supports
688
+ # the reference-image forward pass this pipeline needs (the class is a numerically
689
+ # identical superset of the upstream one for text-to-image).
690
+ diffusers.Krea2Transformer2DModel = Krea2Transformer2DModel
691
+
692
+
693
+ # ---------------------------------------------------------------------------
694
+ # LoRA key conversion (Ostris AI-Toolkit / reference-trainer -> diffusers/PEFT)
695
+ # ---------------------------------------------------------------------------
696
+
697
+
698
+ def _convert_non_diffusers_krea2_lora_to_diffusers(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
699
+ """Map original `krea-ai/krea-2` module names onto `Krea2Transformer2DModel`.
700
+ Handles the `diffusion_model.` prefix (AI-Toolkit saves / ComfyUI) and the
701
+ `base_model.model.` prefix, as well as bare module names."""
702
+ state_dict = {
703
+ (k[len("base_model.model.") :] if k.startswith("base_model.model.") else k): v for k, v in state_dict.items()
704
+ }
705
+ state_dict = {
706
+ (k[len("diffusion_model.") :] if k.startswith("diffusion_model.") else k): v for k, v in state_dict.items()
707
+ }
708
+
709
+ attn_map = {"wq": "to_q", "wk": "to_k", "wv": "to_v", "wo": "to_out.0", "gate": "to_gate"}
710
+ ff_map = {"gate": "ff.gate", "up": "ff.up", "down": "ff.down"}
711
+ # The original model stores these standalone modules under abbreviated
712
+ # `nn.Sequential`-style names.
713
+ standalone_map = {
714
+ "first": "img_in",
715
+ "last.linear": "final_layer.linear",
716
+ "tmlp.0": "time_embed.linear_1",
717
+ "tmlp.2": "time_embed.linear_2",
718
+ "tproj.1": "time_mod_proj",
719
+ "txtmlp.1": "txt_in.linear_1",
720
+ "txtmlp.3": "txt_in.linear_2",
721
+ "txtfusion.projector": "text_fusion.projector",
722
+ }
723
+
724
+ def convert_module(module):
725
+ m = re.match(r"blocks\.(\d+)\.(attn|mlp)\.(\w+)$", module)
726
+ if m:
727
+ idx, kind, sub = m.groups()
728
+ if kind == "attn" and sub in attn_map:
729
+ return f"transformer_blocks.{idx}.attn.{attn_map[sub]}"
730
+ if kind == "mlp" and sub in ff_map:
731
+ return f"transformer_blocks.{idx}.{ff_map[sub]}"
732
+ return None
733
+ m = re.match(r"txtfusion\.(layerwise_blocks|refiner_blocks)\.(\d+)\.(attn|mlp)\.(\w+)$", module)
734
+ if m:
735
+ block, idx, kind, sub = m.groups()
736
+ if kind == "attn" and sub in attn_map:
737
+ return f"text_fusion.{block}.{idx}.attn.{attn_map[sub]}"
738
+ if kind == "mlp" and sub in ff_map:
739
+ return f"text_fusion.{block}.{idx}.{ff_map[sub]}"
740
+ return None
741
+ return standalone_map.get(module)
742
+
743
+ converted_state_dict = {}
744
+ for key in list(state_dict):
745
+ match = re.search(r"\.(?:lora_[AB])\.weight$", key)
746
+ if match is None:
747
+ continue
748
+ diffusers_module = convert_module(key[: match.start()])
749
+ if diffusers_module is None:
750
+ continue
751
+ converted_state_dict[f"transformer.{diffusers_module}{key[match.start() :]}"] = state_dict.pop(key)
752
+
753
+ if len(state_dict) > 0:
754
+ raise ValueError(f"Could not convert LoRA keys: {sorted(state_dict.keys())}")
755
+
756
+ return converted_state_dict
757
+
758
+
759
+ def _normalize_lora_state_dict(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
760
+ """Normalize a Krea 2 LoRA state dict to PEFT `lora_A`/`lora_B` naming and fold any
761
+ `.alpha` tensors into `lora_B` so the effective scale is preserved."""
762
+ state_dict = {
763
+ k.replace(".lora_down.weight", ".lora_A.weight").replace(".lora_up.weight", ".lora_B.weight"): v
764
+ for k, v in state_dict.items()
765
+ }
766
+ # PEFT assumes lora_alpha == rank (scale 1.0) when no alpha is given; fold any
767
+ # explicit alpha into lora_B instead of plumbing network_alphas through.
768
+ for alpha_key in [k for k in state_dict if k.endswith(".alpha")]:
769
+ base = alpha_key[: -len(".alpha")]
770
+ a_key, b_key = base + ".lora_A.weight", base + ".lora_B.weight"
771
+ alpha = float(state_dict.pop(alpha_key))
772
+ if a_key in state_dict and b_key in state_dict:
773
+ rank = state_dict[a_key].shape[0]
774
+ if alpha != rank:
775
+ state_dict[b_key] = state_dict[b_key] * (alpha / rank)
776
+ return state_dict
777
+
778
+
779
+ # ---------------------------------------------------------------------------
780
+ # Pipeline
781
+ # ---------------------------------------------------------------------------
782
+
783
+
784
+ @dataclass
785
+ class Krea2PipelineOutput(BaseOutput):
786
+ """Output class for the Krea 2 pipeline.
787
+
788
+ Args:
789
+ images (`list[PIL.Image.Image]` or `np.ndarray`):
790
+ List of `num_batches * num_images_per_prompt` denoised PIL images or a
791
+ numpy array of shape `(batch_size, height, width, num_channels)`.
792
+ """
793
+
794
+ images: Union[List[PIL.Image.Image], np.ndarray]
795
+
796
+
797
+ def calculate_shift(
798
+ image_seq_len,
799
+ base_seq_len: int = 256,
800
+ max_seq_len: int = 6400,
801
+ base_shift: float = 0.5,
802
+ max_shift: float = 1.15,
803
+ ):
804
+ m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
805
+ b = base_shift - m * base_seq_len
806
+ mu = image_seq_len * m + b
807
+ return mu
808
+
809
+
810
+ class Krea2OstrisEditPipeline(DiffusionPipeline):
811
+ r"""
812
+ Krea 2 text-to-image / reference-image-edit pipeline with Ostris AI-Toolkit LoRA
813
+ loading. See the module docstring for usage.
814
+
815
+ Args:
816
+ scheduler ([`FlowMatchEulerDiscreteScheduler`]):
817
+ Euler flow-matching scheduler configured with the Krea 2 resolution-aware
818
+ exponential time shift.
819
+ vae ([`AutoencoderKLQwenImage`]):
820
+ The Qwen-Image VAE (f8, 16 latent channels).
821
+ text_encoder ([`~transformers.Qwen3VLModel`]):
822
+ Qwen3-VL, including its vision tower (used to embed reference images into
823
+ the prompt conditioning).
824
+ tokenizer ([`~transformers.AutoTokenizer`]):
825
+ The tokenizer paired with the text encoder.
826
+ transformer ([`Krea2Transformer2DModel`]):
827
+ The Krea 2 single-stream MMDiT.
828
+ text_encoder_select_layers (`tuple[int, ...]`, *optional*):
829
+ Indices into the text encoder's `hidden_states` tuple whose states are
830
+ stacked per token as the transformer's text conditioning.
831
+ is_distilled (`bool`, *optional*, defaults to `False`):
832
+ Whether the transformer is the few-step distilled (Turbo) checkpoint. When
833
+ `True`, a fixed timestep shift `mu=1.15` is used and the call defaults
834
+ change to `num_inference_steps=8, guidance_scale=0.0`.
835
+ patch_size (`int`, *optional*, defaults to 2):
836
+ Side length of the square patches the latents are packed into.
837
+ """
838
+
839
+ model_cpu_offload_seq = "text_encoder->transformer->vae"
840
+
841
+ # Default hub repo used to lazily build the Qwen3-VL processor that turns
842
+ # reference images into vision tokens (the Krea 2 repos ship only a tokenizer).
843
+ vl_processor_id = "Qwen/Qwen3-VL-4B-Instruct"
844
+
845
+ def __init__(
846
+ self,
847
+ scheduler: FlowMatchEulerDiscreteScheduler,
848
+ vae: AutoencoderKLQwenImage,
849
+ text_encoder: Qwen3VLModel,
850
+ tokenizer: AutoTokenizer,
851
+ transformer: Krea2Transformer2DModel,
852
+ text_encoder_select_layers: Optional[Union[Tuple[int, ...], List[int]]] = None,
853
+ is_distilled: bool = False,
854
+ patch_size: int = 2,
855
+ ):
856
+ super().__init__()
857
+
858
+ self.register_modules(
859
+ scheduler=scheduler,
860
+ vae=vae,
861
+ text_encoder=text_encoder,
862
+ tokenizer=tokenizer,
863
+ transformer=transformer,
864
+ )
865
+ if text_encoder_select_layers is None:
866
+ text_encoder_select_layers = (2, 5, 8, 11, 14, 17, 20, 23, 26, 29, 32, 35)
867
+ self.register_to_config(text_encoder_select_layers=tuple(text_encoder_select_layers))
868
+ self.text_encoder_select_layers = tuple(text_encoder_select_layers)
869
+ self.register_to_config(is_distilled=is_distilled)
870
+ self.vae_scale_factor = 2 ** len(self.vae.temperal_downsample) if getattr(self, "vae", None) else 8
871
+ self.register_to_config(patch_size=patch_size)
872
+ self.patch_size = patch_size
873
+ self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * self.patch_size)
874
+
875
+ # Fixed instruction template wrapped around every prompt. The system prefix is
876
+ # fed through the encoder as context but its hidden states are sliced off.
877
+ self.prompt_template_encode_prefix = (
878
+ "<|im_start|>system\nDescribe the image by detailing the color, shape, size, texture, quantity, text, "
879
+ "spatial relationships of the objects and background:<|im_end|>\n<|im_start|>user\n"
880
+ )
881
+ self.prompt_template_encode_suffix = "<|im_end|>\n<|im_start|>assistant\n"
882
+ self.prompt_template_encode_start_idx = 34
883
+
884
+ self._vl_processor = None
885
+
886
+ # ------------------------------------------------------------------
887
+ # Prompt encoding (Qwen3-VL; reference images embedded via vision tokens)
888
+ # ------------------------------------------------------------------
889
+ @property
890
+ def vl_processor(self):
891
+ """Qwen3-VL AutoProcessor, loaded lazily (only needed when reference images are
892
+ encoded into the prompt)."""
893
+ if self._vl_processor is None:
894
+ from transformers import AutoProcessor
895
+
896
+ self._vl_processor = AutoProcessor.from_pretrained(self.vl_processor_id)
897
+ return self._vl_processor
898
+
899
+ @staticmethod
900
+ def _to_chw_tensor(image) -> torch.Tensor:
901
+ """Convert a PIL image / numpy array / CHW tensor to a float CHW tensor in [0, 1]."""
902
+ if isinstance(image, torch.Tensor):
903
+ t = image.squeeze(0) if image.ndim == 4 else image
904
+ t = t.float()
905
+ if t.min() < 0: # assume [-1, 1]
906
+ t = (t + 1.0) / 2.0
907
+ return t.clamp(0, 1)
908
+ if isinstance(image, np.ndarray):
909
+ image = PIL.Image.fromarray(image)
910
+ image = image.convert("RGB")
911
+ arr = np.asarray(image).astype(np.float32) / 255.0
912
+ return torch.from_numpy(arr).permute(2, 0, 1)
913
+
914
+ def _prep_vl_images(self, images: List[torch.Tensor], max_pixels: int) -> List[torch.Tensor]:
915
+ """Resize reference images for the Qwen3-VL pass: aspect-preserving downscale
916
+ (never upscaled) to fit ``max_pixels`` total area. The MLLM only needs a coarse
917
+ view of the references; high-res detail flows through the VAE ref latents."""
918
+ prepped = []
919
+ for img in images:
920
+ h, w = img.shape[1], img.shape[2]
921
+ scale = min(1.0, math.sqrt(max_pixels / (h * w)))
922
+ nh, nw = max(round(h * scale), 28), max(round(w * scale), 28)
923
+ if (nh, nw) != (h, w):
924
+ img = (
925
+ F.interpolate(img.unsqueeze(0).float(), size=(nh, nw), mode="bicubic", antialias=True)
926
+ .squeeze(0)
927
+ .clamp(0, 1)
928
+ )
929
+ prepped.append(img.float())
930
+ return prepped
931
+
932
+ def _encode_single_prompt(
933
+ self,
934
+ prompt: str,
935
+ images: Optional[List[torch.Tensor]] = None,
936
+ max_sequence_length: int = 512,
937
+ device: Optional[torch.device] = None,
938
+ ) -> torch.Tensor:
939
+ """Encode one prompt (optionally with reference images embedded as vision
940
+ tokens) into stacked Qwen3-VL hidden states of shape `(seq_len, num_text_layers,
941
+ text_hidden_dim)` at natural (unpadded) length."""
942
+ device = device or self._execution_device
943
+ prefix_idx = self.prompt_template_encode_start_idx
944
+
945
+ # The suffix is tokenized separately so it lands after the prompt tokens.
946
+ suffix_inputs = self.tokenizer([self.prompt_template_encode_suffix], return_tensors="pt").to(device)
947
+ suffix_ids = suffix_inputs["input_ids"]
948
+ suffix_mask = suffix_inputs["attention_mask"].bool()
949
+
950
+ extra_inputs = {}
951
+ if images:
952
+ # Reference images ride in the user message ahead of the prompt via named
953
+ # vision placeholders; the processor expands each <|image_pad|> to the
954
+ # image's token grid.
955
+ image_prompt = "".join(
956
+ f"Picture {i + 1}: <|vision_start|><|image_pad|><|vision_end|>" for i in range(len(images))
957
+ )
958
+ text = self.prompt_template_encode_prefix + image_prompt + prompt
959
+ # No truncation here: the expanded image-pad runs must stay intact.
960
+ inputs = self.vl_processor(text=[text], images=list(images), return_tensors="pt", do_rescale=False).to(
961
+ device
962
+ )
963
+ for k, v in inputs.items():
964
+ if k in ("input_ids", "attention_mask"):
965
+ continue
966
+ if isinstance(v, torch.Tensor) and v.is_floating_point():
967
+ v = v.to(self.text_encoder.dtype)
968
+ extra_inputs[k] = v
969
+ else:
970
+ text = self.prompt_template_encode_prefix + prompt
971
+ inputs = self.tokenizer(
972
+ [text], truncation=True, max_length=max_sequence_length + prefix_idx, return_tensors="pt"
973
+ ).to(device)
974
+
975
+ input_ids = torch.cat([inputs["input_ids"], suffix_ids], dim=1)
976
+ attention_mask = torch.cat([inputs["attention_mask"].bool(), suffix_mask], dim=1)
977
+
978
+ # mm_token_type_ids (used for M-RoPE) must cover the appended suffix tokens
979
+ # too; they are plain text -> type 0.
980
+ if "mm_token_type_ids" in extra_inputs:
981
+ tt = extra_inputs["mm_token_type_ids"]
982
+ extra_inputs["mm_token_type_ids"] = torch.cat(
983
+ [tt, torch.zeros_like(suffix_ids, dtype=tt.dtype)], dim=1
984
+ )
985
+
986
+ outputs = self.text_encoder(
987
+ input_ids=input_ids,
988
+ attention_mask=attention_mask,
989
+ output_hidden_states=True,
990
+ **extra_inputs,
991
+ )
992
+
993
+ hidden_states = torch.stack([outputs.hidden_states[i] for i in self.text_encoder_select_layers], dim=2)
994
+ # Drop the system-prefix tokens; what remains is (image +) prompt + suffix.
995
+ return hidden_states[0, prefix_idx:]
996
+
997
+ def encode_prompt(
998
+ self,
999
+ prompt: Union[str, List[str]],
1000
+ images: Optional[List[torch.Tensor]] = None,
1001
+ num_images_per_prompt: int = 1,
1002
+ max_sequence_length: int = 512,
1003
+ device: Optional[torch.device] = None,
1004
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
1005
+ """Encode prompts (all sharing the same reference images, if any) and right-pad
1006
+ them into a batch. Returns `(prompt_embeds, prompt_embeds_mask)` of shapes
1007
+ `(B, L, num_text_layers, D)` and `(B, L)` (bool)."""
1008
+ device = device or self._execution_device
1009
+ prompt = [prompt] if isinstance(prompt, str) else prompt
1010
+
1011
+ features = [self._encode_single_prompt(p, images, max_sequence_length, device) for p in prompt]
1012
+ max_len = max(f.shape[0] for f in features)
1013
+ embeds = features[0].new_zeros(len(features), max_len, *features[0].shape[1:])
1014
+ mask = torch.zeros(len(features), max_len, dtype=torch.bool, device=device)
1015
+ for i, f in enumerate(features):
1016
+ embeds[i, : f.shape[0]] = f
1017
+ mask[i, : f.shape[0]] = True
1018
+
1019
+ embeds = embeds.repeat_interleave(num_images_per_prompt, dim=0)
1020
+ mask = mask.repeat_interleave(num_images_per_prompt, dim=0)
1021
+ return embeds, mask
1022
+
1023
+ # ------------------------------------------------------------------
1024
+ # Latent packing helpers
1025
+ # ------------------------------------------------------------------
1026
+ def _pack_latents(self, latents: torch.Tensor) -> torch.Tensor:
1027
+ """(B, C, H, W) latents -> (B, H/p * W/p, C * p * p) tokens."""
1028
+ b, c, h, w = latents.shape
1029
+ p = self.patch_size
1030
+ latents = latents.view(b, c, h // p, p, w // p, p)
1031
+ latents = latents.permute(0, 2, 4, 1, 3, 5)
1032
+ return latents.reshape(b, (h // p) * (w // p), c * p * p)
1033
+
1034
+ def _unpack_latents(self, latents: torch.Tensor, height: int, width: int) -> torch.Tensor:
1035
+ """(B, L, C * p * p) tokens -> (B, C, 1, H, W) latents (frame dim for the VAE)."""
1036
+ batch_size, _, channels = latents.shape
1037
+ p = self.patch_size
1038
+ h = p * (int(height) // (self.vae_scale_factor * p))
1039
+ w = p * (int(width) // (self.vae_scale_factor * p))
1040
+ latents = latents.view(batch_size, h // p, w // p, channels // (p * p), p, p)
1041
+ latents = latents.permute(0, 3, 1, 4, 2, 5)
1042
+ return latents.reshape(batch_size, channels // (p * p), 1, h, w)
1043
+
1044
+ def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None):
1045
+ if latents is not None:
1046
+ return latents.to(device=device, dtype=dtype)
1047
+ shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor)
1048
+ if isinstance(generator, list) and len(generator) != batch_size:
1049
+ raise ValueError(
1050
+ f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
1051
+ f" size of {batch_size}. Make sure the batch size matches the length of the generators."
1052
+ )
1053
+ latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
1054
+ return self._pack_latents(latents)
1055
+
1056
+ def _encode_reference_latents(
1057
+ self,
1058
+ images: List[torch.Tensor],
1059
+ max_pixels: int,
1060
+ generator: Optional[torch.Generator],
1061
+ device: torch.device,
1062
+ ) -> List[torch.Tensor]:
1063
+ """Encode `[0, 1]` CHW reference images to normalized VAE latents, one `(C, h, w)`
1064
+ tensor per image. Each image is downscaled (aspect-preserving, never upscaled) to
1065
+ fit within `max_pixels`, then snapped so the latent grid is patchifiable."""
1066
+ snap = self.vae_scale_factor * self.patch_size
1067
+ vae_dtype = self.vae.dtype
1068
+
1069
+ latents_mean = torch.tensor(self.vae.config.latents_mean).view(1, self.vae.config.z_dim, 1, 1, 1)
1070
+ latents_std = torch.tensor(self.vae.config.latents_std).view(1, self.vae.config.z_dim, 1, 1, 1)
1071
+
1072
+ ref_latents = []
1073
+ for img in images:
1074
+ img = img.unsqueeze(0).to(device, dtype=vae_dtype)
1075
+ h, w = img.shape[2], img.shape[3]
1076
+ if h * w > max_pixels:
1077
+ ratio = h / w
1078
+ new_h, new_w = math.sqrt(max_pixels * ratio), math.sqrt(max_pixels / ratio)
1079
+ else:
1080
+ new_h, new_w = float(h), float(w)
1081
+ new_h = max(snap, int(round(new_h / snap)) * snap)
1082
+ new_w = max(snap, int(round(new_w / snap)) * snap)
1083
+ if (new_h, new_w) != (h, w):
1084
+ img = F.interpolate(img.float(), size=(new_h, new_w), mode="bilinear").to(vae_dtype)
1085
+
1086
+ img = (img * 2.0 - 1.0).unsqueeze(2) # [0,1] -> [-1,1], add frame dim
1087
+ latent = self.vae.encode(img).latent_dist.sample(generator)
1088
+ latent = (latent - latents_mean.to(latent.device, latent.dtype)) / latents_std.to(
1089
+ latent.device, latent.dtype
1090
+ )
1091
+ ref_latents.append(latent[:, :, 0][0]) # drop frame + batch dims -> (C, h, w)
1092
+ return ref_latents
1093
+
1094
+ def _pack_reference_latents(
1095
+ self,
1096
+ ref_latents: List[torch.Tensor],
1097
+ device: torch.device,
1098
+ dtype: torch.dtype,
1099
+ placements: Optional[List[Dict[str, Any]]] = None,
1100
+ target_grid_size: Optional[Tuple[int, int]] = None,
1101
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
1102
+ """Patchify reference latents into `(1, ref_seq_len, C * p * p)` tokens and build
1103
+ their `(ref_seq_len, 3)` rotary coordinates. The i-th reference sits on frame
1104
+ axis `i + 1` with its own y/x grid starting at 0 (Kontext "index" placement)."""
1105
+ p = self.patch_size
1106
+ tokens, position_ids = [], []
1107
+ for i, ref in enumerate(ref_latents):
1108
+ ref = ref.unsqueeze(0).to(device, dtype)
1109
+ tokens.append(self._pack_latents(ref))
1110
+ _, _, h, w = ref.shape
1111
+ ids = torch.zeros(h // p, w // p, 3, device=device)
1112
+ ids[..., 0] = i + 1
1113
+ placement = placements[i] if placements is not None else None
1114
+ if placement is None:
1115
+ ids[..., 1] = torch.arange(h // p, device=device)[:, None]
1116
+ ids[..., 2] = torch.arange(w // p, device=device)[None, :]
1117
+ else:
1118
+ if target_grid_size is None:
1119
+ raise ValueError("`target_grid_size` is required for registered references.")
1120
+ bbox = placement.get("bbox_normalized")
1121
+ if bbox is None or len(bbox) != 4:
1122
+ raise ValueError("A registered reference requires bbox_normalized=[x0,y0,x1,y1].")
1123
+ x0, y0, x1, y1 = (float(value) for value in bbox)
1124
+ if not (0 <= x0 < x1 <= 1 and 0 <= y0 < y1 <= 1):
1125
+ raise ValueError(f"Invalid normalized reference bbox: {bbox}")
1126
+ target_h, target_w = target_grid_size
1127
+ ref_h, ref_w = h // p, w // p
1128
+ ys = y0 * target_h + (torch.arange(ref_h, device=device) + 0.5) * (
1129
+ (y1 - y0) * target_h / ref_h
1130
+ ) - 0.5
1131
+ xs = x0 * target_w + (torch.arange(ref_w, device=device) + 0.5) * (
1132
+ (x1 - x0) * target_w / ref_w
1133
+ ) - 0.5
1134
+ ids[..., 1] = ys[:, None]
1135
+ ids[..., 2] = xs[None, :]
1136
+ position_ids.append(ids.reshape(-1, 3))
1137
+ return torch.cat(tokens, dim=1), torch.cat(position_ids, dim=0)
1138
+
1139
+ @staticmethod
1140
+ def prepare_position_ids(text_seq_len: int, grid_height: int, grid_width: int, device: torch.device):
1141
+ """Rotary coordinates for the `[text, image]` sequence: text tokens sit at the
1142
+ origin, image tokens carry their `(0, h, w)` latent-grid coordinates."""
1143
+ text_ids = torch.zeros(text_seq_len, 3, device=device)
1144
+ image_ids = torch.zeros(grid_height, grid_width, 3, device=device)
1145
+ image_ids[..., 1] = torch.arange(grid_height, device=device)[:, None]
1146
+ image_ids[..., 2] = torch.arange(grid_width, device=device)[None, :]
1147
+ image_ids = image_ids.reshape(grid_height * grid_width, 3)
1148
+ return torch.cat([text_ids, image_ids], dim=0)
1149
+
1150
+ # ------------------------------------------------------------------
1151
+ # LoRA loading (Ostris AI-Toolkit / ComfyUI / diffusers formats)
1152
+ # ------------------------------------------------------------------
1153
+ def load_lora_weights(
1154
+ self,
1155
+ pretrained_model_name_or_path_or_dict: Union[str, Dict[str, torch.Tensor]],
1156
+ weight_name: Optional[str] = None,
1157
+ adapter_name: str = "default",
1158
+ **kwargs,
1159
+ ):
1160
+ r"""
1161
+ Load a Krea 2 LoRA into the transformer.
1162
+
1163
+ Accepts a state dict, a local `.safetensors` file or directory, or a hub repo id
1164
+ (with `weight_name` selecting the file when the repo holds several). Handles
1165
+ Ostris AI-Toolkit / ComfyUI key layouts (`diffusion_model.blocks...` with
1166
+ `lora_A`/`lora_B` or `lora_down`/`lora_up`) as well as already-converted
1167
+ diffusers-format state dicts (`transformer.transformer_blocks...`).
1168
+ """
1169
+ if isinstance(pretrained_model_name_or_path_or_dict, dict):
1170
+ state_dict = dict(pretrained_model_name_or_path_or_dict)
1171
+ else:
1172
+ from safetensors.torch import load_file
1173
+
1174
+ path = str(pretrained_model_name_or_path_or_dict)
1175
+ if os.path.isfile(path):
1176
+ file_path = path
1177
+ elif os.path.isdir(path):
1178
+ if weight_name is None:
1179
+ candidates = [f for f in os.listdir(path) if f.endswith(".safetensors")]
1180
+ if len(candidates) != 1:
1181
+ raise ValueError(
1182
+ f"Could not pick a LoRA file in {path}: found {candidates}. Pass `weight_name`."
1183
+ )
1184
+ weight_name = candidates[0]
1185
+ file_path = os.path.join(path, weight_name)
1186
+ else:
1187
+ from huggingface_hub import hf_hub_download, list_repo_files
1188
+
1189
+ if weight_name is None:
1190
+ candidates = [
1191
+ f for f in list_repo_files(path, token=kwargs.get("token", None)) if f.endswith(".safetensors")
1192
+ ]
1193
+ if len(candidates) != 1:
1194
+ raise ValueError(
1195
+ f"Could not pick a LoRA file in hub repo {path}: found {candidates}. Pass `weight_name`."
1196
+ )
1197
+ weight_name = candidates[0]
1198
+ file_path = hf_hub_download(path, weight_name, token=kwargs.get("token", None))
1199
+ state_dict = load_file(file_path)
1200
+
1201
+ state_dict = _normalize_lora_state_dict(state_dict)
1202
+ if not any(k.startswith("transformer.") for k in state_dict):
1203
+ state_dict = _convert_non_diffusers_krea2_lora_to_diffusers(state_dict)
1204
+
1205
+ self.transformer.load_lora_adapter(state_dict, prefix="transformer", adapter_name=adapter_name)
1206
+
1207
+ def unload_lora_weights(self):
1208
+ """Remove all loaded LoRA adapters from the transformer."""
1209
+ transformer = self.transformer
1210
+ if hasattr(transformer, "unload_lora"):
1211
+ transformer.unload_lora()
1212
+ elif getattr(transformer, "peft_config", None):
1213
+ transformer.delete_adapters(list(transformer.peft_config.keys()))
1214
+
1215
+ def fuse_lora(self, lora_scale: float = 1.0, adapter_names: Optional[List[str]] = None, **kwargs):
1216
+ """Fuse the loaded LoRA weights into the transformer for adapter-free inference."""
1217
+ self.transformer.fuse_lora(lora_scale=lora_scale, adapter_names=adapter_names, **kwargs)
1218
+
1219
+ def unfuse_lora(self, **kwargs):
1220
+ self.transformer.unfuse_lora(**kwargs)
1221
+
1222
+ def set_adapters(self, adapter_names: Union[str, List[str]], weights: Optional[Union[float, List[float]]] = None):
1223
+ """Activate (and optionally weight) specific loaded LoRA adapters."""
1224
+ self.transformer.set_adapters(adapter_names, weights)
1225
+
1226
+ # ------------------------------------------------------------------
1227
+ # Generation
1228
+ # ------------------------------------------------------------------
1229
+ @property
1230
+ def guidance_scale(self):
1231
+ return self._guidance_scale
1232
+
1233
+ @property
1234
+ def do_classifier_free_guidance(self):
1235
+ return self._guidance_scale > 0
1236
+
1237
+ @torch.no_grad()
1238
+ def __call__(
1239
+ self,
1240
+ prompt: Union[str, List[str], None] = None,
1241
+ image: Union[PIL.Image.Image, np.ndarray, torch.Tensor, List, None] = None,
1242
+ negative_prompt: Union[str, List[str], None] = None,
1243
+ height: int = 1024,
1244
+ width: int = 1024,
1245
+ num_inference_steps: Optional[int] = None,
1246
+ sigmas: Optional[List[float]] = None,
1247
+ guidance_scale: Optional[float] = None,
1248
+ num_images_per_prompt: int = 1,
1249
+ generator: Union[torch.Generator, List[torch.Generator], None] = None,
1250
+ latents: Optional[torch.Tensor] = None,
1251
+ prompt_embeds: Optional[torch.Tensor] = None,
1252
+ prompt_embeds_mask: Optional[torch.Tensor] = None,
1253
+ negative_prompt_embeds: Optional[torch.Tensor] = None,
1254
+ negative_prompt_embeds_mask: Optional[torch.Tensor] = None,
1255
+ reference_max_pixels: int = 1024 * 1024,
1256
+ reference_placements: Optional[List[Dict[str, Any]]] = None,
1257
+ known_image: Union[PIL.Image.Image, np.ndarray, torch.Tensor, None] = None,
1258
+ known_mask: Union[PIL.Image.Image, np.ndarray, torch.Tensor, None] = None,
1259
+ vl_image_max_pixels: int = 384 * 384,
1260
+ encode_reference_in_prompt: bool = True,
1261
+ kv_cache: bool = False,
1262
+ output_type: Optional[str] = "pil",
1263
+ return_dict: bool = True,
1264
+ attention_kwargs: Optional[Dict[str, Any]] = None,
1265
+ max_sequence_length: int = 512,
1266
+ ):
1267
+ r"""
1268
+ Generate images from a prompt, optionally conditioned on reference images.
1269
+
1270
+ Args:
1271
+ prompt (`str` or `list[str]`):
1272
+ The prompt(s) to guide generation. For edits, describe the change (e.g.
1273
+ "make the sky purple").
1274
+ image (`PIL.Image.Image`, `np.ndarray`, `torch.Tensor` or a list of them, *optional*):
1275
+ Reference image(s). They are encoded into the prompt conditioning via
1276
+ the Qwen3-VL vision tower and appended to the transformer sequence as
1277
+ clean VAE latents at t=0. References keep their own aspect ratio; the
1278
+ output size is set by `height`/`width` independently.
1279
+ negative_prompt (`str` or `list[str]`, *optional*):
1280
+ Prompt(s) not to guide generation; ignored when `guidance_scale <= 0`.
1281
+ height / width (`int`, defaults to 1024):
1282
+ Output size in pixels; rounded up to a multiple of 16 if needed.
1283
+ num_inference_steps (`int`, *optional*):
1284
+ Denoising steps. Defaults to 8 for a distilled (Turbo) checkpoint and 28
1285
+ otherwise.
1286
+ sigmas (`list[float]`, *optional*):
1287
+ Custom sigma grid for the scheduler.
1288
+ guidance_scale (`float`, *optional*):
1289
+ Krea 2 CFG convention: velocity is `cond + scale * (cond - uncond)` and
1290
+ guidance is enabled whenever `scale > 0` (equals standard CFG with scale
1291
+ `1 + scale`). Defaults to 0.0 for a distilled checkpoint and 4.5
1292
+ otherwise.
1293
+ num_images_per_prompt (`int`, defaults to 1):
1294
+ Number of images per prompt.
1295
+ generator (`torch.Generator` or `list[torch.Generator]`, *optional*):
1296
+ RNG for deterministic generation.
1297
+ latents (`torch.Tensor`, *optional*):
1298
+ Pre-generated packed noisy latents `(B, image_seq_len, in_channels)`.
1299
+ prompt_embeds / prompt_embeds_mask (`torch.Tensor`, *optional*):
1300
+ Pre-computed text conditioning `(B, L, num_text_layers, D)` and its
1301
+ bool mask `(B, L)`; skips prompt encoding when given.
1302
+ negative_prompt_embeds / negative_prompt_embeds_mask (`torch.Tensor`, *optional*):
1303
+ Same, for the negative prompt.
1304
+ reference_max_pixels (`int`, defaults to `1024 * 1024`):
1305
+ Pixel budget each reference image is downscaled to fit before VAE
1306
+ encoding (never upscaled).
1307
+ known_image / known_mask (`PIL.Image`, `numpy.ndarray`, or `torch.Tensor`, *optional*):
1308
+ A full-canvas clean image and binary keep mask for inpaint/outpaint
1309
+ preservation. After every flow step, mask-selected tokens are replaced
1310
+ with the clean latent noised to the next scheduler sigma using the same
1311
+ initial noise. Supply both or neither.
1312
+ vl_image_max_pixels (`int`, defaults to `384 * 384`):
1313
+ Pixel budget for the (coarse) Qwen3-VL view of each reference image.
1314
+ encode_reference_in_prompt (`bool`, defaults to `True`):
1315
+ Whether reference images are also embedded into the text conditioning
1316
+ through the Qwen3-VL vision tower (matches AI-Toolkit edit training).
1317
+ kv_cache (`bool`, defaults to `False`):
1318
+ Cache the reference tokens' attention K/V: they are precomputed
1319
+ in a single t=0 pass and reused on every denoising step, so the
1320
+ reference tokens never ride along in the per-step sequence --
1321
+ faster, especially with CFG or many steps. The LoRA must be
1322
+ trained with AI-Toolkit's ``kv_cache`` model kwarg (reference
1323
+ tokens attend only to each other) for this to work properly;
1324
+ leave off for normally trained edit LoRAs.
1325
+ output_type (`str`, defaults to `"pil"`):
1326
+ `"pil"`, `"np"`, `"pt"` or `"latent"`.
1327
+ return_dict (`bool`, defaults to `True`):
1328
+ Whether to return a [`Krea2PipelineOutput`] instead of a plain tuple.
1329
+ attention_kwargs (`dict`, *optional*):
1330
+ Forwarded to the transformer; a `scale` entry sets the LoRA scale.
1331
+ max_sequence_length (`int`, defaults to 512):
1332
+ Maximum prompt token length (truncation only; no fixed padding).
1333
+
1334
+ Returns:
1335
+ [`Krea2PipelineOutput`] or `tuple`: the generated images.
1336
+ """
1337
+ if num_inference_steps is None:
1338
+ num_inference_steps = 8 if self.config.is_distilled else 28
1339
+ if guidance_scale is None:
1340
+ guidance_scale = 0.0 if self.config.is_distilled else 4.5
1341
+
1342
+ multiple = self.vae_scale_factor * self.patch_size
1343
+ if height % multiple != 0 or width % multiple != 0:
1344
+ rounded_height = ((height + multiple - 1) // multiple) * multiple
1345
+ rounded_width = ((width + multiple - 1) // multiple) * multiple
1346
+ logger.warning(
1347
+ f"`height` and `width` must be multiples of {multiple}; rounding up from {height}x{width} to"
1348
+ f" {rounded_height}x{rounded_width}."
1349
+ )
1350
+ height, width = rounded_height, rounded_width
1351
+
1352
+ if prompt is None and prompt_embeds is None:
1353
+ raise ValueError("Provide either `prompt` or `prompt_embeds`.")
1354
+ if prompt_embeds is not None and prompt_embeds_mask is None:
1355
+ raise ValueError("`prompt_embeds` requires `prompt_embeds_mask`.")
1356
+ if negative_prompt_embeds is not None and negative_prompt_embeds_mask is None:
1357
+ raise ValueError("`negative_prompt_embeds` requires `negative_prompt_embeds_mask`.")
1358
+ if (known_image is None) != (known_mask is None):
1359
+ raise ValueError("`known_image` and `known_mask` must be supplied together.")
1360
+
1361
+ self._guidance_scale = guidance_scale
1362
+
1363
+ if prompt is not None and isinstance(prompt, str):
1364
+ batch_size = 1
1365
+ elif prompt is not None:
1366
+ batch_size = len(prompt)
1367
+ else:
1368
+ batch_size = prompt_embeds.shape[0]
1369
+
1370
+ device = self._execution_device
1371
+ transformer_dtype = self.transformer.dtype
1372
+
1373
+ # 1. Normalize reference images to a list of [0, 1] CHW tensors.
1374
+ ref_images = None
1375
+ if image is not None:
1376
+ image_list = image if isinstance(image, (list, tuple)) else [image]
1377
+ ref_images = [self._to_chw_tensor(img) for img in image_list]
1378
+
1379
+ # 2. Encode the prompt(s). With references, the coarse VL view of each image is
1380
+ # embedded in the user message so the text conditioning "sees" them.
1381
+ vl_images = None
1382
+ if ref_images is not None and encode_reference_in_prompt:
1383
+ vl_images = self._prep_vl_images([img.to(device) for img in ref_images], vl_image_max_pixels)
1384
+
1385
+ if prompt_embeds is None:
1386
+ prompt_embeds, prompt_embeds_mask = self.encode_prompt(
1387
+ prompt, vl_images, num_images_per_prompt, max_sequence_length, device
1388
+ )
1389
+ prompt_embeds = prompt_embeds.to(transformer_dtype)
1390
+
1391
+ if self.do_classifier_free_guidance:
1392
+ if negative_prompt_embeds is None:
1393
+ negative_prompt = negative_prompt if negative_prompt is not None else ""
1394
+ if isinstance(negative_prompt, str):
1395
+ negative_prompt = [negative_prompt] * batch_size
1396
+ negative_prompt_embeds, negative_prompt_embeds_mask = self.encode_prompt(
1397
+ negative_prompt, vl_images, num_images_per_prompt, max_sequence_length, device
1398
+ )
1399
+ negative_prompt_embeds = negative_prompt_embeds.to(transformer_dtype)
1400
+
1401
+ # 3. Prepare the noisy latents (kept in float32 across scheduler steps).
1402
+ num_channels_latents = self.transformer.config.in_channels // (self.patch_size**2)
1403
+ latents = self.prepare_latents(
1404
+ batch_size * num_images_per_prompt,
1405
+ num_channels_latents,
1406
+ height,
1407
+ width,
1408
+ torch.float32,
1409
+ device,
1410
+ generator,
1411
+ latents,
1412
+ )
1413
+ grid_height = height // (self.vae_scale_factor * self.patch_size)
1414
+ grid_width = width // (self.vae_scale_factor * self.patch_size)
1415
+ initial_noise = latents.clone() if known_image is not None else None
1416
+
1417
+ known_latents = None
1418
+ known_token_mask = None
1419
+ if known_image is not None:
1420
+ known_tensor = self._to_chw_tensor(known_image)
1421
+ if tuple(known_tensor.shape[-2:]) != (height, width):
1422
+ raise ValueError("`known_image` must match the requested output dimensions.")
1423
+ known_latent = self._encode_reference_latents(
1424
+ [known_tensor], height * width, generator, device
1425
+ )[0].unsqueeze(0)
1426
+ known_latents = self._pack_latents(known_latent).float()
1427
+ known_latents = known_latents.expand(latents.shape[0], -1, -1)
1428
+
1429
+ if isinstance(known_mask, torch.Tensor):
1430
+ mask_tensor = known_mask.detach().float()
1431
+ while mask_tensor.ndim > 2:
1432
+ mask_tensor = mask_tensor[0]
1433
+ if mask_tensor.max() > 1:
1434
+ mask_tensor = mask_tensor / 255.0
1435
+ else:
1436
+ if isinstance(known_mask, np.ndarray):
1437
+ mask_array = known_mask
1438
+ else:
1439
+ mask_array = np.asarray(known_mask.convert("L"))
1440
+ if mask_array.ndim == 3:
1441
+ mask_array = mask_array[..., 0]
1442
+ mask_tensor = torch.from_numpy(np.asarray(mask_array).copy()).float() / 255.0
1443
+ if tuple(mask_tensor.shape) != (height, width):
1444
+ raise ValueError("`known_mask` must match the requested output dimensions.")
1445
+ latent_h = height // self.vae_scale_factor
1446
+ latent_w = width // self.vae_scale_factor
1447
+ mask_tensor = F.interpolate(
1448
+ mask_tensor[None, None].to(device),
1449
+ size=(latent_h, latent_w),
1450
+ mode="nearest",
1451
+ )
1452
+ p = self.patch_size
1453
+ mask_tensor = mask_tensor.view(1, 1, latent_h // p, p, latent_w // p, p)
1454
+ known_token_mask = (mask_tensor.amin(dim=(1, 3, 5)) > 0.5).reshape(1, -1, 1)
1455
+ known_token_mask = known_token_mask.expand(latents.shape[0], -1, -1)
1456
+
1457
+ # 4. Encode + pack reference latents (shared across the batch) and build the
1458
+ # combined rotary coordinates.
1459
+ ref_tokens, ref_seq_len = None, 0
1460
+ neg_position_ids = None
1461
+ position_ids = self.prepare_position_ids(prompt_embeds.shape[1], grid_height, grid_width, device)
1462
+ if self.do_classifier_free_guidance:
1463
+ neg_position_ids = self.prepare_position_ids(
1464
+ negative_prompt_embeds.shape[1], grid_height, grid_width, device
1465
+ )
1466
+ ref_kv = None
1467
+ if ref_images is not None:
1468
+ if reference_placements is not None and len(reference_placements) != len(ref_images):
1469
+ raise ValueError("`reference_placements` must match the number of reference images.")
1470
+ ref_latents = self._encode_reference_latents(ref_images, reference_max_pixels, generator, device)
1471
+ ref_tokens, ref_position_ids = self._pack_reference_latents(
1472
+ ref_latents,
1473
+ device,
1474
+ transformer_dtype,
1475
+ placements=reference_placements,
1476
+ target_grid_size=(grid_height, grid_width),
1477
+ )
1478
+ ref_seq_len = ref_tokens.shape[1]
1479
+ ref_tokens = ref_tokens.expand(latents.shape[0], -1, -1)
1480
+ if kv_cache:
1481
+ # Precompute pass: the refs alone run through the blocks once at t=0
1482
+ # and every denoising step reuses their per-block K/V, so the ref
1483
+ # tokens are dropped from the per-step sequence entirely.
1484
+ ref_kv = self.transformer.precompute_ref_kv(ref_tokens, ref_position_ids, attention_kwargs)
1485
+ ref_tokens, ref_seq_len = None, 0
1486
+ else:
1487
+ position_ids = torch.cat([position_ids, ref_position_ids], dim=0)
1488
+ if neg_position_ids is not None:
1489
+ neg_position_ids = torch.cat([neg_position_ids, ref_position_ids], dim=0)
1490
+
1491
+ # 5. Prepare timesteps. The distilled (Turbo) checkpoint was trained at a fixed
1492
+ # exponential time shift mu=1.15; the base checkpoint interpolates mu from the
1493
+ # image token count.
1494
+ sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas
1495
+ if self.config.is_distilled:
1496
+ mu = 1.15
1497
+ else:
1498
+ mu = calculate_shift(
1499
+ grid_height * grid_width,
1500
+ self.scheduler.config.get("base_image_seq_len", 256),
1501
+ self.scheduler.config.get("max_image_seq_len", 6400),
1502
+ self.scheduler.config.get("base_shift", 0.5),
1503
+ self.scheduler.config.get("max_shift", 1.15),
1504
+ )
1505
+ self.scheduler.set_timesteps(sigmas=sigmas, device=device, mu=mu)
1506
+ timesteps = self.scheduler.timesteps
1507
+ self.scheduler.set_begin_index(0)
1508
+
1509
+ # 6. Denoising loop (Euler flow ODE integration via the scheduler).
1510
+ with self.progress_bar(total=num_inference_steps) as progress_bar:
1511
+ for step_index, t in enumerate(timesteps):
1512
+ timestep = (t / self.scheduler.config.num_train_timesteps).expand(latents.shape[0]).to(
1513
+ transformer_dtype
1514
+ )
1515
+
1516
+ model_input = latents.to(transformer_dtype)
1517
+ if ref_tokens is not None:
1518
+ model_input = torch.cat([model_input, ref_tokens], dim=1)
1519
+
1520
+ noise_pred = self.transformer(
1521
+ hidden_states=model_input,
1522
+ encoder_hidden_states=prompt_embeds,
1523
+ timestep=timestep,
1524
+ position_ids=position_ids,
1525
+ encoder_attention_mask=prompt_embeds_mask,
1526
+ ref_seq_len=ref_seq_len,
1527
+ ref_kv_cache=ref_kv,
1528
+ attention_kwargs=attention_kwargs,
1529
+ return_dict=False,
1530
+ )[0]
1531
+
1532
+ if self.do_classifier_free_guidance:
1533
+ neg_noise_pred = self.transformer(
1534
+ hidden_states=model_input,
1535
+ encoder_hidden_states=negative_prompt_embeds,
1536
+ timestep=timestep,
1537
+ position_ids=neg_position_ids,
1538
+ encoder_attention_mask=negative_prompt_embeds_mask,
1539
+ ref_seq_len=ref_seq_len,
1540
+ ref_kv_cache=ref_kv,
1541
+ attention_kwargs=attention_kwargs,
1542
+ return_dict=False,
1543
+ )[0]
1544
+ noise_pred = noise_pred + guidance_scale * (noise_pred - neg_noise_pred)
1545
+
1546
+ latents = self.scheduler.step(noise_pred.float(), t, latents, return_dict=False)[0]
1547
+ if known_latents is not None:
1548
+ next_sigma = self.scheduler.sigmas[step_index + 1].to(
1549
+ device=latents.device, dtype=latents.dtype
1550
+ )
1551
+ noised_known = known_latents + next_sigma * (
1552
+ initial_noise - known_latents
1553
+ )
1554
+ latents = torch.where(known_token_mask, noised_known, latents)
1555
+ progress_bar.update()
1556
+
1557
+ # 7. Decode latents.
1558
+ if output_type == "latent":
1559
+ image_out = latents
1560
+ else:
1561
+ latents = self._unpack_latents(latents, height, width).to(self.vae.dtype)
1562
+ latents_mean = (
1563
+ torch.tensor(self.vae.config.latents_mean)
1564
+ .view(1, self.vae.config.z_dim, 1, 1, 1)
1565
+ .to(latents.device, latents.dtype)
1566
+ )
1567
+ latents_std = (
1568
+ torch.tensor(self.vae.config.latents_std)
1569
+ .view(1, self.vae.config.z_dim, 1, 1, 1)
1570
+ .to(latents.device, latents.dtype)
1571
+ )
1572
+ latents = latents * latents_std + latents_mean
1573
+ image_out = self.vae.decode(latents, return_dict=False)[0][:, :, 0]
1574
+ image_out = self.image_processor.postprocess(image_out, output_type=output_type)
1575
+
1576
+ self.maybe_free_model_hooks()
1577
+
1578
+ if not return_dict:
1579
+ return (image_out,)
1580
+ return Krea2PipelineOutput(images=image_out)
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ accelerate
2
+ diffusers==0.39.0
3
+ huggingface_hub
4
+ numpy
5
+ opencv-python-headless
6
+ peft
7
+ pillow
8
+ safetensors
9
+ sentencepiece
10
+ torch
11
+ transformers>=4.57
showcase/01_photo_internal_mask.png ADDED
showcase/01_photo_internal_result.webp ADDED
showcase/01_photo_internal_source.webp ADDED
showcase/02_watercolor_outpaint_mask.png ADDED
showcase/02_watercolor_outpaint_result.webp ADDED

Git LFS Details

  • SHA256: da9ce77585777ccfb8092643b1e9fe8e8e14173bdb8669e06c2b82afd43262fb
  • Pointer size: 131 Bytes
  • Size of remote file: 313 kB
showcase/02_watercolor_outpaint_source.webp ADDED

Git LFS Details

  • SHA256: 4efe48474687d31d47f8b89b4c73e47a9369d37d47695741c225cc30dd55f437
  • Pointer size: 131 Bytes
  • Size of remote file: 215 kB
showcase/03_3d_outpaint_mask.png ADDED
showcase/03_3d_outpaint_result.webp ADDED

Git LFS Details

  • SHA256: e586b4c9105b1ab9f4d83c1ed200c067d6735e6084925ebc8ee3ce051ae3ab9a
  • Pointer size: 131 Bytes
  • Size of remote file: 170 kB
showcase/03_3d_outpaint_source.webp ADDED

Git LFS Details

  • SHA256: 447be13e6afb4f534fb3520e8649ee380eba5da12cdb3e9538fdecc2bd5b8578
  • Pointer size: 131 Bytes
  • Size of remote file: 119 kB
showcase/04_portrait_mixed_mask.png ADDED
showcase/04_portrait_mixed_result.webp ADDED

Git LFS Details

  • SHA256: 9d2d3c20ac925d9f6fb07e3793a7e988bef1340d9461e0a9e96b76927ad2f571
  • Pointer size: 131 Bytes
  • Size of remote file: 106 kB
showcase/04_portrait_mixed_source.webp ADDED
showcase/05_botanical_disconnected_mask.png ADDED
showcase/05_botanical_disconnected_result.webp ADDED

Git LFS Details

  • SHA256: 56c262a0c3a7b05dc4d1995f2acfd70853ddeaa87fff120b806754e3c72bcb30
  • Pointer size: 131 Bytes
  • Size of remote file: 124 kB
showcase/05_botanical_disconnected_source.webp ADDED

Git LFS Details

  • SHA256: e5b791b266899fb92e46ffc2c81d43f8c25294de63b1c876a9d200520f442f21
  • Pointer size: 131 Bytes
  • Size of remote file: 148 kB
showcase/06_interior_boundary_mask.png ADDED
showcase/06_interior_boundary_result.webp ADDED

Git LFS Details

  • SHA256: 2235d5edd1e8a39540e46a0f85497175ea26f83fc9ac6f06b352243449b74ac5
  • Pointer size: 131 Bytes
  • Size of remote file: 145 kB
showcase/06_interior_boundary_source.webp ADDED

Git LFS Details

  • SHA256: a76702fbe0d6aafa79b04fffdb372d4bc9fa5990a711093f0df865ad7d1d6223
  • Pointer size: 131 Bytes
  • Size of remote file: 115 kB
showcase/07_product_narrow_mask.png ADDED
showcase/07_product_narrow_result.webp ADDED
showcase/07_product_narrow_source.webp ADDED
showcase/08_landscape_complex_mask.png ADDED
showcase/08_landscape_complex_result.webp ADDED

Git LFS Details

  • SHA256: f782f98d34c81e00d56a9ff5260cc2ef1fdffd0b826a4a0278f65ac82057ac55
  • Pointer size: 131 Bytes
  • Size of remote file: 260 kB
showcase/08_landscape_complex_source.webp ADDED

Git LFS Details

  • SHA256: 59eb9767eff575a1914a9405e0b58996191b76e7906ec2d862996bf6edb71f14
  • Pointer size: 131 Bytes
  • Size of remote file: 288 kB
showcase/README.md ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AnyPaint Showcase
2
+
3
+ All sources are synthetic and use original, non-IP prompts. Every result is the first
4
+ 8-step output for its recorded prompt and fixed seed; no rerolls were removed.
5
+
6
+ | Case | Source | Generated mask | Result |
7
+ |---|:---:|:---:|:---:|
8
+ | Photographic internal edit | ![](01_photo_internal_source.webp) | ![](01_photo_internal_mask.png) | ![](01_photo_internal_result.webp) |
9
+ | Watercolor downward outpainting | ![](02_watercolor_outpaint_source.webp) | ![](02_watercolor_outpaint_mask.png) | ![](02_watercolor_outpaint_result.webp) |
10
+ | Stylized 3D horizontal outpainting | ![](03_3d_outpaint_source.webp) | ![](03_3d_outpaint_mask.png) | ![](03_3d_outpaint_result.webp) |
11
+ | Original-character mixed edit | ![](04_portrait_mixed_source.webp) | ![](04_portrait_mixed_mask.png) | ![](04_portrait_mixed_result.webp) |
12
+ | Disconnected botanical masks | ![](05_botanical_disconnected_source.webp) | ![](05_botanical_disconnected_mask.png) | ![](05_botanical_disconnected_result.webp) |
13
+ | Boundary-crossing interior edit | ![](06_interior_boundary_source.webp) | ![](06_interior_boundary_mask.png) | ![](06_interior_boundary_result.webp) |
14
+ | Narrow curved product edit | ![](07_product_narrow_source.webp) | ![](07_product_narrow_mask.png) | ![](07_product_narrow_result.webp) |
15
+ | Complex landscape mask | ![](08_landscape_complex_source.webp) | ![](08_landscape_complex_mask.png) | ![](08_landscape_complex_result.webp) |
16
+
17
+ See `manifest.json` for prompts, seeds, placement, and timings.
showcase/contact_sheet.webp ADDED

Git LFS Details

  • SHA256: 0b65fceaa9f08e0d6ac01ca60462d8c29ceaf4dc1f65550448595c72e07ae041
  • Pointer size: 131 Bytes
  • Size of remote file: 243 kB
showcase/manifest.json ADDED
@@ -0,0 +1,818 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "id": "01_photo_internal",
4
+ "label": "Photographic internal edit",
5
+ "source_size": [
6
+ 1024,
7
+ 1024
8
+ ],
9
+ "canvas_size": [
10
+ 1024,
11
+ 1024
12
+ ],
13
+ "bbox": [
14
+ 0,
15
+ 0,
16
+ 1024,
17
+ 1024
18
+ ],
19
+ "source_seed": 810301,
20
+ "edit_seed": 820301,
21
+ "source_prompt": "an original sunlit modern kitchen interior, pale stone island, oak cabinets, soft morning light, editorial architectural photography, no text, no logos",
22
+ "prompt": "a sunlit modern kitchen interior with a handmade ceramic bowl of fresh lemons on the center island, coherent morning light, complete architectural photograph",
23
+ "mask": "01_photo_internal_mask.png",
24
+ "source": "01_photo_internal_source.webp",
25
+ "result": "01_photo_internal_result.webp",
26
+ "source_timings": {
27
+ "artist_lora_switch_seconds": 0.0,
28
+ "artist_loras": [],
29
+ "conditioning_cache_hit": false,
30
+ "generation_seconds": 3.58,
31
+ "mode": "generate",
32
+ "mode_switch_seconds": 0.002,
33
+ "peak_cuda_gib": 21.764,
34
+ "reference_count": 0,
35
+ "reference_kv_cache": false,
36
+ "reference_preprocess": {
37
+ "original_sizes": [],
38
+ "output_sizes": [],
39
+ "profile": null
40
+ },
41
+ "text_conditioning_seconds": 0.069,
42
+ "total_seconds": 3.651,
43
+ "vlm_reference_count": 0
44
+ },
45
+ "edit_timings": {
46
+ "artist_lora_switch_seconds": 0.0,
47
+ "artist_loras": [],
48
+ "conditioning_cache_hit": false,
49
+ "generation_seconds": 4.702,
50
+ "mode": "in_out",
51
+ "mode_switch_seconds": 0.002,
52
+ "output_contract": "raw_model_output",
53
+ "pass_count": 1,
54
+ "peak_cuda_gib": 22.028,
55
+ "plan": "single_pass_mixed_mask",
56
+ "preprocess": {
57
+ "bbox_normalized": [
58
+ 0.0,
59
+ 0.0,
60
+ 1.0,
61
+ 1.0
62
+ ],
63
+ "bbox_pixels": [
64
+ 0,
65
+ 0,
66
+ 1024,
67
+ 1024
68
+ ],
69
+ "canvas_size": [
70
+ 1024,
71
+ 1024
72
+ ],
73
+ "condition_fill": "median-known-rgb",
74
+ "condition_size": [
75
+ 384,
76
+ 384
77
+ ],
78
+ "generated_fraction": 0.073249,
79
+ "mask_convention": "0=known,255=generated",
80
+ "seam_px": 32
81
+ },
82
+ "reference_count": 1,
83
+ "reference_kv_cache": true,
84
+ "reference_preprocess": {
85
+ "original_sizes": [
86
+ [
87
+ 1024,
88
+ 1024
89
+ ]
90
+ ],
91
+ "output_sizes": [
92
+ [
93
+ 1024,
94
+ 1024
95
+ ]
96
+ ],
97
+ "profile": null
98
+ },
99
+ "text_conditioning_seconds": 0.1,
100
+ "total_seconds": 4.804,
101
+ "vlm_reference_count": 1
102
+ }
103
+ },
104
+ {
105
+ "id": "02_watercolor_outpaint",
106
+ "label": "Watercolor downward outpainting",
107
+ "source_size": [
108
+ 1024,
109
+ 1024
110
+ ],
111
+ "canvas_size": [
112
+ 1024,
113
+ 1536
114
+ ],
115
+ "bbox": [
116
+ 0,
117
+ 0,
118
+ 1024,
119
+ 1024
120
+ ],
121
+ "source_seed": 810302,
122
+ "edit_seed": 820302,
123
+ "source_prompt": "an original watercolor greenhouse beside a quiet coastal garden, airy washes, delicate ink details, soft daylight, no text",
124
+ "prompt": "an original watercolor greenhouse and coastal garden, the path and flowering plants continuing naturally downward, airy washes, complete vertical composition",
125
+ "mask": "02_watercolor_outpaint_mask.png",
126
+ "source": "02_watercolor_outpaint_source.webp",
127
+ "result": "02_watercolor_outpaint_result.webp",
128
+ "source_timings": {
129
+ "artist_lora_switch_seconds": 0.0,
130
+ "artist_loras": [],
131
+ "conditioning_cache_hit": false,
132
+ "generation_seconds": 3.546,
133
+ "mode": "generate",
134
+ "mode_switch_seconds": 0.002,
135
+ "peak_cuda_gib": 21.762,
136
+ "reference_count": 0,
137
+ "reference_kv_cache": false,
138
+ "reference_preprocess": {
139
+ "original_sizes": [],
140
+ "output_sizes": [],
141
+ "profile": null
142
+ },
143
+ "text_conditioning_seconds": 0.06,
144
+ "total_seconds": 3.608,
145
+ "vlm_reference_count": 0
146
+ },
147
+ "edit_timings": {
148
+ "artist_lora_switch_seconds": 0.0,
149
+ "artist_loras": [],
150
+ "conditioning_cache_hit": false,
151
+ "generation_seconds": 7.254,
152
+ "mode": "in_out",
153
+ "mode_switch_seconds": 0.002,
154
+ "output_contract": "raw_model_output",
155
+ "pass_count": 1,
156
+ "peak_cuda_gib": 22.395,
157
+ "plan": "single_pass_mixed_mask",
158
+ "preprocess": {
159
+ "bbox_normalized": [
160
+ 0.0,
161
+ 0.0,
162
+ 1.0,
163
+ 1.0
164
+ ],
165
+ "bbox_pixels": [
166
+ 0,
167
+ 0,
168
+ 1024,
169
+ 1024
170
+ ],
171
+ "canvas_size": [
172
+ 1024,
173
+ 1536
174
+ ],
175
+ "condition_fill": "median-known-rgb",
176
+ "condition_size": [
177
+ 256,
178
+ 384
179
+ ],
180
+ "generated_fraction": 0.333333,
181
+ "mask_convention": "0=known,255=generated",
182
+ "seam_px": 32
183
+ },
184
+ "reference_count": 1,
185
+ "reference_kv_cache": true,
186
+ "reference_preprocess": {
187
+ "original_sizes": [
188
+ [
189
+ 1024,
190
+ 1024
191
+ ]
192
+ ],
193
+ "output_sizes": [
194
+ [
195
+ 1024,
196
+ 1024
197
+ ]
198
+ ],
199
+ "profile": null
200
+ },
201
+ "text_conditioning_seconds": 0.093,
202
+ "total_seconds": 7.349,
203
+ "vlm_reference_count": 1
204
+ }
205
+ },
206
+ {
207
+ "id": "03_3d_outpaint",
208
+ "label": "Stylized 3D horizontal outpainting",
209
+ "source_size": [
210
+ 1024,
211
+ 1024
212
+ ],
213
+ "canvas_size": [
214
+ 1536,
215
+ 1024
216
+ ],
217
+ "bbox": [
218
+ 0,
219
+ 0,
220
+ 1024,
221
+ 1024
222
+ ],
223
+ "source_seed": 810303,
224
+ "edit_seed": 820303,
225
+ "source_prompt": "an original small retro-futuristic exploration rover in a colorful alien desert, stylized 3D render, detailed materials, cinematic daylight, no text, no logos",
226
+ "prompt": "an original retro-futuristic exploration rover crossing a colorful alien desert, distant mineral formations extending to the right, stylized 3D cinematic render",
227
+ "mask": "03_3d_outpaint_mask.png",
228
+ "source": "03_3d_outpaint_source.webp",
229
+ "result": "03_3d_outpaint_result.webp",
230
+ "source_timings": {
231
+ "artist_lora_switch_seconds": 0.0,
232
+ "artist_loras": [],
233
+ "conditioning_cache_hit": false,
234
+ "generation_seconds": 3.56,
235
+ "mode": "generate",
236
+ "mode_switch_seconds": 0.002,
237
+ "peak_cuda_gib": 21.762,
238
+ "reference_count": 0,
239
+ "reference_kv_cache": false,
240
+ "reference_preprocess": {
241
+ "original_sizes": [],
242
+ "output_sizes": [],
243
+ "profile": null
244
+ },
245
+ "text_conditioning_seconds": 0.059,
246
+ "total_seconds": 3.62,
247
+ "vlm_reference_count": 0
248
+ },
249
+ "edit_timings": {
250
+ "artist_lora_switch_seconds": 0.0,
251
+ "artist_loras": [],
252
+ "conditioning_cache_hit": false,
253
+ "generation_seconds": 7.286,
254
+ "mode": "in_out",
255
+ "mode_switch_seconds": 0.002,
256
+ "output_contract": "raw_model_output",
257
+ "pass_count": 1,
258
+ "peak_cuda_gib": 22.397,
259
+ "plan": "single_pass_mixed_mask",
260
+ "preprocess": {
261
+ "bbox_normalized": [
262
+ 0.0,
263
+ 0.0,
264
+ 1.0,
265
+ 1.0
266
+ ],
267
+ "bbox_pixels": [
268
+ 0,
269
+ 0,
270
+ 1024,
271
+ 1024
272
+ ],
273
+ "canvas_size": [
274
+ 1536,
275
+ 1024
276
+ ],
277
+ "condition_fill": "median-known-rgb",
278
+ "condition_size": [
279
+ 384,
280
+ 256
281
+ ],
282
+ "generated_fraction": 0.333333,
283
+ "mask_convention": "0=known,255=generated",
284
+ "seam_px": 32
285
+ },
286
+ "reference_count": 1,
287
+ "reference_kv_cache": true,
288
+ "reference_preprocess": {
289
+ "original_sizes": [
290
+ [
291
+ 1024,
292
+ 1024
293
+ ]
294
+ ],
295
+ "output_sizes": [
296
+ [
297
+ 1024,
298
+ 1024
299
+ ]
300
+ ],
301
+ "profile": null
302
+ },
303
+ "text_conditioning_seconds": 0.095,
304
+ "total_seconds": 7.383,
305
+ "vlm_reference_count": 1
306
+ }
307
+ },
308
+ {
309
+ "id": "04_portrait_mixed",
310
+ "label": "Original-character mixed edit",
311
+ "source_size": [
312
+ 1024,
313
+ 1024
314
+ ],
315
+ "canvas_size": [
316
+ 1536,
317
+ 1024
318
+ ],
319
+ "bbox": [
320
+ 0,
321
+ 0,
322
+ 1024,
323
+ 1024
324
+ ],
325
+ "source_seed": 810304,
326
+ "edit_seed": 820304,
327
+ "source_prompt": "an original adult woman with short silver hair wearing a simple teal coat in a bright contemporary gallery, full body fashion photograph, fictional subject, no text",
328
+ "prompt": "an original adult woman with short silver hair wearing a richly textured coral coat in a spacious contemporary gallery, complete full-body fashion photograph",
329
+ "mask": "04_portrait_mixed_mask.png",
330
+ "source": "04_portrait_mixed_source.webp",
331
+ "result": "04_portrait_mixed_result.webp",
332
+ "source_timings": {
333
+ "artist_lora_switch_seconds": 0.0,
334
+ "artist_loras": [],
335
+ "conditioning_cache_hit": false,
336
+ "generation_seconds": 3.553,
337
+ "mode": "generate",
338
+ "mode_switch_seconds": 0.002,
339
+ "peak_cuda_gib": 21.761,
340
+ "reference_count": 0,
341
+ "reference_kv_cache": false,
342
+ "reference_preprocess": {
343
+ "original_sizes": [],
344
+ "output_sizes": [],
345
+ "profile": null
346
+ },
347
+ "text_conditioning_seconds": 0.059,
348
+ "total_seconds": 3.614,
349
+ "vlm_reference_count": 0
350
+ },
351
+ "edit_timings": {
352
+ "artist_lora_switch_seconds": 0.0,
353
+ "artist_loras": [],
354
+ "conditioning_cache_hit": false,
355
+ "generation_seconds": 7.279,
356
+ "mode": "in_out",
357
+ "mode_switch_seconds": 0.002,
358
+ "output_contract": "raw_model_output",
359
+ "pass_count": 1,
360
+ "peak_cuda_gib": 22.396,
361
+ "plan": "single_pass_mixed_mask",
362
+ "preprocess": {
363
+ "bbox_normalized": [
364
+ 0.0,
365
+ 0.0,
366
+ 1.0,
367
+ 1.0
368
+ ],
369
+ "bbox_pixels": [
370
+ 0,
371
+ 0,
372
+ 1024,
373
+ 1024
374
+ ],
375
+ "canvas_size": [
376
+ 1536,
377
+ 1024
378
+ ],
379
+ "condition_fill": "median-known-rgb",
380
+ "condition_size": [
381
+ 384,
382
+ 256
383
+ ],
384
+ "generated_fraction": 0.455404,
385
+ "mask_convention": "0=known,255=generated",
386
+ "seam_px": 32
387
+ },
388
+ "reference_count": 1,
389
+ "reference_kv_cache": true,
390
+ "reference_preprocess": {
391
+ "original_sizes": [
392
+ [
393
+ 1024,
394
+ 1024
395
+ ]
396
+ ],
397
+ "output_sizes": [
398
+ [
399
+ 1024,
400
+ 1024
401
+ ]
402
+ ],
403
+ "profile": null
404
+ },
405
+ "text_conditioning_seconds": 0.094,
406
+ "total_seconds": 7.375,
407
+ "vlm_reference_count": 1
408
+ }
409
+ },
410
+ {
411
+ "id": "05_botanical_disconnected",
412
+ "label": "Disconnected botanical masks",
413
+ "source_size": [
414
+ 1024,
415
+ 1024
416
+ ],
417
+ "canvas_size": [
418
+ 1024,
419
+ 1024
420
+ ],
421
+ "bbox": [
422
+ 0,
423
+ 0,
424
+ 1024,
425
+ 1024
426
+ ],
427
+ "source_seed": 810305,
428
+ "edit_seed": 820305,
429
+ "source_prompt": "an original botanical field illustration of unusual blue and white meadow flowers, fine paper texture, naturalistic painted details, no labels, no text",
430
+ "prompt": "an original botanical field illustration with several vivid red seed pods among blue and white meadow flowers, fine paper texture, naturalistic painted details",
431
+ "mask": "05_botanical_disconnected_mask.png",
432
+ "source": "05_botanical_disconnected_source.webp",
433
+ "result": "05_botanical_disconnected_result.webp",
434
+ "source_timings": {
435
+ "artist_lora_switch_seconds": 0.0,
436
+ "artist_loras": [],
437
+ "conditioning_cache_hit": false,
438
+ "generation_seconds": 3.55,
439
+ "mode": "generate",
440
+ "mode_switch_seconds": 0.002,
441
+ "peak_cuda_gib": 21.76,
442
+ "reference_count": 0,
443
+ "reference_kv_cache": false,
444
+ "reference_preprocess": {
445
+ "original_sizes": [],
446
+ "output_sizes": [],
447
+ "profile": null
448
+ },
449
+ "text_conditioning_seconds": 0.062,
450
+ "total_seconds": 3.614,
451
+ "vlm_reference_count": 0
452
+ },
453
+ "edit_timings": {
454
+ "artist_lora_switch_seconds": 0.0,
455
+ "artist_loras": [],
456
+ "conditioning_cache_hit": false,
457
+ "generation_seconds": 4.705,
458
+ "mode": "in_out",
459
+ "mode_switch_seconds": 0.002,
460
+ "output_contract": "raw_model_output",
461
+ "pass_count": 1,
462
+ "peak_cuda_gib": 22.028,
463
+ "plan": "single_pass_mixed_mask",
464
+ "preprocess": {
465
+ "bbox_normalized": [
466
+ 0.0,
467
+ 0.0,
468
+ 1.0,
469
+ 1.0
470
+ ],
471
+ "bbox_pixels": [
472
+ 0,
473
+ 0,
474
+ 1024,
475
+ 1024
476
+ ],
477
+ "canvas_size": [
478
+ 1024,
479
+ 1024
480
+ ],
481
+ "condition_fill": "median-known-rgb",
482
+ "condition_size": [
483
+ 384,
484
+ 384
485
+ ],
486
+ "generated_fraction": 0.08834,
487
+ "mask_convention": "0=known,255=generated",
488
+ "seam_px": 32
489
+ },
490
+ "reference_count": 1,
491
+ "reference_kv_cache": true,
492
+ "reference_preprocess": {
493
+ "original_sizes": [
494
+ [
495
+ 1024,
496
+ 1024
497
+ ]
498
+ ],
499
+ "output_sizes": [
500
+ [
501
+ 1024,
502
+ 1024
503
+ ]
504
+ ],
505
+ "profile": null
506
+ },
507
+ "text_conditioning_seconds": 0.093,
508
+ "total_seconds": 4.799,
509
+ "vlm_reference_count": 1
510
+ }
511
+ },
512
+ {
513
+ "id": "06_interior_boundary",
514
+ "label": "Boundary-crossing interior edit",
515
+ "source_size": [
516
+ 1024,
517
+ 1024
518
+ ],
519
+ "canvas_size": [
520
+ 1536,
521
+ 1024
522
+ ],
523
+ "bbox": [
524
+ 512,
525
+ 0,
526
+ 1536,
527
+ 1024
528
+ ],
529
+ "source_seed": 810306,
530
+ "edit_seed": 820306,
531
+ "source_prompt": "an original quiet reading room with tall windows, linen armchair, walnut shelves, warm afternoon light, refined interior photography, no text",
532
+ "prompt": "a quiet reading room expanding to the left into a coherent library alcove, tall windows, walnut shelves, warm afternoon light, complete interior photograph",
533
+ "mask": "06_interior_boundary_mask.png",
534
+ "source": "06_interior_boundary_source.webp",
535
+ "result": "06_interior_boundary_result.webp",
536
+ "source_timings": {
537
+ "artist_lora_switch_seconds": 0.0,
538
+ "artist_loras": [],
539
+ "conditioning_cache_hit": false,
540
+ "generation_seconds": 3.576,
541
+ "mode": "generate",
542
+ "mode_switch_seconds": 0.002,
543
+ "peak_cuda_gib": 21.763,
544
+ "reference_count": 0,
545
+ "reference_kv_cache": false,
546
+ "reference_preprocess": {
547
+ "original_sizes": [],
548
+ "output_sizes": [],
549
+ "profile": null
550
+ },
551
+ "text_conditioning_seconds": 0.061,
552
+ "total_seconds": 3.638,
553
+ "vlm_reference_count": 0
554
+ },
555
+ "edit_timings": {
556
+ "artist_lora_switch_seconds": 0.0,
557
+ "artist_loras": [],
558
+ "conditioning_cache_hit": false,
559
+ "generation_seconds": 7.293,
560
+ "mode": "in_out",
561
+ "mode_switch_seconds": 0.002,
562
+ "output_contract": "raw_model_output",
563
+ "pass_count": 1,
564
+ "peak_cuda_gib": 22.396,
565
+ "plan": "single_pass_mixed_mask",
566
+ "preprocess": {
567
+ "bbox_normalized": [
568
+ 0.0,
569
+ 0.0,
570
+ 1.0,
571
+ 1.0
572
+ ],
573
+ "bbox_pixels": [
574
+ 512,
575
+ 0,
576
+ 1536,
577
+ 1024
578
+ ],
579
+ "canvas_size": [
580
+ 1536,
581
+ 1024
582
+ ],
583
+ "condition_fill": "median-known-rgb",
584
+ "condition_size": [
585
+ 384,
586
+ 256
587
+ ],
588
+ "generated_fraction": 0.369276,
589
+ "mask_convention": "0=known,255=generated",
590
+ "seam_px": 32
591
+ },
592
+ "reference_count": 1,
593
+ "reference_kv_cache": true,
594
+ "reference_preprocess": {
595
+ "original_sizes": [
596
+ [
597
+ 1024,
598
+ 1024
599
+ ]
600
+ ],
601
+ "output_sizes": [
602
+ [
603
+ 1024,
604
+ 1024
605
+ ]
606
+ ],
607
+ "profile": null
608
+ },
609
+ "text_conditioning_seconds": 0.094,
610
+ "total_seconds": 7.388,
611
+ "vlm_reference_count": 1
612
+ }
613
+ },
614
+ {
615
+ "id": "07_product_narrow",
616
+ "label": "Narrow curved product edit",
617
+ "source_size": [
618
+ 1024,
619
+ 1024
620
+ ],
621
+ "canvas_size": [
622
+ 1024,
623
+ 1024
624
+ ],
625
+ "bbox": [
626
+ 0,
627
+ 0,
628
+ 1024,
629
+ 1024
630
+ ],
631
+ "source_seed": 810307,
632
+ "edit_seed": 820307,
633
+ "source_prompt": "an original sculptural desk lamp made from brushed aluminum on a neutral studio set, premium product photography, soft shadows, no text, no logo",
634
+ "prompt": "an original sculptural desk lamp with a thin glowing amber light strip following its curved stem, brushed aluminum, premium neutral product photography",
635
+ "mask": "07_product_narrow_mask.png",
636
+ "source": "07_product_narrow_source.webp",
637
+ "result": "07_product_narrow_result.webp",
638
+ "source_timings": {
639
+ "artist_lora_switch_seconds": 0.0,
640
+ "artist_loras": [],
641
+ "conditioning_cache_hit": false,
642
+ "generation_seconds": 3.566,
643
+ "mode": "generate",
644
+ "mode_switch_seconds": 0.002,
645
+ "peak_cuda_gib": 21.761,
646
+ "reference_count": 0,
647
+ "reference_kv_cache": false,
648
+ "reference_preprocess": {
649
+ "original_sizes": [],
650
+ "output_sizes": [],
651
+ "profile": null
652
+ },
653
+ "text_conditioning_seconds": 0.06,
654
+ "total_seconds": 3.627,
655
+ "vlm_reference_count": 0
656
+ },
657
+ "edit_timings": {
658
+ "artist_lora_switch_seconds": 0.0,
659
+ "artist_loras": [],
660
+ "conditioning_cache_hit": false,
661
+ "generation_seconds": 4.694,
662
+ "mode": "in_out",
663
+ "mode_switch_seconds": 0.002,
664
+ "output_contract": "raw_model_output",
665
+ "pass_count": 1,
666
+ "peak_cuda_gib": 22.028,
667
+ "plan": "single_pass_mixed_mask",
668
+ "preprocess": {
669
+ "bbox_normalized": [
670
+ 0.0,
671
+ 0.0,
672
+ 1.0,
673
+ 1.0
674
+ ],
675
+ "bbox_pixels": [
676
+ 0,
677
+ 0,
678
+ 1024,
679
+ 1024
680
+ ],
681
+ "canvas_size": [
682
+ 1024,
683
+ 1024
684
+ ],
685
+ "condition_fill": "median-known-rgb",
686
+ "condition_size": [
687
+ 384,
688
+ 384
689
+ ],
690
+ "generated_fraction": 0.026233,
691
+ "mask_convention": "0=known,255=generated",
692
+ "seam_px": 32
693
+ },
694
+ "reference_count": 1,
695
+ "reference_kv_cache": true,
696
+ "reference_preprocess": {
697
+ "original_sizes": [
698
+ [
699
+ 1024,
700
+ 1024
701
+ ]
702
+ ],
703
+ "output_sizes": [
704
+ [
705
+ 1024,
706
+ 1024
707
+ ]
708
+ ],
709
+ "profile": null
710
+ },
711
+ "text_conditioning_seconds": 0.092,
712
+ "total_seconds": 4.787,
713
+ "vlm_reference_count": 1
714
+ }
715
+ },
716
+ {
717
+ "id": "08_landscape_complex",
718
+ "label": "Complex landscape mask",
719
+ "source_size": [
720
+ 1536,
721
+ 1024
722
+ ],
723
+ "canvas_size": [
724
+ 1536,
725
+ 1024
726
+ ],
727
+ "bbox": [
728
+ 0,
729
+ 0,
730
+ 1536,
731
+ 1024
732
+ ],
733
+ "source_seed": 810308,
734
+ "edit_seed": 820308,
735
+ "source_prompt": "an original alpine valley after rain, winding river, dark rocks and low clouds, wide cinematic landscape photograph, no buildings, no text",
736
+ "prompt": "an original alpine valley after rain with a winding turquoise river and warm sunlight breaking through the low clouds, coherent wide cinematic landscape photograph",
737
+ "mask": "08_landscape_complex_mask.png",
738
+ "source": "08_landscape_complex_source.webp",
739
+ "result": "08_landscape_complex_result.webp",
740
+ "source_timings": {
741
+ "artist_lora_switch_seconds": 0.0,
742
+ "artist_loras": [],
743
+ "conditioning_cache_hit": false,
744
+ "generation_seconds": 5.631,
745
+ "mode": "generate",
746
+ "mode_switch_seconds": 0.002,
747
+ "peak_cuda_gib": 22.106,
748
+ "reference_count": 0,
749
+ "reference_kv_cache": false,
750
+ "reference_preprocess": {
751
+ "original_sizes": [],
752
+ "output_sizes": [],
753
+ "profile": null
754
+ },
755
+ "text_conditioning_seconds": 0.059,
756
+ "total_seconds": 5.691,
757
+ "vlm_reference_count": 0
758
+ },
759
+ "edit_timings": {
760
+ "artist_lora_switch_seconds": 0.0,
761
+ "artist_loras": [],
762
+ "conditioning_cache_hit": false,
763
+ "generation_seconds": 7.326,
764
+ "mode": "in_out",
765
+ "mode_switch_seconds": 0.002,
766
+ "output_contract": "raw_model_output",
767
+ "pass_count": 1,
768
+ "peak_cuda_gib": 22.395,
769
+ "plan": "single_pass_mixed_mask",
770
+ "preprocess": {
771
+ "bbox_normalized": [
772
+ 0.0,
773
+ 0.0,
774
+ 1.0,
775
+ 1.0
776
+ ],
777
+ "bbox_pixels": [
778
+ 0,
779
+ 0,
780
+ 1536,
781
+ 1024
782
+ ],
783
+ "canvas_size": [
784
+ 1536,
785
+ 1024
786
+ ],
787
+ "condition_fill": "median-known-rgb",
788
+ "condition_size": [
789
+ 384,
790
+ 256
791
+ ],
792
+ "generated_fraction": 0.272167,
793
+ "mask_convention": "0=known,255=generated",
794
+ "seam_px": 32
795
+ },
796
+ "reference_count": 1,
797
+ "reference_kv_cache": true,
798
+ "reference_preprocess": {
799
+ "original_sizes": [
800
+ [
801
+ 1536,
802
+ 1024
803
+ ]
804
+ ],
805
+ "output_sizes": [
806
+ [
807
+ 1536,
808
+ 1024
809
+ ]
810
+ ],
811
+ "profile": null
812
+ },
813
+ "text_conditioning_seconds": 0.096,
814
+ "total_seconds": 7.423,
815
+ "vlm_reference_count": 1
816
+ }
817
+ }
818
+ ]