├── LICENSE ├── README.md ├── __init__.py ├── advanced_workflow.json ├── nodes.py └── simple_workflow.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 kaibioinfo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI_AdvancedReduxControl 2 | 3 | As many of you might noticed, the recently released Redux model is rather a model for generating multiple variants of an image, but it does not allow for changing an image based on a prompt. 4 | 5 | If you use Redux with an Image and add a prompt, your prompt is just ignored. In general, there is no strength slider or anything in Redux to control how much the coniditioning image should determine the final outcome of your image. 6 | 7 | For this purpose I wrote this little custom node that allows you change the strength of the Redux effect. 8 | 9 | ## Changelog 10 | 11 | - **v2.0** This version adds the option to mask the conditioning image and to submit non-square images. The simple node hasn't changed and is backcompatible, the advanced mode is completely redesigned, so use v1 if you need backward compatibility. 12 | 13 | ## Examples 14 | 15 | I used the following pexel image as an example conditioning image: [[https://www.pexels.com/de-de/foto/29455324/]] 16 | 17 | ![original](https://github.com/user-attachments/assets/16c8bce5-8eb3-4acf-93e9-847a81e969e0) 18 | 19 | Lets say we want to have a similar image, but as comic/cartoon. The prompt I use is "comic, cartoon, vintage comic" 20 | 21 | Using Redux on Flux1-dev I obtain the following image. 22 | 23 | **original Redux setting** 24 | ![ComfyUI_00106_](https://github.com/user-attachments/assets/0c5506ef-5131-4b57-962c-ab3703881363) 25 | 26 | As you can see, the prompt is vastly ignored. Using the custom node and "medium" setting I obtain 27 | 28 | **Redux medium strength** 29 | ![image](https://github.com/user-attachments/assets/eb81a55a-6bdd-43ef-a8da-8d27f210c116) 30 | 31 | Lets do the same with anime. The prompt is "anime drawing in anime style. Studio Ghibli, Makoto Shinkai." 32 | 33 | As the anime keyword has a strong effect in Flux, we see a better prompt following on default than with comics. 34 | 35 | **original Redux setting** 36 | ![image](https://github.com/user-attachments/assets/e5795369-2b8e-477a-974f-e0250d8689b6) 37 | 38 | Still, its far from perfect. With "medium" setting we get an image that is much closer to anime or studio Ghibli. 39 | 40 | **Redux medium strength** 41 | ![image](https://github.com/user-attachments/assets/b632457a-3a7e-4d99-981e-6c2682d16e2e) 42 | 43 | 44 | You can also mix more than one images together. Here is an example with adding a second image: [[https://www.pexels.com/de-de/foto/komplizierte-bogen-der-mogul-architektur-in-jaipur-29406307/]] 45 | 46 | Mixing both together and using the anime prompt above gives me 47 | 48 | ![image](https://github.com/user-attachments/assets/1385b22f-4497-4fdf-8255-3a15bda74a1d) 49 | 50 | Finally, we try a very challenging prompt: "Marble statues, sculptures, stone statues. stone and marble texture. Two sculptures made out of marble stone.". As you can see, I repeated the prompt multiple times to increase its strength. 51 | But despite the repeats, the default Redux workflow will just give us the input image Reduxed - our prompt is totally ignored. 52 | 53 | **original Redux setting** 54 | ![ComfyUI_00108_](https://github.com/user-attachments/assets/24ad66e9-4f21-497d-8d0e-cb4778f0d1e9) 55 | 56 | With medium we get an image back that looks more like porcelain instead of marble, but at least the two women are sculptures now. 57 | 58 | **Redux medium strength** 59 | ![image](https://github.com/user-attachments/assets/dce4aa6f-52ab-4ef0-b027-193318895969) 60 | 61 | Further decreasing the Redux strength will transform the woman into statues finally, but it will also further decrease their likeness to the conditioning image. In almost all my experiments, it was better to repeat multiple seeds with the "medium" setting instead of further decreasing the strength. 62 | 63 | # Masked Conditioning Images 64 | 65 | With **v.2** you can now also add a mask to the conditioning image. 66 | 67 | ![image](https://github.com/user-attachments/assets/71644833-1169-47b9-a843-83fd739b17c8) 68 | 69 | In this example I just masked the flower pattern on the clothing of the right women. I then prompt for "Man walking in New York, smiling, holding a smart phone in his hand.". As you can see, his shirt adapts to the flower pattern, while nothing outside the mask has any impact on the outcoming image. 70 | 71 | ![image](https://github.com/user-attachments/assets/2163c140-4980-4738-88db-77c8286742e6) 72 | 73 | When the masking area is very small, you have to increase the strength of the conditioning image as "less of the image" is used to condition your generation. 74 | 75 | ## Non-Square Images 76 | 77 | Redux (or better CLIP) cannot deal with non-square images by default. It will just center-crop your conditioning image such that it has a square resolution. Now that the node supports masking, we can simply support non-square images, too. We just make the image square by adding black borders to the shorter edge. Of course, we do not want to have these borders in the generated image, so we add a mask that cover the original image but not the black padding border. 78 | 79 | You do not have to do this yourself. There is a "keep aspect ratio" option that automatically generates the padding and adjust the mask for you. 80 | 81 | Here is an example: the input image (again from Pexel: ) is this one: https://www.pexels.com/photo/man-wearing-blue-gray-and-black-crew-neck-shirt-1103832/ 82 | 83 | To make a point, I cropped the images to make it maximal non-square. 84 | 85 | ![image](https://github.com/user-attachments/assets/8add3187-77b3-49b2-bd9e-c82297925a8d) 86 | 87 | With the normal workflow and the prompt "comic, vintage comic, cartoon" we would get this image back: 88 | 89 | ![image](https://github.com/user-attachments/assets/169d33e0-27db-4e4c-b5cc-ec0b6729032a) 90 | 91 | With the "keep aspect ratio" option enabled, we get this instead: 92 | 93 | ![image](https://github.com/user-attachments/assets/10e3a8b8-9752-4060-b6b5-ed35f9764320) 94 | 95 | Similar to masks, the conditioning effect will be weaker when we use only a small mask (or here: when the aspect ratio is extremely unbalanced). Thus, I would still recommend to avoid images with too extreme aspect ratios as this example image above. 96 | 97 | ## Usage 98 | 99 | I was told that the images above for some reason do not contain the workflow. So I just uploaded the workflow files into the github. The **simple_workflow.json** is the workflow containing a single setting, the **advanced_workflow.json** has several customization options as well as masking and aspect ratio. 100 | 101 | ### StyleModelApplySimple 102 | 103 | This workflow is a replacement for the ComfyUI StyleModelApply node. It has a single option that controls the influence of the conditioning image on the generation. The example images are all generated with the "medium" strength option. However, when using masking, you might have to use "strongest" or "strong" instead. 104 | 105 | 106 | ### ReduxAdvanced 107 | 108 | This node allows for more customization. As input it gets the conditioning (prompt), the Redux style model, the CLIP vision model and optionally(!) the mask. Its parameters are: 109 | 110 | - **downsampling_factor**: This is the most important parameter and it determines how strongly the conditioning image influences the generated image. In fact, the strength value in the StyleModelApplySimple node is just changing this single value from 1 (strongest), to 5 (weakest). The "medium" strength option is a downsampling_factor of 3. You can also choose other values up to 9. 111 | - **downsampling_function**: These functions are the same as in any graphics program when you resize an image. The default is "area", but "bicubic" and "nearest_exact" are also interesting choices. The chosen function can very much change the outcome, so its worth experimenting a bit. 112 | - **mode**: How the image should be cropped: 113 | - - center crop (square) is what Redux is doing by default: cropping your image to a square image and then resizing it to 384x384 pixel 114 | - - keep aspect ratio will add a padding to your image such that it gets square. It will adjust the mask (or generate one if no one is given) such that the padding is not part of the mask. Note that the final image is still resized to 384x384, so your image will be compressed more if you add more padding. 115 | - - autocrop with mask will crop your image in a way that the masked area is in the center. It will also crop your image such that only the masked area and a margin remains. The margin is specified as the autocrop_margin parameter and is relative to the total size of the image. So autocropping with a margin of 0.1 means the crop is done on the masked area + 10% of the image on each side. 116 | - **weight**: This option downscales the Redux tokens by the given value squared. This is very similar to the "conditioning average" approach many people use to reduce the effect of Redux on the generated image. This is an alternative way of reducing Redux' impact, but downsampling works better in most cases. However, feel free to also experiment with this value, or even with a combination of both: downsampling AND weight. 117 | - **autocrop_margin** this parameter is only used when "autocrop with mask" is selected as mode 118 | 119 | The node outputs the conditioning, as well as the cropped and resized image and its mask. You neither need the image nor the mask, they are just for debugging. Play around with the cropping option and use the "Image preview" node to see how it effects the cropped image and mask. 120 | 121 | ## Short background on Redux 122 | 123 | Redux works in two steps. First there is a Clip Vision model that crops your input image into square aspect ratio and reduce its size to 384x384 pixels. It splits this image into 27x27 small patches and each patch is projected into CLIP space. 124 | 125 | Redux itself is just a very small linear function that projects these clip image patches into the T5 latent space. The resulting tokens are then added to your T5 prompt. 126 | 127 | Intuitively, Redux is translating your conditioning input image into "a prompt" that is added at the end of your own prompt. 128 | 129 | So why is Redux dominating the final prompt? It's because the user prompt is usually very short (255 or 512 tokens). Redux, in contrast, adds 729 new tokens to your prompt. This might be 3 times as much as your original prompt. Also, the Redux prompt might contain much more information than a user written prompt that just contains the word "anime". 130 | 131 | So there are two solutions here: Either we shrink the strength of the Redux prompt, or we shorten the Redux prompt. 132 | 133 | The next sections are a bit chaotic: I changed the method several times and many stuff I tried is outdated already. The only and best technique I found so far is described in **Interpolation methods**. 134 | 135 | ## Controling Redux with Token downsampling 136 | To shrink the Redux prompt and increase the influence of the user prompt, we can use a simple trick: We take the 27x27 image patches and split them into 9x9 blocks, each containing 3x3 patches. We then merge all 3x3 tokens into one by averaging their latent embeddings. So instead of having a very long prompt with 27x27=729 tokens we now only have 9x9=81 tokens. So our newly added prompt is much smaller than the user provided prompt and, thus, have less influence on the image generation. 137 | 138 | Downsampling is what happens when you use the "medium" setting. Of all three techniques I tried to decrease the Redux effect, downsampling worked best. ~~However, there are no further customization options. You can only downsample to 81 tokens (downsampling more is too much)~~. 139 | 140 | ## Interpolation methods 141 | 142 | Instead of averaging over small blocks of tokens, we can use a convolution function to shrink our 27x27 images patches to an arbitrary size. There are different functions available which most of you probably know from image resizing (its the same procedure). The averaging method above is "area", but there are also other methods available such as "bicubic". 143 | 144 | ## Controling Redux with Token merging 145 | 146 | The idea here is to shrink the Redux prompt length by merging similar tokens together. Just think about large part of your input image contain more or less the same stuff anyways, so why having always 729 tokens? My implementation here is extremely simple and stupid and not very efficient, but anyways: I just go over all Redux tokens and merge two tokens if their cosine similarity is above a user defined threshold. 147 | 148 | Even a threshold like 0.9 is already removing half of the Redux tokens. A threshold of 0.8 is often reducing the Redux tokens so much that they are in similar length as the user prompt. 149 | 150 | I would start with a threshold of 0.8. If the image is blurry, increase the value a bit. If there is no effect of your prompt, decrease the threshold slightly. 151 | 152 | ## Controling Redux with Token downscaling 153 | 154 | We can also just multiply the tokens by a certain strength value. As lower the strength, as closer the values are to zero. This is similar to prompt weighting which was quite popular for earlier stable diffusion versions, but never really worked that well for T5. Nevertheless, this technique seem to work well enough for flux. 155 | 156 | If you use downscaling, you have to use a very low weight. You can directly start with 0.3 and go down to 0.1 if you want to improve the effect. High weights like 0.6 usually have no impact. 157 | 158 | ## Doing both or all three? 159 | 160 | My feeling currently is that downsampling by far works best. So I would first try downsampling with 1:3 and only use the other options if the effect is too weak or too strong. 161 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .nodes import NODE_CLASS_MAPPINGS 2 | 3 | __all__ = ['NODE_CLASS_MAPPINGS'] -------------------------------------------------------------------------------- /advanced_workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 54, 3 | "last_link_id": 168, 4 | "nodes": [ 5 | { 6 | "id": 16, 7 | "type": "KSamplerSelect", 8 | "pos": [ 9 | 480, 10 | 912 11 | ], 12 | "size": [ 13 | 315, 14 | 58 15 | ], 16 | "flags": {}, 17 | "order": 0, 18 | "mode": 0, 19 | "inputs": [], 20 | "outputs": [ 21 | { 22 | "name": "SAMPLER", 23 | "type": "SAMPLER", 24 | "links": [ 25 | 19 26 | ], 27 | "shape": 3 28 | } 29 | ], 30 | "properties": { 31 | "Node name for S&R": "KSamplerSelect" 32 | }, 33 | "widgets_values": [ 34 | "euler" 35 | ] 36 | }, 37 | { 38 | "id": 13, 39 | "type": "SamplerCustomAdvanced", 40 | "pos": [ 41 | 864, 42 | 192 43 | ], 44 | "size": [ 45 | 272.3617858886719, 46 | 124.53733825683594 47 | ], 48 | "flags": {}, 49 | "order": 17, 50 | "mode": 0, 51 | "inputs": [ 52 | { 53 | "name": "noise", 54 | "type": "NOISE", 55 | "link": 37, 56 | "slot_index": 0 57 | }, 58 | { 59 | "name": "guider", 60 | "type": "GUIDER", 61 | "link": 30, 62 | "slot_index": 1 63 | }, 64 | { 65 | "name": "sampler", 66 | "type": "SAMPLER", 67 | "link": 19, 68 | "slot_index": 2 69 | }, 70 | { 71 | "name": "sigmas", 72 | "type": "SIGMAS", 73 | "link": 20, 74 | "slot_index": 3 75 | }, 76 | { 77 | "name": "latent_image", 78 | "type": "LATENT", 79 | "link": 116, 80 | "slot_index": 4 81 | } 82 | ], 83 | "outputs": [ 84 | { 85 | "name": "output", 86 | "type": "LATENT", 87 | "links": [ 88 | 24 89 | ], 90 | "slot_index": 0, 91 | "shape": 3 92 | }, 93 | { 94 | "name": "denoised_output", 95 | "type": "LATENT", 96 | "links": null, 97 | "shape": 3 98 | } 99 | ], 100 | "properties": { 101 | "Node name for S&R": "SamplerCustomAdvanced" 102 | }, 103 | "widgets_values": [] 104 | }, 105 | { 106 | "id": 8, 107 | "type": "VAEDecode", 108 | "pos": [ 109 | 866, 110 | 367 111 | ], 112 | "size": [ 113 | 210, 114 | 46 115 | ], 116 | "flags": {}, 117 | "order": 18, 118 | "mode": 0, 119 | "inputs": [ 120 | { 121 | "name": "samples", 122 | "type": "LATENT", 123 | "link": 24 124 | }, 125 | { 126 | "name": "vae", 127 | "type": "VAE", 128 | "link": 12 129 | } 130 | ], 131 | "outputs": [ 132 | { 133 | "name": "IMAGE", 134 | "type": "IMAGE", 135 | "links": [ 136 | 9 137 | ], 138 | "slot_index": 0 139 | } 140 | ], 141 | "properties": { 142 | "Node name for S&R": "VAEDecode" 143 | }, 144 | "widgets_values": [] 145 | }, 146 | { 147 | "id": 27, 148 | "type": "EmptySD3LatentImage", 149 | "pos": [ 150 | 480, 151 | 624 152 | ], 153 | "size": [ 154 | 315, 155 | 106 156 | ], 157 | "flags": {}, 158 | "order": 10, 159 | "mode": 0, 160 | "inputs": [ 161 | { 162 | "name": "width", 163 | "type": "INT", 164 | "link": 112, 165 | "widget": { 166 | "name": "width" 167 | } 168 | }, 169 | { 170 | "name": "height", 171 | "type": "INT", 172 | "link": 113, 173 | "widget": { 174 | "name": "height" 175 | } 176 | } 177 | ], 178 | "outputs": [ 179 | { 180 | "name": "LATENT", 181 | "type": "LATENT", 182 | "links": [ 183 | 116 184 | ], 185 | "slot_index": 0, 186 | "shape": 3 187 | } 188 | ], 189 | "properties": { 190 | "Node name for S&R": "EmptySD3LatentImage" 191 | }, 192 | "widgets_values": [ 193 | 1152, 194 | 896, 195 | 1 196 | ] 197 | }, 198 | { 199 | "id": 10, 200 | "type": "VAELoader", 201 | "pos": [ 202 | 48, 203 | 432 204 | ], 205 | "size": [ 206 | 311.81634521484375, 207 | 60.429901123046875 208 | ], 209 | "flags": {}, 210 | "order": 1, 211 | "mode": 0, 212 | "inputs": [], 213 | "outputs": [ 214 | { 215 | "name": "VAE", 216 | "type": "VAE", 217 | "links": [ 218 | 12 219 | ], 220 | "slot_index": 0, 221 | "shape": 3 222 | } 223 | ], 224 | "properties": { 225 | "Node name for S&R": "VAELoader" 226 | }, 227 | "widgets_values": [ 228 | "ae.safetensors" 229 | ] 230 | }, 231 | { 232 | "id": 22, 233 | "type": "BasicGuider", 234 | "pos": [ 235 | 960, 236 | 66 237 | ], 238 | "size": [ 239 | 222.3482666015625, 240 | 46 241 | ], 242 | "flags": {}, 243 | "order": 16, 244 | "mode": 0, 245 | "inputs": [ 246 | { 247 | "name": "model", 248 | "type": "MODEL", 249 | "link": 126, 250 | "slot_index": 0 251 | }, 252 | { 253 | "name": "conditioning", 254 | "type": "CONDITIONING", 255 | "link": 168, 256 | "slot_index": 1 257 | } 258 | ], 259 | "outputs": [ 260 | { 261 | "name": "GUIDER", 262 | "type": "GUIDER", 263 | "links": [ 264 | 30 265 | ], 266 | "slot_index": 0, 267 | "shape": 3 268 | } 269 | ], 270 | "properties": { 271 | "Node name for S&R": "BasicGuider" 272 | }, 273 | "widgets_values": [] 274 | }, 275 | { 276 | "id": 35, 277 | "type": "PrimitiveNode", 278 | "pos": [ 279 | 672, 280 | 480 281 | ], 282 | "size": [ 283 | 210, 284 | 82 285 | ], 286 | "flags": {}, 287 | "order": 2, 288 | "mode": 0, 289 | "inputs": [], 290 | "outputs": [ 291 | { 292 | "name": "INT", 293 | "type": "INT", 294 | "links": [ 295 | 113, 296 | 114 297 | ], 298 | "slot_index": 0, 299 | "widget": { 300 | "name": "height" 301 | } 302 | } 303 | ], 304 | "title": "height", 305 | "properties": { 306 | "Run widget replace on values": false 307 | }, 308 | "widgets_values": [ 309 | 896, 310 | "fixed" 311 | ], 312 | "color": "#323", 313 | "bgcolor": "#535" 314 | }, 315 | { 316 | "id": 34, 317 | "type": "PrimitiveNode", 318 | "pos": [ 319 | 432, 320 | 480 321 | ], 322 | "size": [ 323 | 210, 324 | 82 325 | ], 326 | "flags": {}, 327 | "order": 3, 328 | "mode": 0, 329 | "inputs": [], 330 | "outputs": [ 331 | { 332 | "name": "INT", 333 | "type": "INT", 334 | "links": [ 335 | 112, 336 | 115 337 | ], 338 | "slot_index": 0, 339 | "widget": { 340 | "name": "width" 341 | } 342 | } 343 | ], 344 | "title": "width", 345 | "properties": { 346 | "Run widget replace on values": false 347 | }, 348 | "widgets_values": [ 349 | 1152, 350 | "fixed" 351 | ], 352 | "color": "#323", 353 | "bgcolor": "#535" 354 | }, 355 | { 356 | "id": 30, 357 | "type": "ModelSamplingFlux", 358 | "pos": [ 359 | 38, 360 | 654 361 | ], 362 | "size": [ 363 | 315, 364 | 130 365 | ], 366 | "flags": {}, 367 | "order": 12, 368 | "mode": 0, 369 | "inputs": [ 370 | { 371 | "name": "model", 372 | "type": "MODEL", 373 | "link": 56, 374 | "slot_index": 0 375 | }, 376 | { 377 | "name": "width", 378 | "type": "INT", 379 | "link": 115, 380 | "slot_index": 1, 381 | "widget": { 382 | "name": "width" 383 | } 384 | }, 385 | { 386 | "name": "height", 387 | "type": "INT", 388 | "link": 114, 389 | "slot_index": 2, 390 | "widget": { 391 | "name": "height" 392 | } 393 | } 394 | ], 395 | "outputs": [ 396 | { 397 | "name": "MODEL", 398 | "type": "MODEL", 399 | "links": [ 400 | 127 401 | ], 402 | "slot_index": 0, 403 | "shape": 3 404 | } 405 | ], 406 | "properties": { 407 | "Node name for S&R": "ModelSamplingFlux" 408 | }, 409 | "widgets_values": [ 410 | 1.15, 411 | 0.5, 412 | 1152, 413 | 896 414 | ] 415 | }, 416 | { 417 | "id": 17, 418 | "type": "BasicScheduler", 419 | "pos": [ 420 | 480, 421 | 1008 422 | ], 423 | "size": [ 424 | 315, 425 | 106 426 | ], 427 | "flags": {}, 428 | "order": 14, 429 | "mode": 0, 430 | "inputs": [ 431 | { 432 | "name": "model", 433 | "type": "MODEL", 434 | "link": 127, 435 | "slot_index": 0 436 | } 437 | ], 438 | "outputs": [ 439 | { 440 | "name": "SIGMAS", 441 | "type": "SIGMAS", 442 | "links": [ 443 | 20 444 | ], 445 | "shape": 3 446 | } 447 | ], 448 | "properties": { 449 | "Node name for S&R": "BasicScheduler" 450 | }, 451 | "widgets_values": [ 452 | "simple", 453 | 25, 454 | 1 455 | ] 456 | }, 457 | { 458 | "id": 11, 459 | "type": "DualCLIPLoader", 460 | "pos": [ 461 | 23, 462 | 278 463 | ], 464 | "size": [ 465 | 315, 466 | 106 467 | ], 468 | "flags": {}, 469 | "order": 4, 470 | "mode": 0, 471 | "inputs": [], 472 | "outputs": [ 473 | { 474 | "name": "CLIP", 475 | "type": "CLIP", 476 | "links": [ 477 | 10 478 | ], 479 | "slot_index": 0, 480 | "shape": 3 481 | } 482 | ], 483 | "properties": { 484 | "Node name for S&R": "DualCLIPLoader" 485 | }, 486 | "widgets_values": [ 487 | "t5xxl_fp16.safetensors", 488 | "clip_l.safetensors", 489 | "flux" 490 | ] 491 | }, 492 | { 493 | "id": 12, 494 | "type": "UNETLoader", 495 | "pos": [ 496 | 48, 497 | 144 498 | ], 499 | "size": [ 500 | 315, 501 | 82 502 | ], 503 | "flags": {}, 504 | "order": 5, 505 | "mode": 0, 506 | "inputs": [], 507 | "outputs": [ 508 | { 509 | "name": "MODEL", 510 | "type": "MODEL", 511 | "links": [ 512 | 56, 513 | 126 514 | ], 515 | "slot_index": 0, 516 | "shape": 3 517 | } 518 | ], 519 | "properties": { 520 | "Node name for S&R": "UNETLoader" 521 | }, 522 | "widgets_values": [ 523 | "flux1-dev.safetensors", 524 | "fp8_e4m3fn" 525 | ], 526 | "color": "#223", 527 | "bgcolor": "#335" 528 | }, 529 | { 530 | "id": 25, 531 | "type": "RandomNoise", 532 | "pos": [ 533 | 480, 534 | 768 535 | ], 536 | "size": [ 537 | 315, 538 | 82 539 | ], 540 | "flags": {}, 541 | "order": 6, 542 | "mode": 0, 543 | "inputs": [], 544 | "outputs": [ 545 | { 546 | "name": "NOISE", 547 | "type": "NOISE", 548 | "links": [ 549 | 37 550 | ], 551 | "shape": 3 552 | } 553 | ], 554 | "properties": { 555 | "Node name for S&R": "RandomNoise" 556 | }, 557 | "widgets_values": [ 558 | 916915540436202, 559 | "fixed" 560 | ], 561 | "color": "#2a363b", 562 | "bgcolor": "#3f5159" 563 | }, 564 | { 565 | "id": 9, 566 | "type": "SaveImage", 567 | "pos": [ 568 | 1155, 569 | 196 570 | ], 571 | "size": [ 572 | 985.3012084960938, 573 | 1060.3828125 574 | ], 575 | "flags": {}, 576 | "order": 19, 577 | "mode": 0, 578 | "inputs": [ 579 | { 580 | "name": "images", 581 | "type": "IMAGE", 582 | "link": 9 583 | } 584 | ], 585 | "outputs": [], 586 | "properties": {}, 587 | "widgets_values": [ 588 | "ComfyUI" 589 | ] 590 | }, 591 | { 592 | "id": 6, 593 | "type": "CLIPTextEncode", 594 | "pos": [ 595 | 358, 596 | 252 597 | ], 598 | "size": [ 599 | 422.84503173828125, 600 | 164.31304931640625 601 | ], 602 | "flags": {}, 603 | "order": 11, 604 | "mode": 0, 605 | "inputs": [ 606 | { 607 | "name": "clip", 608 | "type": "CLIP", 609 | "link": 10 610 | } 611 | ], 612 | "outputs": [ 613 | { 614 | "name": "CONDITIONING", 615 | "type": "CONDITIONING", 616 | "links": [ 617 | 41 618 | ], 619 | "slot_index": 0 620 | } 621 | ], 622 | "title": "CLIP Text Encode (Positive Prompt)", 623 | "properties": { 624 | "Node name for S&R": "CLIPTextEncode" 625 | }, 626 | "widgets_values": [ 627 | "comic style, vintage comic, cartoon" 628 | ], 629 | "color": "#232", 630 | "bgcolor": "#353" 631 | }, 632 | { 633 | "id": 38, 634 | "type": "CLIPVisionLoader", 635 | "pos": [ 636 | 60, 637 | -410 638 | ], 639 | "size": [ 640 | 370, 641 | 60 642 | ], 643 | "flags": {}, 644 | "order": 7, 645 | "mode": 0, 646 | "inputs": [], 647 | "outputs": [ 648 | { 649 | "name": "CLIP_VISION", 650 | "type": "CLIP_VISION", 651 | "links": [ 652 | 163 653 | ], 654 | "slot_index": 0 655 | } 656 | ], 657 | "properties": { 658 | "Node name for S&R": "CLIPVisionLoader" 659 | }, 660 | "widgets_values": [ 661 | "sigclip_vision_patch14_384.safetensors" 662 | ] 663 | }, 664 | { 665 | "id": 40, 666 | "type": "LoadImage", 667 | "pos": [ 668 | 63, 669 | -301 670 | ], 671 | "size": [ 672 | 315, 673 | 314 674 | ], 675 | "flags": {}, 676 | "order": 8, 677 | "mode": 0, 678 | "inputs": [], 679 | "outputs": [ 680 | { 681 | "name": "IMAGE", 682 | "type": "IMAGE", 683 | "links": [ 684 | 164 685 | ], 686 | "slot_index": 0 687 | }, 688 | { 689 | "name": "MASK", 690 | "type": "MASK", 691 | "links": [], 692 | "slot_index": 1 693 | } 694 | ], 695 | "properties": { 696 | "Node name for S&R": "LoadImage" 697 | }, 698 | "widgets_values": [ 699 | "pasted/image (114).png", 700 | "image" 701 | ] 702 | }, 703 | { 704 | "id": 26, 705 | "type": "FluxGuidance", 706 | "pos": [ 707 | 480, 708 | 144 709 | ], 710 | "size": [ 711 | 317.4000244140625, 712 | 58 713 | ], 714 | "flags": {}, 715 | "order": 13, 716 | "mode": 0, 717 | "inputs": [ 718 | { 719 | "name": "conditioning", 720 | "type": "CONDITIONING", 721 | "link": 41 722 | } 723 | ], 724 | "outputs": [ 725 | { 726 | "name": "CONDITIONING", 727 | "type": "CONDITIONING", 728 | "links": [ 729 | 167 730 | ], 731 | "slot_index": 0, 732 | "shape": 3 733 | } 734 | ], 735 | "properties": { 736 | "Node name for S&R": "FluxGuidance" 737 | }, 738 | "widgets_values": [ 739 | 3.5 740 | ], 741 | "color": "#233", 742 | "bgcolor": "#355" 743 | }, 744 | { 745 | "id": 54, 746 | "type": "ReduxAdvanced", 747 | "pos": [ 748 | 751, 749 | -387 750 | ], 751 | "size": [ 752 | 317.4000244140625, 753 | 234 754 | ], 755 | "flags": {}, 756 | "order": 15, 757 | "mode": 0, 758 | "inputs": [ 759 | { 760 | "name": "conditioning", 761 | "type": "CONDITIONING", 762 | "link": 167 763 | }, 764 | { 765 | "name": "style_model", 766 | "type": "STYLE_MODEL", 767 | "link": 165 768 | }, 769 | { 770 | "name": "clip_vision", 771 | "type": "CLIP_VISION", 772 | "link": 163 773 | }, 774 | { 775 | "name": "image", 776 | "type": "IMAGE", 777 | "link": 164 778 | }, 779 | { 780 | "name": "mask", 781 | "type": "MASK", 782 | "link": null, 783 | "shape": 7 784 | } 785 | ], 786 | "outputs": [ 787 | { 788 | "name": "CONDITIONING", 789 | "type": "CONDITIONING", 790 | "links": [ 791 | 168 792 | ], 793 | "slot_index": 0 794 | }, 795 | { 796 | "name": "IMAGE", 797 | "type": "IMAGE", 798 | "links": null 799 | }, 800 | { 801 | "name": "MASK", 802 | "type": "MASK", 803 | "links": null 804 | } 805 | ], 806 | "properties": { 807 | "Node name for S&R": "ReduxAdvanced" 808 | }, 809 | "widgets_values": [ 810 | 3, 811 | "area", 812 | "center crop (square)", 813 | 1, 814 | 0.1 815 | ] 816 | }, 817 | { 818 | "id": 42, 819 | "type": "StyleModelLoader", 820 | "pos": [ 821 | 444, 822 | -71 823 | ], 824 | "size": [ 825 | 340, 826 | 60 827 | ], 828 | "flags": {}, 829 | "order": 9, 830 | "mode": 0, 831 | "inputs": [], 832 | "outputs": [ 833 | { 834 | "name": "STYLE_MODEL", 835 | "type": "STYLE_MODEL", 836 | "links": [ 837 | 165 838 | ], 839 | "slot_index": 0 840 | } 841 | ], 842 | "properties": { 843 | "Node name for S&R": "StyleModelLoader" 844 | }, 845 | "widgets_values": [ 846 | "flux1-redux-dev.safetensors" 847 | ] 848 | } 849 | ], 850 | "links": [ 851 | [ 852 | 9, 853 | 8, 854 | 0, 855 | 9, 856 | 0, 857 | "IMAGE" 858 | ], 859 | [ 860 | 10, 861 | 11, 862 | 0, 863 | 6, 864 | 0, 865 | "CLIP" 866 | ], 867 | [ 868 | 12, 869 | 10, 870 | 0, 871 | 8, 872 | 1, 873 | "VAE" 874 | ], 875 | [ 876 | 19, 877 | 16, 878 | 0, 879 | 13, 880 | 2, 881 | "SAMPLER" 882 | ], 883 | [ 884 | 20, 885 | 17, 886 | 0, 887 | 13, 888 | 3, 889 | "SIGMAS" 890 | ], 891 | [ 892 | 24, 893 | 13, 894 | 0, 895 | 8, 896 | 0, 897 | "LATENT" 898 | ], 899 | [ 900 | 30, 901 | 22, 902 | 0, 903 | 13, 904 | 1, 905 | "GUIDER" 906 | ], 907 | [ 908 | 37, 909 | 25, 910 | 0, 911 | 13, 912 | 0, 913 | "NOISE" 914 | ], 915 | [ 916 | 41, 917 | 6, 918 | 0, 919 | 26, 920 | 0, 921 | "CONDITIONING" 922 | ], 923 | [ 924 | 56, 925 | 12, 926 | 0, 927 | 30, 928 | 0, 929 | "MODEL" 930 | ], 931 | [ 932 | 112, 933 | 34, 934 | 0, 935 | 27, 936 | 0, 937 | "INT" 938 | ], 939 | [ 940 | 113, 941 | 35, 942 | 0, 943 | 27, 944 | 1, 945 | "INT" 946 | ], 947 | [ 948 | 114, 949 | 35, 950 | 0, 951 | 30, 952 | 2, 953 | "INT" 954 | ], 955 | [ 956 | 115, 957 | 34, 958 | 0, 959 | 30, 960 | 1, 961 | "INT" 962 | ], 963 | [ 964 | 116, 965 | 27, 966 | 0, 967 | 13, 968 | 4, 969 | "LATENT" 970 | ], 971 | [ 972 | 126, 973 | 12, 974 | 0, 975 | 22, 976 | 0, 977 | "MODEL" 978 | ], 979 | [ 980 | 127, 981 | 30, 982 | 0, 983 | 17, 984 | 0, 985 | "MODEL" 986 | ], 987 | [ 988 | 163, 989 | 38, 990 | 0, 991 | 54, 992 | 2, 993 | "CLIP_VISION" 994 | ], 995 | [ 996 | 164, 997 | 40, 998 | 0, 999 | 54, 1000 | 3, 1001 | "IMAGE" 1002 | ], 1003 | [ 1004 | 165, 1005 | 42, 1006 | 0, 1007 | 54, 1008 | 1, 1009 | "STYLE_MODEL" 1010 | ], 1011 | [ 1012 | 167, 1013 | 26, 1014 | 0, 1015 | 54, 1016 | 0, 1017 | "CONDITIONING" 1018 | ], 1019 | [ 1020 | 168, 1021 | 54, 1022 | 0, 1023 | 22, 1024 | 1, 1025 | "CONDITIONING" 1026 | ] 1027 | ], 1028 | "groups": [ 1029 | { 1030 | "id": 1, 1031 | "title": "Redux Model", 1032 | "bounding": [ 1033 | 50, 1034 | -483.6000061035156, 1035 | 1040, 1036 | 507.6000061035156 1037 | ], 1038 | "color": "#3f789e", 1039 | "font_size": 24, 1040 | "flags": {} 1041 | } 1042 | ], 1043 | "config": {}, 1044 | "extra": { 1045 | "ds": { 1046 | "scale": 0.8264462809917354, 1047 | "offset": [ 1048 | 155.27574076895425, 1049 | 405.621837140206 1050 | ] 1051 | }, 1052 | "groupNodes": {} 1053 | }, 1054 | "version": 0.4 1055 | } -------------------------------------------------------------------------------- /nodes.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import torch 3 | import comfy 4 | import folder_paths 5 | import nodes 6 | import os 7 | import math 8 | import re 9 | import safetensors 10 | import glob 11 | from collections import namedtuple 12 | 13 | @torch.no_grad() 14 | def automerge(tensor, threshold): 15 | (batchsize, slices, dim) = tensor.shape 16 | newTensor=[] 17 | for batch in range(batchsize): 18 | tokens = [] 19 | lastEmbed = tensor[batch,0,:] 20 | merge=[lastEmbed] 21 | tokens.append(lastEmbed) 22 | for i in range(1,slices): 23 | tok = tensor[batch,i,:] 24 | cosine = torch.dot(tok,lastEmbed)/torch.sqrt(torch.dot(tok,tok)*torch.dot(lastEmbed,lastEmbed)) 25 | if cosine >= threshold: 26 | merge.append(tok) 27 | lastEmbed = torch.stack(merge).mean(dim=0) 28 | else: 29 | tokens.append(lastEmbed) 30 | merge=[] 31 | lastEmbed=tok 32 | newTensor.append(torch.stack(tokens)) 33 | return torch.stack(newTensor) 34 | 35 | STRENGTHS = ["highest", "high", "medium", "low", "lowest"] 36 | STRENGTHS_VALUES = [1,2, 3,4,5] 37 | 38 | class StyleModelApplySimple: 39 | @classmethod 40 | def INPUT_TYPES(s): 41 | return {"required": {"conditioning": ("CONDITIONING", ), 42 | "style_model": ("STYLE_MODEL", ), 43 | "clip_vision_output": ("CLIP_VISION_OUTPUT", ), 44 | "image_strength": (STRENGTHS, {"default": "medium"}) 45 | }} 46 | RETURN_TYPES = ("CONDITIONING",) 47 | FUNCTION = "apply_stylemodel" 48 | 49 | CATEGORY = "conditioning/style_model" 50 | 51 | def apply_stylemodel(self, clip_vision_output, style_model, conditioning, image_strength): 52 | stren = STRENGTHS.index(image_strength) 53 | downsampling_factor = STRENGTHS_VALUES[stren] 54 | mode="area" if downsampling_factor==3 else "bicubic" 55 | cond = style_model.get_cond(clip_vision_output).flatten(start_dim=0, end_dim=1).unsqueeze(dim=0) 56 | if downsampling_factor>1: 57 | (b,t,h)=cond.shape 58 | m = int(np.sqrt(t)) 59 | cond=torch.nn.functional.interpolate(cond.view(b, m, m, h).transpose(1,-1), size=(m//downsampling_factor, m//downsampling_factor), mode=mode)# 60 | cond=cond.transpose(1,-1).reshape(b,-1,h) 61 | c = [] 62 | for t in conditioning: 63 | n = [torch.cat((t[0], cond), dim=1), t[1].copy()] 64 | c.append(n) 65 | return (c, ) 66 | 67 | def standardizeMask(mask): 68 | if mask is None: 69 | return None 70 | if len(mask.shape) == 2: 71 | (h,w)=mask.shape 72 | mask=mask.view(1,1,h,w) 73 | elif len(mask.shape)==3: 74 | (b,h,w)=mask.shape 75 | mask=mask.view(b,1,h,w) 76 | return mask 77 | 78 | def crop(img, mask, box, desiredSize): 79 | (ox,oy,w,h) = box 80 | if mask is not None: 81 | mask=torch.nn.functional.interpolate(mask, size=(h,w), mode="bicubic").view(-1,h,w,1) 82 | img = torch.nn.functional.interpolate(img.transpose(-1,1), size=(w,h), mode="bicubic", antialias=True) 83 | return (img[:, :, ox:(desiredSize+ox), oy:(desiredSize+oy)].transpose(1,-1), None if mask == None else mask[:, oy:(desiredSize+oy), ox:(desiredSize+ox),:]) 84 | 85 | def letterbox(img, mask, w, h, desiredSize): 86 | (b,oh,ow,c) = img.shape 87 | img = torch.nn.functional.interpolate(img.transpose(-1,1), size=(w,h), mode="bicubic", antialias=True).transpose(1,-1) 88 | letterbox = torch.zeros(size=(b,desiredSize,desiredSize, c)) 89 | offsetx = (desiredSize-w)//2 90 | offsety = (desiredSize-h)//2 91 | letterbox[:, offsety:(offsety+h), offsetx:(offsetx+w), :] += img 92 | img = letterbox 93 | if mask is not None: 94 | mask=torch.nn.functional.interpolate(mask, size=(h,w), mode="bicubic") 95 | letterbox = torch.zeros(size=(b,1,desiredSize,desiredSize)) 96 | letterbox[:, :, offsety:(offsety+h), offsetx:(offsetx+w)] += mask 97 | mask = letterbox.view(b,1,desiredSize,desiredSize) 98 | return (img, mask) 99 | 100 | def getBoundingBox(mask, w, h, relativeMargin, desiredSize): 101 | mask=mask.view(h,w) 102 | marginW = math.ceil(relativeMargin * w) 103 | marginH = math.ceil(relativeMargin * h) 104 | indices = torch.nonzero(mask, as_tuple=False) 105 | y_min, x_min = indices.min(dim=0).values 106 | y_max, x_max = indices.max(dim=0).values 107 | x_min = max(0, x_min.item() - marginW) 108 | y_min = max(0, y_min.item() - marginH) 109 | x_max = min(w, x_max.item() + marginW) 110 | y_max = min(h, y_max.item() + marginH) 111 | 112 | box_width = x_max - x_min 113 | box_height = y_max - y_min 114 | 115 | larger_edge = max(box_width, box_height, desiredSize) 116 | if box_width < larger_edge: 117 | delta = larger_edge - box_width 118 | left_space = x_min 119 | right_space = w - x_max 120 | expand_left = min(delta // 2, left_space) 121 | expand_right = min(delta - expand_left, right_space) 122 | expand_left += min(delta - (expand_left+expand_right), left_space-expand_left) 123 | x_min -= expand_left 124 | x_max += expand_right 125 | 126 | if box_height < larger_edge: 127 | delta = larger_edge - box_height 128 | top_space = y_min 129 | bottom_space = h - y_max 130 | expand_top = min(delta // 2, top_space) 131 | expand_bottom = min(delta - expand_top, bottom_space) 132 | expand_top += min(delta - (expand_top+expand_bottom), top_space-expand_top) 133 | y_min -= expand_top 134 | y_max += expand_bottom 135 | 136 | x_min = max(0, x_min) 137 | y_min = max(0, y_min) 138 | x_max = min(w, x_max) 139 | y_max = min(h, y_max) 140 | return x_min, y_min, x_max, y_max 141 | 142 | 143 | def patchifyMask(mask, patchSize=14): 144 | if mask is None: 145 | return mask 146 | (b, imgSize, imgSize,_) = mask.shape 147 | toks = imgSize//patchSize 148 | return torch.nn.MaxPool2d(kernel_size=(patchSize,patchSize),stride=patchSize)(mask.view(b,imgSize,imgSize)).view(b,toks,toks,1) 149 | 150 | def prepareImageAndMask(visionEncoder, image, mask, mode, autocrop_margin, desiredSize=384): 151 | mode = IMAGE_MODES.index(mode) 152 | (B,H,W,C) = image.shape 153 | if mode==0: # center crop square 154 | imgsize = min(H,W) 155 | ratio = desiredSize/imgsize 156 | (w,h) = (round(W*ratio), round(H*ratio)) 157 | image, mask = crop(image, standardizeMask(mask), ((w - desiredSize)//2, (h - desiredSize)//2, w, h), desiredSize) 158 | elif mode==1: 159 | if mask is None: 160 | mask = torch.ones(size=(B,H,W)) 161 | imgsize = max(H,W) 162 | ratio = desiredSize/imgsize 163 | (w,h) = (round(W*ratio), round(H*ratio)) 164 | image, mask = letterbox(image, standardizeMask(mask), w, h, desiredSize) 165 | elif mode==2: 166 | (bx,by,bx2,by2) = getBoundingBox(mask,W,H,autocrop_margin, desiredSize) 167 | image = image[:,by:by2,bx:bx2,:] 168 | mask = mask[:,by:by2,bx:bx2] 169 | imgsize = max(bx2-bx,by2-by) 170 | ratio = desiredSize/imgsize 171 | (w,h) = (round((bx2-bx)*ratio), round((by2-by)*ratio)) 172 | image, mask = letterbox(image, standardizeMask(mask), w, h, desiredSize) 173 | return (image,mask) 174 | 175 | def processMask(mask,imgSize=384, patchSize=14): 176 | if len(mask.shape) == 2: 177 | (h,w)=mask.shape 178 | mask=mask.view(1,1,h,w) 179 | elif len(mask.shape)==3: 180 | (b,h,w)=mask.shape 181 | mask=mask.view(b,1,h,w) 182 | scalingFactor = imgSize/min(h,w) 183 | # scale 184 | mask=torch.nn.functional.interpolate(mask, size=(round(h*scalingFactor),round(w*scalingFactor)), mode="bicubic") 185 | # crop 186 | horizontalBorder = (imgSize-mask.shape[3])//2 187 | verticalBorder = (imgSize-mask.shape[2])//2 188 | mask=mask[:, :, verticalBorder:(verticalBorder+imgSize),horizontalBorder:(horizontalBorder+imgSize)].view(b,imgSize,imgSize) 189 | toks = imgSize//patchSize 190 | return torch.nn.MaxPool2d(kernel_size=(patchSize,patchSize),stride=patchSize)(mask).view(b,toks,toks,1) 191 | 192 | IMAGE_MODES = [ 193 | "center crop (square)", 194 | "keep aspect ratio", 195 | "autocrop with mask" 196 | ] 197 | 198 | class ReduxAdvanced: 199 | @classmethod 200 | def INPUT_TYPES(s): 201 | return {"required": {"conditioning": ("CONDITIONING", ), 202 | "style_model": ("STYLE_MODEL", ), 203 | "clip_vision": ("CLIP_VISION", ), 204 | "image": ("IMAGE",), 205 | "downsampling_factor": ("FLOAT", {"default": 3, "min": 1, "max":9, "step": 0.1}), 206 | "downsampling_function": (["nearest", "bilinear", "bicubic","area","nearest-exact"], {"default": "area"}), 207 | "mode": (IMAGE_MODES, {"default": "center crop (square)"}), 208 | "weight": ("FLOAT", {"default": 1.0, "min":0.0, "max":1.0, "step":0.01}) 209 | }, 210 | "optional": { 211 | "mask": ("MASK", ), 212 | "autocrop_margin": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.01}) 213 | }} 214 | RETURN_TYPES = ("CONDITIONING","IMAGE", "MASK") 215 | FUNCTION = "apply_stylemodel" 216 | 217 | CATEGORY = "conditioning/style_model" 218 | 219 | def apply_stylemodel(self, clip_vision, image, style_model, conditioning, downsampling_factor, downsampling_function,mode,weight, mask=None, autocrop_margin=0.0): 220 | desiredSize = 384 221 | patchSize = 14 222 | if clip_vision.model.vision_model.embeddings.position_embedding.weight.shape[0] == 1024: 223 | desiredSize = 512 224 | patchSize = 16 225 | image, masko = prepareImageAndMask(clip_vision, image, mask, mode, autocrop_margin, desiredSize) 226 | clip_vision_output,mask=(clip_vision.encode_image(image), patchifyMask(masko, patchSize)) 227 | mode="area" 228 | cond = style_model.get_cond(clip_vision_output).flatten(start_dim=0, end_dim=1).unsqueeze(dim=0) 229 | (b,t,h)=cond.shape 230 | m = int(np.sqrt(t)) 231 | if downsampling_factor>1: 232 | cond = cond.view(b, m, m, h) 233 | if mask is not None: 234 | cond = cond*mask 235 | downsampled_size = (round(m/downsampling_factor), round(m/downsampling_factor)) 236 | cond=torch.nn.functional.interpolate(cond.transpose(1,-1), size=downsampled_size, mode=downsampling_function) 237 | cond=cond.transpose(1,-1).reshape(b,-1,h) 238 | mask = None if mask is None else torch.nn.functional.interpolate(mask.view(b, m, m, 1).transpose(1,-1), size=downsampled_size, mode=mode).transpose(-1,1) 239 | cond = cond*(weight*weight) 240 | c = [] 241 | if mask is not None: 242 | mask = (mask>0).reshape(b,-1) 243 | max_len = mask.sum(dim=1).max().item() 244 | padded_embeddings = torch.zeros((b, max_len, h), dtype=cond.dtype, device=cond.device) 245 | for i in range(b): 246 | filtered = cond[i][mask[i]] 247 | padded_embeddings[i, :filtered.size(0)] = filtered 248 | cond = padded_embeddings 249 | 250 | for t in conditioning: 251 | n = [torch.cat((t[0], cond), dim=1), t[1].copy()] 252 | c.append(n) 253 | return (c, image, masko.squeeze(-1) if masko is not None else None) 254 | 255 | 256 | # A dictionary that contains all nodes you want to export with their names 257 | # NOTE: names should be globally unique 258 | NODE_CLASS_MAPPINGS = { 259 | "StyleModelApplySimple": StyleModelApplySimple, 260 | "ReduxAdvanced": ReduxAdvanced 261 | } 262 | 263 | # A dictionary that contains the friendly/humanly readable titles for the nodes 264 | NODE_DISPLAY_NAME_MAPPINGS = { 265 | "StyleModelApplySimple": "Apply style model (simple)", 266 | "ReduxAdvanced": "Apply Redux model (advanced)" 267 | } 268 | -------------------------------------------------------------------------------- /simple_workflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 55, 3 | "last_link_id": 169, 4 | "nodes": [ 5 | { 6 | "id": 16, 7 | "type": "KSamplerSelect", 8 | "pos": [ 9 | 480, 10 | 912 11 | ], 12 | "size": [ 13 | 315, 14 | 58 15 | ], 16 | "flags": {}, 17 | "order": 0, 18 | "mode": 0, 19 | "inputs": [], 20 | "outputs": [ 21 | { 22 | "name": "SAMPLER", 23 | "type": "SAMPLER", 24 | "links": [ 25 | 19 26 | ], 27 | "shape": 3 28 | } 29 | ], 30 | "properties": { 31 | "Node name for S&R": "KSamplerSelect" 32 | }, 33 | "widgets_values": [ 34 | "euler" 35 | ] 36 | }, 37 | { 38 | "id": 13, 39 | "type": "SamplerCustomAdvanced", 40 | "pos": [ 41 | 864, 42 | 192 43 | ], 44 | "size": [ 45 | 272.3617858886719, 46 | 124.53733825683594 47 | ], 48 | "flags": {}, 49 | "order": 18, 50 | "mode": 0, 51 | "inputs": [ 52 | { 53 | "name": "noise", 54 | "type": "NOISE", 55 | "link": 37, 56 | "slot_index": 0 57 | }, 58 | { 59 | "name": "guider", 60 | "type": "GUIDER", 61 | "link": 30, 62 | "slot_index": 1 63 | }, 64 | { 65 | "name": "sampler", 66 | "type": "SAMPLER", 67 | "link": 19, 68 | "slot_index": 2 69 | }, 70 | { 71 | "name": "sigmas", 72 | "type": "SIGMAS", 73 | "link": 20, 74 | "slot_index": 3 75 | }, 76 | { 77 | "name": "latent_image", 78 | "type": "LATENT", 79 | "link": 116, 80 | "slot_index": 4 81 | } 82 | ], 83 | "outputs": [ 84 | { 85 | "name": "output", 86 | "type": "LATENT", 87 | "links": [ 88 | 24 89 | ], 90 | "slot_index": 0, 91 | "shape": 3 92 | }, 93 | { 94 | "name": "denoised_output", 95 | "type": "LATENT", 96 | "links": null, 97 | "shape": 3 98 | } 99 | ], 100 | "properties": { 101 | "Node name for S&R": "SamplerCustomAdvanced" 102 | }, 103 | "widgets_values": [] 104 | }, 105 | { 106 | "id": 8, 107 | "type": "VAEDecode", 108 | "pos": [ 109 | 866, 110 | 367 111 | ], 112 | "size": [ 113 | 210, 114 | 46 115 | ], 116 | "flags": {}, 117 | "order": 19, 118 | "mode": 0, 119 | "inputs": [ 120 | { 121 | "name": "samples", 122 | "type": "LATENT", 123 | "link": 24 124 | }, 125 | { 126 | "name": "vae", 127 | "type": "VAE", 128 | "link": 12 129 | } 130 | ], 131 | "outputs": [ 132 | { 133 | "name": "IMAGE", 134 | "type": "IMAGE", 135 | "links": [ 136 | 9 137 | ], 138 | "slot_index": 0 139 | } 140 | ], 141 | "properties": { 142 | "Node name for S&R": "VAEDecode" 143 | }, 144 | "widgets_values": [] 145 | }, 146 | { 147 | "id": 27, 148 | "type": "EmptySD3LatentImage", 149 | "pos": [ 150 | 480, 151 | 624 152 | ], 153 | "size": [ 154 | 315, 155 | 106 156 | ], 157 | "flags": {}, 158 | "order": 10, 159 | "mode": 0, 160 | "inputs": [ 161 | { 162 | "name": "width", 163 | "type": "INT", 164 | "link": 112, 165 | "widget": { 166 | "name": "width" 167 | } 168 | }, 169 | { 170 | "name": "height", 171 | "type": "INT", 172 | "link": 113, 173 | "widget": { 174 | "name": "height" 175 | } 176 | } 177 | ], 178 | "outputs": [ 179 | { 180 | "name": "LATENT", 181 | "type": "LATENT", 182 | "links": [ 183 | 116 184 | ], 185 | "slot_index": 0, 186 | "shape": 3 187 | } 188 | ], 189 | "properties": { 190 | "Node name for S&R": "EmptySD3LatentImage" 191 | }, 192 | "widgets_values": [ 193 | 1152, 194 | 896, 195 | 1 196 | ] 197 | }, 198 | { 199 | "id": 10, 200 | "type": "VAELoader", 201 | "pos": [ 202 | 48, 203 | 432 204 | ], 205 | "size": [ 206 | 311.81634521484375, 207 | 60.429901123046875 208 | ], 209 | "flags": {}, 210 | "order": 1, 211 | "mode": 0, 212 | "inputs": [], 213 | "outputs": [ 214 | { 215 | "name": "VAE", 216 | "type": "VAE", 217 | "links": [ 218 | 12 219 | ], 220 | "slot_index": 0, 221 | "shape": 3 222 | } 223 | ], 224 | "properties": { 225 | "Node name for S&R": "VAELoader" 226 | }, 227 | "widgets_values": [ 228 | "ae.safetensors" 229 | ] 230 | }, 231 | { 232 | "id": 22, 233 | "type": "BasicGuider", 234 | "pos": [ 235 | 960, 236 | 66 237 | ], 238 | "size": [ 239 | 222.3482666015625, 240 | 46 241 | ], 242 | "flags": {}, 243 | "order": 17, 244 | "mode": 0, 245 | "inputs": [ 246 | { 247 | "name": "model", 248 | "type": "MODEL", 249 | "link": 126, 250 | "slot_index": 0 251 | }, 252 | { 253 | "name": "conditioning", 254 | "type": "CONDITIONING", 255 | "link": 169, 256 | "slot_index": 1 257 | } 258 | ], 259 | "outputs": [ 260 | { 261 | "name": "GUIDER", 262 | "type": "GUIDER", 263 | "links": [ 264 | 30 265 | ], 266 | "slot_index": 0, 267 | "shape": 3 268 | } 269 | ], 270 | "properties": { 271 | "Node name for S&R": "BasicGuider" 272 | }, 273 | "widgets_values": [] 274 | }, 275 | { 276 | "id": 35, 277 | "type": "PrimitiveNode", 278 | "pos": [ 279 | 672, 280 | 480 281 | ], 282 | "size": [ 283 | 210, 284 | 82 285 | ], 286 | "flags": {}, 287 | "order": 2, 288 | "mode": 0, 289 | "inputs": [], 290 | "outputs": [ 291 | { 292 | "name": "INT", 293 | "type": "INT", 294 | "links": [ 295 | 113, 296 | 114 297 | ], 298 | "slot_index": 0, 299 | "widget": { 300 | "name": "height" 301 | } 302 | } 303 | ], 304 | "title": "height", 305 | "properties": { 306 | "Run widget replace on values": false 307 | }, 308 | "widgets_values": [ 309 | 896, 310 | "fixed" 311 | ], 312 | "color": "#323", 313 | "bgcolor": "#535" 314 | }, 315 | { 316 | "id": 34, 317 | "type": "PrimitiveNode", 318 | "pos": [ 319 | 432, 320 | 480 321 | ], 322 | "size": [ 323 | 210, 324 | 82 325 | ], 326 | "flags": {}, 327 | "order": 3, 328 | "mode": 0, 329 | "inputs": [], 330 | "outputs": [ 331 | { 332 | "name": "INT", 333 | "type": "INT", 334 | "links": [ 335 | 112, 336 | 115 337 | ], 338 | "slot_index": 0, 339 | "widget": { 340 | "name": "width" 341 | } 342 | } 343 | ], 344 | "title": "width", 345 | "properties": { 346 | "Run widget replace on values": false 347 | }, 348 | "widgets_values": [ 349 | 1152, 350 | "fixed" 351 | ], 352 | "color": "#323", 353 | "bgcolor": "#535" 354 | }, 355 | { 356 | "id": 30, 357 | "type": "ModelSamplingFlux", 358 | "pos": [ 359 | 38, 360 | 654 361 | ], 362 | "size": [ 363 | 315, 364 | 130 365 | ], 366 | "flags": {}, 367 | "order": 12, 368 | "mode": 0, 369 | "inputs": [ 370 | { 371 | "name": "model", 372 | "type": "MODEL", 373 | "link": 56, 374 | "slot_index": 0 375 | }, 376 | { 377 | "name": "width", 378 | "type": "INT", 379 | "link": 115, 380 | "slot_index": 1, 381 | "widget": { 382 | "name": "width" 383 | } 384 | }, 385 | { 386 | "name": "height", 387 | "type": "INT", 388 | "link": 114, 389 | "slot_index": 2, 390 | "widget": { 391 | "name": "height" 392 | } 393 | } 394 | ], 395 | "outputs": [ 396 | { 397 | "name": "MODEL", 398 | "type": "MODEL", 399 | "links": [ 400 | 127 401 | ], 402 | "slot_index": 0, 403 | "shape": 3 404 | } 405 | ], 406 | "properties": { 407 | "Node name for S&R": "ModelSamplingFlux" 408 | }, 409 | "widgets_values": [ 410 | 1.15, 411 | 0.5, 412 | 1152, 413 | 896 414 | ] 415 | }, 416 | { 417 | "id": 17, 418 | "type": "BasicScheduler", 419 | "pos": [ 420 | 480, 421 | 1008 422 | ], 423 | "size": [ 424 | 315, 425 | 106 426 | ], 427 | "flags": {}, 428 | "order": 15, 429 | "mode": 0, 430 | "inputs": [ 431 | { 432 | "name": "model", 433 | "type": "MODEL", 434 | "link": 127, 435 | "slot_index": 0 436 | } 437 | ], 438 | "outputs": [ 439 | { 440 | "name": "SIGMAS", 441 | "type": "SIGMAS", 442 | "links": [ 443 | 20 444 | ], 445 | "shape": 3 446 | } 447 | ], 448 | "properties": { 449 | "Node name for S&R": "BasicScheduler" 450 | }, 451 | "widgets_values": [ 452 | "simple", 453 | 25, 454 | 1 455 | ] 456 | }, 457 | { 458 | "id": 11, 459 | "type": "DualCLIPLoader", 460 | "pos": [ 461 | 23, 462 | 278 463 | ], 464 | "size": [ 465 | 315, 466 | 106 467 | ], 468 | "flags": {}, 469 | "order": 4, 470 | "mode": 0, 471 | "inputs": [], 472 | "outputs": [ 473 | { 474 | "name": "CLIP", 475 | "type": "CLIP", 476 | "links": [ 477 | 10 478 | ], 479 | "slot_index": 0, 480 | "shape": 3 481 | } 482 | ], 483 | "properties": { 484 | "Node name for S&R": "DualCLIPLoader" 485 | }, 486 | "widgets_values": [ 487 | "t5xxl_fp16.safetensors", 488 | "clip_l.safetensors", 489 | "flux" 490 | ] 491 | }, 492 | { 493 | "id": 12, 494 | "type": "UNETLoader", 495 | "pos": [ 496 | 48, 497 | 144 498 | ], 499 | "size": [ 500 | 315, 501 | 82 502 | ], 503 | "flags": {}, 504 | "order": 5, 505 | "mode": 0, 506 | "inputs": [], 507 | "outputs": [ 508 | { 509 | "name": "MODEL", 510 | "type": "MODEL", 511 | "links": [ 512 | 56, 513 | 126 514 | ], 515 | "slot_index": 0, 516 | "shape": 3 517 | } 518 | ], 519 | "properties": { 520 | "Node name for S&R": "UNETLoader" 521 | }, 522 | "widgets_values": [ 523 | "flux1-dev.safetensors", 524 | "fp8_e4m3fn" 525 | ], 526 | "color": "#223", 527 | "bgcolor": "#335" 528 | }, 529 | { 530 | "id": 25, 531 | "type": "RandomNoise", 532 | "pos": [ 533 | 480, 534 | 768 535 | ], 536 | "size": [ 537 | 315, 538 | 82 539 | ], 540 | "flags": {}, 541 | "order": 6, 542 | "mode": 0, 543 | "inputs": [], 544 | "outputs": [ 545 | { 546 | "name": "NOISE", 547 | "type": "NOISE", 548 | "links": [ 549 | 37 550 | ], 551 | "shape": 3 552 | } 553 | ], 554 | "properties": { 555 | "Node name for S&R": "RandomNoise" 556 | }, 557 | "widgets_values": [ 558 | 916915540436202, 559 | "fixed" 560 | ], 561 | "color": "#2a363b", 562 | "bgcolor": "#3f5159" 563 | }, 564 | { 565 | "id": 9, 566 | "type": "SaveImage", 567 | "pos": [ 568 | 1155, 569 | 196 570 | ], 571 | "size": [ 572 | 985.3012084960938, 573 | 1060.3828125 574 | ], 575 | "flags": {}, 576 | "order": 20, 577 | "mode": 0, 578 | "inputs": [ 579 | { 580 | "name": "images", 581 | "type": "IMAGE", 582 | "link": 9 583 | } 584 | ], 585 | "outputs": [], 586 | "properties": {}, 587 | "widgets_values": [ 588 | "ComfyUI" 589 | ] 590 | }, 591 | { 592 | "id": 6, 593 | "type": "CLIPTextEncode", 594 | "pos": [ 595 | 358, 596 | 252 597 | ], 598 | "size": [ 599 | 422.84503173828125, 600 | 164.31304931640625 601 | ], 602 | "flags": {}, 603 | "order": 11, 604 | "mode": 0, 605 | "inputs": [ 606 | { 607 | "name": "clip", 608 | "type": "CLIP", 609 | "link": 10 610 | } 611 | ], 612 | "outputs": [ 613 | { 614 | "name": "CONDITIONING", 615 | "type": "CONDITIONING", 616 | "links": [ 617 | 41 618 | ], 619 | "slot_index": 0 620 | } 621 | ], 622 | "title": "CLIP Text Encode (Positive Prompt)", 623 | "properties": { 624 | "Node name for S&R": "CLIPTextEncode" 625 | }, 626 | "widgets_values": [ 627 | "comic style, vintage comic, cartoon" 628 | ], 629 | "color": "#232", 630 | "bgcolor": "#353" 631 | }, 632 | { 633 | "id": 38, 634 | "type": "CLIPVisionLoader", 635 | "pos": [ 636 | 60, 637 | -410 638 | ], 639 | "size": [ 640 | 370, 641 | 60 642 | ], 643 | "flags": {}, 644 | "order": 7, 645 | "mode": 0, 646 | "inputs": [], 647 | "outputs": [ 648 | { 649 | "name": "CLIP_VISION", 650 | "type": "CLIP_VISION", 651 | "links": [ 652 | 164 653 | ], 654 | "slot_index": 0 655 | } 656 | ], 657 | "properties": { 658 | "Node name for S&R": "CLIPVisionLoader" 659 | }, 660 | "widgets_values": [ 661 | "sigclip_vision_patch14_384.safetensors" 662 | ] 663 | }, 664 | { 665 | "id": 40, 666 | "type": "LoadImage", 667 | "pos": [ 668 | 63, 669 | -301 670 | ], 671 | "size": [ 672 | 315, 673 | 314 674 | ], 675 | "flags": {}, 676 | "order": 8, 677 | "mode": 0, 678 | "inputs": [], 679 | "outputs": [ 680 | { 681 | "name": "IMAGE", 682 | "type": "IMAGE", 683 | "links": [ 684 | 165 685 | ], 686 | "slot_index": 0 687 | }, 688 | { 689 | "name": "MASK", 690 | "type": "MASK", 691 | "links": [], 692 | "slot_index": 1 693 | } 694 | ], 695 | "properties": { 696 | "Node name for S&R": "LoadImage" 697 | }, 698 | "widgets_values": [ 699 | "large.png", 700 | "image" 701 | ] 702 | }, 703 | { 704 | "id": 55, 705 | "type": "CLIPVisionEncode", 706 | "pos": [ 707 | 425, 708 | -223 709 | ], 710 | "size": [ 711 | 380.4000244140625, 712 | 46 713 | ], 714 | "flags": {}, 715 | "order": 13, 716 | "mode": 0, 717 | "inputs": [ 718 | { 719 | "name": "clip_vision", 720 | "type": "CLIP_VISION", 721 | "link": 164 722 | }, 723 | { 724 | "name": "image", 725 | "type": "IMAGE", 726 | "link": 165 727 | } 728 | ], 729 | "outputs": [ 730 | { 731 | "name": "CLIP_VISION_OUTPUT", 732 | "type": "CLIP_VISION_OUTPUT", 733 | "links": [ 734 | 166 735 | ], 736 | "slot_index": 0 737 | } 738 | ], 739 | "properties": { 740 | "Node name for S&R": "CLIPVisionEncode" 741 | } 742 | }, 743 | { 744 | "id": 42, 745 | "type": "StyleModelLoader", 746 | "pos": [ 747 | 415, 748 | -105 749 | ], 750 | "size": [ 751 | 340, 752 | 60 753 | ], 754 | "flags": {}, 755 | "order": 9, 756 | "mode": 0, 757 | "inputs": [], 758 | "outputs": [ 759 | { 760 | "name": "STYLE_MODEL", 761 | "type": "STYLE_MODEL", 762 | "links": [ 763 | 167 764 | ], 765 | "slot_index": 0 766 | } 767 | ], 768 | "properties": { 769 | "Node name for S&R": "StyleModelLoader" 770 | }, 771 | "widgets_values": [ 772 | "flux1-redux-dev.safetensors" 773 | ] 774 | }, 775 | { 776 | "id": 26, 777 | "type": "FluxGuidance", 778 | "pos": [ 779 | 480, 780 | 144 781 | ], 782 | "size": [ 783 | 317.4000244140625, 784 | 58 785 | ], 786 | "flags": {}, 787 | "order": 14, 788 | "mode": 0, 789 | "inputs": [ 790 | { 791 | "name": "conditioning", 792 | "type": "CONDITIONING", 793 | "link": 41 794 | } 795 | ], 796 | "outputs": [ 797 | { 798 | "name": "CONDITIONING", 799 | "type": "CONDITIONING", 800 | "links": [ 801 | 168 802 | ], 803 | "slot_index": 0, 804 | "shape": 3 805 | } 806 | ], 807 | "properties": { 808 | "Node name for S&R": "FluxGuidance" 809 | }, 810 | "widgets_values": [ 811 | 3.5 812 | ], 813 | "color": "#233", 814 | "bgcolor": "#355" 815 | }, 816 | { 817 | "id": 54, 818 | "type": "StyleModelApplySimple", 819 | "pos": [ 820 | 666, 821 | -413 822 | ], 823 | "size": [ 824 | 393, 825 | 98 826 | ], 827 | "flags": {}, 828 | "order": 16, 829 | "mode": 0, 830 | "inputs": [ 831 | { 832 | "name": "conditioning", 833 | "type": "CONDITIONING", 834 | "link": 168 835 | }, 836 | { 837 | "name": "style_model", 838 | "type": "STYLE_MODEL", 839 | "link": 167 840 | }, 841 | { 842 | "name": "clip_vision_output", 843 | "type": "CLIP_VISION_OUTPUT", 844 | "link": 166 845 | } 846 | ], 847 | "outputs": [ 848 | { 849 | "name": "CONDITIONING", 850 | "type": "CONDITIONING", 851 | "links": [ 852 | 169 853 | ], 854 | "slot_index": 0 855 | } 856 | ], 857 | "properties": { 858 | "Node name for S&R": "StyleModelApplySimple" 859 | }, 860 | "widgets_values": [ 861 | "medium" 862 | ] 863 | } 864 | ], 865 | "links": [ 866 | [ 867 | 9, 868 | 8, 869 | 0, 870 | 9, 871 | 0, 872 | "IMAGE" 873 | ], 874 | [ 875 | 10, 876 | 11, 877 | 0, 878 | 6, 879 | 0, 880 | "CLIP" 881 | ], 882 | [ 883 | 12, 884 | 10, 885 | 0, 886 | 8, 887 | 1, 888 | "VAE" 889 | ], 890 | [ 891 | 19, 892 | 16, 893 | 0, 894 | 13, 895 | 2, 896 | "SAMPLER" 897 | ], 898 | [ 899 | 20, 900 | 17, 901 | 0, 902 | 13, 903 | 3, 904 | "SIGMAS" 905 | ], 906 | [ 907 | 24, 908 | 13, 909 | 0, 910 | 8, 911 | 0, 912 | "LATENT" 913 | ], 914 | [ 915 | 30, 916 | 22, 917 | 0, 918 | 13, 919 | 1, 920 | "GUIDER" 921 | ], 922 | [ 923 | 37, 924 | 25, 925 | 0, 926 | 13, 927 | 0, 928 | "NOISE" 929 | ], 930 | [ 931 | 41, 932 | 6, 933 | 0, 934 | 26, 935 | 0, 936 | "CONDITIONING" 937 | ], 938 | [ 939 | 56, 940 | 12, 941 | 0, 942 | 30, 943 | 0, 944 | "MODEL" 945 | ], 946 | [ 947 | 112, 948 | 34, 949 | 0, 950 | 27, 951 | 0, 952 | "INT" 953 | ], 954 | [ 955 | 113, 956 | 35, 957 | 0, 958 | 27, 959 | 1, 960 | "INT" 961 | ], 962 | [ 963 | 114, 964 | 35, 965 | 0, 966 | 30, 967 | 2, 968 | "INT" 969 | ], 970 | [ 971 | 115, 972 | 34, 973 | 0, 974 | 30, 975 | 1, 976 | "INT" 977 | ], 978 | [ 979 | 116, 980 | 27, 981 | 0, 982 | 13, 983 | 4, 984 | "LATENT" 985 | ], 986 | [ 987 | 126, 988 | 12, 989 | 0, 990 | 22, 991 | 0, 992 | "MODEL" 993 | ], 994 | [ 995 | 127, 996 | 30, 997 | 0, 998 | 17, 999 | 0, 1000 | "MODEL" 1001 | ], 1002 | [ 1003 | 164, 1004 | 38, 1005 | 0, 1006 | 55, 1007 | 0, 1008 | "CLIP_VISION" 1009 | ], 1010 | [ 1011 | 165, 1012 | 40, 1013 | 0, 1014 | 55, 1015 | 1, 1016 | "IMAGE" 1017 | ], 1018 | [ 1019 | 166, 1020 | 55, 1021 | 0, 1022 | 54, 1023 | 2, 1024 | "CLIP_VISION_OUTPUT" 1025 | ], 1026 | [ 1027 | 167, 1028 | 42, 1029 | 0, 1030 | 54, 1031 | 1, 1032 | "STYLE_MODEL" 1033 | ], 1034 | [ 1035 | 168, 1036 | 26, 1037 | 0, 1038 | 54, 1039 | 0, 1040 | "CONDITIONING" 1041 | ], 1042 | [ 1043 | 169, 1044 | 54, 1045 | 0, 1046 | 22, 1047 | 1, 1048 | "CONDITIONING" 1049 | ] 1050 | ], 1051 | "groups": [ 1052 | { 1053 | "id": 1, 1054 | "title": "Redux Model", 1055 | "bounding": [ 1056 | 50, 1057 | -483.6000061035156, 1058 | 1040, 1059 | 507.6000061035156 1060 | ], 1061 | "color": "#3f789e", 1062 | "font_size": 24, 1063 | "flags": {} 1064 | } 1065 | ], 1066 | "config": {}, 1067 | "extra": { 1068 | "ds": { 1069 | "scale": 0.6830134553650711, 1070 | "offset": [ 1071 | 396.86012157918003, 1072 | 443.05618462505464 1073 | ] 1074 | }, 1075 | "groupNodes": {} 1076 | }, 1077 | "version": 0.4 1078 | } --------------------------------------------------------------------------------