Author SHA1 Message Date
POM ac1ea3a682 Update SteerableMotion.py 2025-10-03 21:34:07 +02:00
POM 3c28833342 It 2025-06-27 16:56:26 +01:00
POM 9dc2753916 Update README.md: Split usage section into Wan and Animatediff approaches 2025-06-27 16:52:15 +01:00
POM cf7c6aaafd Rename sequence option strings and update logic accordingly 2025-06-23 16:12:56 +02:00
POM e0ce6ad576 Simplify VideoContinuationGenerator: Improve parameter naming and remove complex replacement logic
- Rename parameters for maximum clarity:
  • when_to_start_control_images → how_to_use_control_images
  • when_to_start_inpaint_masks → how_to_use_inpaint_masks
- Update option names to clearly indicate input frame prioritization:
  • beginning_of_generation → start_sequence_at_beginning_but_prioritise_input_frames
  • after_overlap_frames → start_sequence_after_overlap_frames_prioritise_input_frames
- Remove instead_of_input_frames option and all complex replacement logic
- Simplify to two clear options that always prioritize input overlap frames
- Improve tooltips to clearly explain prioritization behavior
- Focus on user-friendly workflows that preserve input video structure
2025-06-23 16:06:38 +02:00
POM 76264adfc7 Enhance VideoContinuationGenerator: Add instead_of_input_frames option and improve parameter naming
- Add 'instead_of_input_frames' option to both when_to_start_control_images and when_to_start_inpaint_masks
- Rename parameters for clarity: when_to_start_control_frames → when_to_start_control_images, when_to_start_masks → when_to_start_inpaint_masks
- Enable full control replacement: control images can now replace overlap frames + middle frames + end frame
- Improve inpaint mask logic: when_to_start_inpaint_masks now affects custom masks properly
- Add comprehensive logic for insufficient control images with graceful fallbacks
- Maintain backward compatibility while adding powerful new functionality
2025-06-23 15:58:54 +02:00
POM 8e2a9a8130 Fix inverted logic and improve tooltips for VideoContinuationGenerator control frame and mask parameters 2025-06-23 11:47:58 +02:00
POM 12bdbfbc68 Refactor: Correct and rename VideoContinuationGenerator start logic
- Rename 'beginning' to 'beginning_of_generation' for clarity
- Rename 'after overlap_frames' to 'after_overlap_frames'
- Swap underlying logic to match intuitive naming
- Set default to 'beginning_of_generation' for both parameters
- Update tooltips to accurately reflect the new behavior
2025-06-23 11:34:47 +02:00
POM 08cd413f0b Refactor VideoContinuationGenerator for clarity
- Replaces confusing 'when_to_start' parameters with a single 'continuation_mode' dropdown.
- New modes ('Generate new content', 'Stitch to existing sequence') are goal-oriented for better UX.
- 'Generate new content' (default) uses control frames from C0 and masks the middle for inpainting.
- 'Stitch to existing sequence' skips overlapping control frames and treats them as known areas.
- This change makes the node's behavior more intuitive and easier to understand.
2025-06-23 11:27:08 +02:00
POM cfd1d8b256 Add when_to_start_masks parameter to VideoContinuationGenerator
- Adds dropdown with 'beginning' and 'after overlap_frames' options
- Controls when mask generation begins in the continuation video
- 'beginning' mode: masks start from frame 0 (standard behavior)
- 'after overlap_frames' mode: masks follow control frame timing logic
- Provides consistent mask/control frame alignment for better results
2025-06-23 11:11:07 +02:00
POM 9ed5f0b528 Add when_to_start_control_frames parameter to VideoContinuationGenerator
- Adds dropdown with 'beginning' and 'after overlap_frames' options
- Controls when control frames become active in the continuation video
- 'beginning' mode starts from first control frame regardless of overlap
- 'after overlap_frames' mode skips overlap count to avoid duplication
- Includes proper logic and tooltip explanation
2025-06-23 11:05:39 +02:00
POM a73c7b9759 Update SteerableMotion.py 2025-06-22 22:41:28 +02:00
POM b95e9eed09 Add files via upload 2024-06-16 01:01:54 +02:00
POM d747e46b21 Update README.md 2024-05-30 06:45:03 -07:00
POM 122f9778c6 Add files via upload 2024-05-30 15:44:22 +02:00
POM 53bfbd85c7 Delete demo/creative_interpolation_example.json 2024-05-30 06:43:14 -07:00
POM 13f6a58723 Delete demo/steerable-motion_liquidy_loop.json 2024-05-30 06:43:06 -07:00
POM f9250d62ee Add files via upload 2024-05-30 15:42:47 +02:00
POM 46742f3e4e Merge pull request #76 from banodoco/feature/interpolate
Fix SparseCtrl Issue
2024-05-30 13:49:53 +02:00
peteromallet a93622a7e8 Fix SparseCtrl Issue 2024-05-30 04:29:28 -07:00
POM e9c92e78c5 Add files via upload 2024-05-27 01:30:09 +02:00
POM ab32b862c6 Add files via upload 2024-05-27 01:25:31 +02:00
POM 1e796ed917 Delete demo/steerable-moton_chocky-realistiche.json 2024-05-27 01:21:23 +02:00
POM ae350a6e7f Delete demo/steerable-motion_liquity_loop.json 2024-05-27 01:21:15 +02:00
POM a7050f41dd Add files via upload 2024-05-27 01:20:56 +02:00
POM 1311a0859d Add files via upload 2024-05-27 01:17:00 +02:00
POM 09e5797b0a Update README.md 2024-05-27 01:16:21 +02:00
POM e3c1031117 Add files via upload 2024-05-25 23:46:27 +02:00
POM 8339ca5368 Merge pull request #75 from banodoco/feature/interpolate
Feature/interpolate
2024-05-25 23:43:55 +02:00
peteromallet 22559e6482 Change name 2024-05-25 21:39:07 +02:00
peteromallet 34d751db9d Renaming 2024-05-25 21:31:44 +02:00
peteromallet b9f4bf6730 Fix import 2024-05-25 21:24:16 +02:00
peteromallet 8370a2f1ef FIxing classes 2024-05-25 20:05:19 +02:00
peteromallet d756dcf1e1 Film for fixing frames 2024-05-25 18:46:18 +02:00
peteromallet 563e18f71e Drop frames 2024-05-25 14:09:59 +02:00
POM f5203e1d1d Add files via upload 2024-05-25 14:07:00 +02:00
POM 70d6558e23 Add files via upload 2024-05-16 20:13:43 +02:00
POM 8bc082c9d4 Add files via upload 2024-05-16 00:50:41 +02:00
POM 18ea974485 Merge pull request #68 from IDGallagher/fix/new-ipadapter
Updated to new IPAdapter + fixes
2024-05-13 19:12:37 +02:00
IDGallagher 2a4768b32f Updated to new IPAdapter + fixes 2024-05-13 17:22:15 +01:00
POM 5e207a8c7c Add files via upload 2024-05-11 16:15:54 +02:00
POM eb364489f5 Add files via upload 2024-05-09 11:42:02 +02:00
POM 450853abb8 Add files via upload 2024-05-05 11:22:16 +02:00
POM 28e6870c83 Add files via upload 2024-05-05 02:11:59 +02:00
POM 0b6511d02f Add files via upload 2024-05-03 22:42:40 +02:00
POM c0308e0dfd Add files via upload 2024-05-02 13:33:30 +02:00
POM 859b10921a Merge pull request #64 from richinsley/main
set matplotlib to Agg to prevent crash on macOS
2024-04-29 20:50:21 +02:00
Rich Insley 41205e798b set matplotlib to Agg to prevent crash on macOS 2024-04-29 09:49:28 -07:00
POM 6c7f4b3527 Add files via upload 2024-04-29 03:24:21 +02:00
POM e2f8620f19 Rename SuperBeasts-POM-SmoothBatchCreative-V1.3.1 (1).json to SuperBeasts-POM-SmoothBatchCreative-V1.3.1.json 2024-04-29 02:37:53 +02:00
POM bfbfef3cc3 Delete demo/SuperBeasts-POM-SmoothBatchCreative-V1.3.1.json 2024-04-29 02:37:38 +02:00
POM 80c4dc32e9 Add files via upload 2024-04-29 02:36:57 +02:00
POM dc84026f35 Update README.md 2024-04-29 02:11:05 +02:00
POM c50ff9ce90 Delete demo/SuperBeasts-POM-SmoothBatchCreative-V1.3.json 2024-04-29 02:09:38 +02:00
POM 567d8f7e3e Add files via upload 2024-04-29 02:09:05 +02:00
POM bc43177c56 Update README.md 2024-04-29 01:35:44 +02:00
POM 61844af6d3 Update README.md 2024-04-29 00:45:51 +02:00
peteromallet 1d97e4cdef 1.4 2024-04-29 00:38:08 +02:00
POM 691f31e74c Merge pull request #63 from IDGallagher/bug_fixes
Bug fixes
2024-04-28 23:26:01 +02:00
IDGallagher e277e2e86f Stop base ipa_weight being applied twice. Its now applied when creating the IPA 2024-04-28 22:17:07 +01:00
IDGallagher 199b1cc17b Fix error when ipa_weight is set to 0 on base 2024-04-28 22:11:52 +01:00
IDGallagher 1f6aecba32 Fix error when an IPAdapter has already been added to the model 2024-04-28 21:54:33 +01:00
POM 0da68f7e2d Add files via upload 2024-04-28 21:01:45 +02:00
POM a68fe98a6f Merge pull request #62 from IDGallagher/feature/memory_optimization
Feature/memory optimization
2024-04-26 19:36:31 +02:00
POM 4efaa8cddd Add files via upload 2024-04-25 22:12:20 +01:00
IDGallagher e741e8ee63 Remove print statements 2024-04-25 19:48:48 +01:00
IDGallagher 35f14ac5be Merge branch 'main' into feature/memory_optimization
# Conflicts:
#	SteerableMotion.py
2024-04-25 12:24:38 +01:00
IDGallagher dfe4d11197 Comments 2024-04-25 12:20:11 +01:00
POM f56fbe8b8f Add files via upload 2024-04-25 10:33:03 +01:00
IDGallagher 6da0c3940e Noise and detail fixes 2024-04-24 18:26:36 +01:00
IDGallagher 8d29e7f33c Batching working for detailed images 2024-04-24 15:16:22 +01:00
IDGallagher ffd8d74064 Batching working for non-detailed ipadapter 2024-04-24 09:46:59 +01:00
IDGallagher f376266873 . 2024-04-23 11:29:22 +01:00
IDGallagher f4000bf8d7 Updated IPAdapter 2024-04-19 21:19:59 +01:00
peteromallet ec187b754c Fix 2024-04-17 21:27:26 +02:00
POM c0be5dcf90 Add files via upload 2024-04-17 16:38:28 +02:00
POM f24af2424b Merge pull request #57 from IDGallagher/feature/scheduled-weights
Feature/scheduled weights
2024-04-17 16:21:57 +02:00
IDGallagher 05ee88b077 Merge branch 'feature/scheduled-weights' 2024-04-17 11:39:23 +01:00
IDGallagher 0e342bcf3a Switch to batched IPAdapter and create weights in exactly same way that masks were 2024-04-17 10:46:23 +01:00
peteromallet aa97900f7e Fix 2024-04-15 19:41:39 +02:00
IDGallagher 574c737dc1 Import working 2024-04-15 18:03:18 +01:00
IDGallagher 8e3d0c8169 Imported latest IPAdapter code 2024-04-15 17:48:00 +01:00
peteromallet 55d6016d96 Updating README 2024-04-08 19:55:54 +02:00
peteromallet b358ade073 Fixing what I done broke 2024-04-08 19:41:18 +02:00
peteromallet a6ff35af46 Welcome to the new world 2024-04-08 02:28:20 +02:00
peteromallet 717e291247 IPA Tile 2024-03-30 04:05:53 +01:00
peteromallet 99ff0d3c62 IPA Tile 2024-03-30 04:04:56 +01:00
peteromallet 259b38b755 Bug fixes 2024-03-28 01:50:02 +01:00
peteromallet 7e203779d6 Fix 2024-03-28 00:12:36 +01:00
peteromallet c807b18b9a Start work on tile IPA feature 2024-03-27 23:31:50 +01:00
POM bb16d05c44 Add files via upload 2024-03-26 15:37:35 +01:00
POM 85951f7edd Add files via upload 2024-03-22 01:03:14 +01:00
POM e4e319f07a Add files via upload 2024-03-21 23:44:40 +01:00
POM 25c596cf72 Delete demo/creative_interpolation_example_lcm.json.json 2024-03-21 23:44:34 +01:00
POM c71faf118d Add files via upload 2024-03-21 23:38:19 +01:00
POM 9109899dac Add files via upload 2024-03-13 05:38:01 -07:00
peteromallet 5294a2e8fd LICENCE 2024-03-01 17:00:35 +01:00
POM f0bca7a3f6 Update creative_interpolation_example.json 2024-02-26 11:11:29 +01:00
POM c72b42287d Add files via upload 2024-02-19 18:12:36 +01:00
peteromallet 748d96080d Fixing collosally stupid problem 2024-02-17 01:19:39 +01:00
peteromallet 8bd1ed3bc1 Fix 2024-02-16 21:58:14 +01:00
peteromallet 5579e78cd3 Adding ending buffer 2024-02-16 01:28:27 +01:00
peteromallet d0c7a3e268 Adding ending buffer 2024-02-16 01:23:14 +01:00
peteromallet f688b4fa26 Fixes 2024-02-13 23:34:28 +01:00
peteromallet 74c326a9a2 Update workflow 2024-02-07 16:33:10 +01:00
peteromallet a03dca58bc Update workflow 2024-02-07 16:32:32 +01:00
peteromallet ecb102a4c4 Update workflow 2024-02-07 12:14:41 +01:00
peteromallet e5d8f922a8 Push new workflow 2024-02-07 12:11:43 +01:00
peteromallet 7dd4c79f04 Push new workflow 2024-02-07 12:07:24 +01:00
POM 010de35145 Add files via upload 2024-01-29 19:47:57 +01:00
POM 9e3748b6f4 Add files via upload 2024-01-29 10:42:08 +01:00
peter942 45763752e6 Fix 2024-01-26 10:00:54 +01:00
peter942 3aff9359ee Smoooooother 2024-01-25 22:23:13 +01:00
peter942 b08c641c56 Hacky fix 2024-01-23 22:00:29 +01:00
peter942 1dfe28b0bc Small fixes 2024-01-23 11:55:45 +01:00
peter942 1e21f2388a 1.2 2024-01-22 22:45:36 +01:00
peter942 5d71e61efd 1.2 2024-01-22 21:54:22 +01:00
peter942 6582d8ede2 New workflow 2024-01-13 01:57:21 +01:00
peter942 6154baaa42 Fixes and improvements 2024-01-13 01:36:09 +01:00
peter942 909566d968 1.1 2024-01-10 02:21:38 +01:00
POM 1f046a5e15 Add files via upload 2023-12-15 23:28:21 +01:00
POM 6ce7fd98b0 Add files via upload 2023-12-15 23:17:01 +01:00
POM 103ee8fbac Add files via upload 2023-12-14 15:46:21 +01:00
POM e3fb0d45e7 Delete demo/input_example.png 2023-12-14 01:57:08 +01:00
POM 051da25a8a Delete demo/1.1.gif 2023-12-14 01:56:56 +01:00
POM 4cad6a14b4 Delete demo/0.8.gif 2023-12-14 01:56:45 +01:00
POM de00c6e65c Add files via upload 2023-12-14 01:56:22 +01:00
POM f69025171f Update README.md 2023-12-14 01:54:24 +01:00
86 changed files with 64717 additions and 3842 deletions
+43
View File
@@ -0,0 +1,43 @@
Open Source Native License (OSNL)
Version 0.1 - March 1, 2024
Preamble
The Open Source Native License (OSNL) is designed to ensure that software remains free and open, fostering innovation and knowledge sharing within the community. It grants individuals, researchers, and commercial entities who open source their primary business assets, the freedom to use the software in any manner they choose.
This distinctive approach aims to balance the benefits of open-source development with the realities of commercial enterprise. It ensures that software remains a shared, community-driven resource while enabling businesses to thrive in an open-source ecosystem. Additional licenses are available for non-open source commercial entities.
1. Definitions
- "This License" refers to Version 1.0 of the Open Source Native License.
- "The Program" refers to the software distributed under this License.
- "You" refers to the individual or entity utilizing or contributing to the Program.
- "Primary Business Assets" are the core resources, capabilities, and technology that constitute the main value proposition and operational basis of your business.
2. Grant of License
Subject to the terms and conditions of this License, you are hereby granted a free, perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license to use, reproduce, modify, distribute, and sublicense the Program, provided you comply with the following condition:
- Individual or researcher: You are granted the rights to use, modify, distribute, and contribute to the Program for any purpose, including educational, research, and personal projects, without the necessity to make your personal projects open source, provided these activities do not constitute a commercial enterprise. For any use that transitions to commercial purposes, the conditions applicable to commercial entities as outlined in this License will then apply.
- Commercial Entity who meets open source condition: Your primary business assets, including all core technologies, software, and platforms, must be available under an OSI-approved open source license or OSNL. This condition does not apply to ancillary or peripheral services not constituting primary business assets.
2.1 Commercial Use by Non-Open Source Businesses
Non-open source businesses that wish to utilize the Program or its derivatives as a component of their products or services are required to obtain an additional license. These entities must proactively contact Banodoco to request such a license. Banodoco reserves the right, at its own discretion, to grant or deny this additional license. Until an additional license is granted by Banodoco, non-open source businesses are not authorized to exercise any rights provided under this License regarding the use of the Program or its derivatives.
3. Redistribution
You may reproduce and distribute copies of the Program or derivative works thereof in any medium, with or without modifications, provided that you meet the following conditions:
- You must give any recipients of the Program a copy of this License.
- You must ensure that any modified files carry prominent notices stating that you changed the files.
- You must disclose the source of the Program, and if you distribute any portion of it in a compiled or object code form, you must also provide the full source code under this License.
- Any distribution of the Program or derivative works must comply with the Primary Business Open Source Condition.
4. Disclaimer of Warranty
THE PROGRAM IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF THE PROGRAM.
5. General
This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Program.
+43 -48
View File
@@ -1,67 +1,62 @@
# Steerable Motion - ComfyUI node for creative interpolation and other methods for controlling Animatediff (Beta)
# Steerable Motion, ComfyUI custom nodes & workflows node for steering videos with batches of images
This a ComfyUI node for batch creative interpolation. The goal is to allow you to input a batch of images, and to provide a range of simple settings to control how the images are interpolated between.
Steerable Motion is a set of ComfyUI nodes and workflows for travelling between images.
## Installation
## Installation in Comfy
1. If you haven't already, download [ComfyUI](https://github.com/comfyanonymous/ComfyUI) and [Comfy Manager](https://github.com/ltdrdata/ComfyUI-Manager).
1. go to your custom_nodes folder and run: git clone https://github.com/peteromallet/ComfyUI-Creative-Interpolation.git
2. Download Controlnet tile from Comfy Manager: control_v11f1e_sd15_tile_fp16.safetensors
1. If you haven't already, install [ComfyUI](https://github.com/comfyanonymous/ComfyUI) and [Comfy Manager](https://github.com/ltdrdata/ComfyUI-Manager) - you can find instructions on their pages.
2. When the workflow opens, download the dependent nodes by pressing "Install Missing Custom Nodes" in Comfy Manager. Search and download the required models from Comfy Manager also.
## Usage
## Wan
Here's a workflow to get started with: https://raw.githubusercontent.com/peteromallet/ComfyUI-Creative-Interpolation/main/demo/creative_interpolation_example.json
The Wan approach uses VACE to create anchor images and continuations from previous images, which are chained together at the end:
You'll need to drop the input images into the 'creative_interpolation_input' folder in numerical order - 0.png, 1.png, etc.
Key frames are **distributed either linearly of dynamically**.
If you set type_of_frame_distribution to linear, you need to set linear_frames_per_keyframe to the gap you wish to have between each key frame - e.g. 16 would mean the frames are at 0, 16, 32, 48, etc.
Alternatively, if you set it to dynamic, you can input the positions of the key frames in the text box below this.
Other than this, please experiment with the different settings to achieve your desired effect:
The main settings are:
- frames_per_key_frame: How many frames to generate between each main key frame you provide.
- length_of_key_frame_influence: How many frames to apply the ControlNet for after each key frame - the larger the number, the the wider range the input images will influence.
- cn_strength: How strong the control of the ControlNet should overall.
- buffer: the number of buffer frames places before your video - this is to prevent the end of the generation influencing the beginning due to how the context scheduler works.
The **batch_size should be equal to the frames_per_key_frame * number_of_key_frames + 4** - the additional 4 is to accommodate for buffer frames that improve consistency.
Also, **there's currently a bug where it needs to be restarted after each batch**. This will be fixed soon.
As an example, here's a batch of input images:
![Batch of input images](https://github.com/peteromallet/ComfyUI-Creative-Interpolation/blob/main/demo/input_example.png)
And here's what it looks like when the length_of_key_frame_influence is set to 1.1:
![Main example](demo/wan_example.gif)
![1.1 Interpolation](https://github.com/peteromallet/ComfyUI-Creative-Interpolation/blob/main/demo/1.1.gif)
### Sample workflow for Wan
You can find a workflow [here](demo/Vace_Travel.json) to get started.
## Animatediff
The Animatediff approach uses a combination of IP-Adapter and SparseCtrl to travel between images:
![Main example](demo/main_example.gif)
While here's while that looks like at 0.8:
### 5 basic workflows for Animatediff
![0.8 Interpolation](https://github.com/peteromallet/ComfyUI-Creative-Interpolation/blob/main/demo/0.8.gif)
Below are 5 basic workflows - each with their own weird and unique characteristics - all with differing levels of adherence and different types of motion - most of the changes come from tweaking the IPA configuration and switching out base models:
These settings can be so powerful I believe - please share what works for you and your results!
- [Smooth n' Steady](https://raw.githubusercontent.com/banodoco/steerable-motion/main/demo/steerable-motion_smooth-n-steady.json): tends to have nice smooth motion - good starting point
- [Rad Attack](https://raw.githubusercontent.com/banodoco/steerable-motion/main/demo/steerable-motion_rad-attack.json): probably the best for realistic motion
- [Slurshy Realistiche](https://raw.githubusercontent.com/banodoco/steerable-motion/main/demo/steerable-motion_slurshy-realistiche.json): moves in a slightly realistic manner but is a little bit slurshy
- [Chocky Realistiche](https://raw.githubusercontent.com/banodoco/steerable-motion/main/demo/steerable-motion_chocky-realistiche.json): realistic-ish but very blocky
- [Liquidy Loop](https://raw.githubusercontent.com/banodoco/steerable-motion/main/demo/steerable-motion_liquidy-loop.json): smooth and liquidy
## Coming Soon
You can see each in acton below:
- Fix restarting bug
- Clean up code
- Simplify settings
- Nuanced settings for each key frame
- Improvements to structure and consistency
- Better control over style
![basic workflows](demo/basic_workflows.gif)
## Want to give feedback, share creations, or join our community?
## Philosophy for getting the most from these
You can drop into our Discord here: https://discord.com/invite/8Wx9dFu5tP
This isn't an approach like text to video that will perform well out of the box, it's more like a paint brush - an artistic tool that you need to figure out how to get the best from.
Through trial and error, you'll need to build an understanding of how the motion and settings work, what its limitations are, which inputs images work best with it, etc.
It won't work for everything but if you can figure out how to wield it, this approach can provide enough control for you to make beautiful things that match your imagination precisely.
In both cases, tweaking the settings can greatly influence the motion - for example, below you can see two examples of the same images animated - but with the one setting tweaked, the length of each frame's influence:
![Tweaking settings example](demo/tweaking_settings.gif)
## Want to give feedback, or join a community who are pushing open source models to their artistic and technical limits?
You're very welcome to drop into our Discord [here](https://discord.com/invite/8Wx9dFu5tP).
## Credits
This code draws heavily from [Kosinkadink's ComfyUI-Advanced-ControlNet](https://github.com/Kosinkadink/ComfyUI-Advanced-ControlNet) and Cubiq's [IPAdapter_plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus), while the workflows uses [Kosinkadink's Animatediff Evolved](https://github.com/Kosinkadink/ComfyUI-AnimateDiff-Evolved), Fizzledorf's [Fizznodes](https://github.com/FizzleDorf/ComfyUI_FizzNodes), Fannovel16's [Frame Interpolation](https://github.com/Fannovel16/ComfyUI-Frame-Interpolation) and more. Thanks to all and of course the Animatediff team, Controlnet, others, and of course our supportive community!
For Animatediff, the code draws heavily from Cubiq's [IPAdapter_plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus), while the workflow uses Kosinkadink's [Animatediff Evolved](https://github.com/Kosinkadink/ComfyUI-AnimateDiff-Evolved) and [ComfyUI-Advanced-ControlNet](https://github.com/Kosinkadink/ComfyUI-Advanced-ControlNet), Fizzledorf's [Fizznodes](https://github.com/FizzleDorf/ComfyUI_FizzNodes), Fannovel16's [Frame Interpolation](https://github.com/Fannovel16/ComfyUI-Frame-Interpolation) and more.
For Wan, it's built on top of the work of Kijai's wonderful [ComfyUI-WanVideoWrapper](https://github.com/kijai/ComfyUI-WanVideoWrapper) and of course the VACE and Wan teams.
+935 -235
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 MiB

After

Width:  |  Height:  |  Size: 16 MiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 888 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 MiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 MiB

After

Width:  |  Height:  |  Size: 21 MiB

-773
View File
@@ -1,773 +0,0 @@
from typing import Union
from torch import Tensor
import torch
import comfy.utils
import comfy.controlnet as comfy_cn
from comfy.controlnet import ControlBase, ControlNet, ControlLora, T2IAdapter, broadcast_image_to
def get_properly_arranged_t2i_weights(initial_weights: list[float]):
new_weights = []
new_weights.extend([initial_weights[0]]*3)
new_weights.extend([initial_weights[1]]*3)
new_weights.extend([initial_weights[2]]*3)
new_weights.extend([initial_weights[3]]*3)
return new_weights
class ControlWeightTypeImport:
DEFAULT = "default"
UNIVERSAL = "universal"
T2IADAPTER = "t2iadapter"
CONTROLNET = "controlnet"
CONTROLLORA = "controllora"
CONTROLLLLITE = "controllllite"
class ControlWeightsImport:
def __init__(self, weight_type: str, base_multiplier: float=1.0, flip_weights: bool=False, weights: list[float]=None, weight_mask: Tensor=None):
self.weight_type = weight_type
self.base_multiplier = base_multiplier
self.flip_weights = flip_weights
self.weights = weights
if self.weights is not None and self.flip_weights:
self.weights.reverse()
self.weight_mask = weight_mask
def get(self, idx: int) -> Union[float, Tensor]:
# if weights is not none, return index
if self.weights is not None:
return self.weights[idx]
return 1.0
@classmethod
def default(cls):
return cls(ControlWeightTypeImport.DEFAULT)
@classmethod
def universal(cls, base_multiplier: float, flip_weights: bool=False):
return cls(ControlWeightTypeImport.UNIVERSAL, base_multiplier=base_multiplier, flip_weights=flip_weights)
@classmethod
def universal_mask(cls, weight_mask: Tensor):
return cls(ControlWeightTypeImport.UNIVERSAL, weight_mask=weight_mask)
@classmethod
def t2iadapter(cls, weights: list[float]=None, flip_weights: bool=False):
if weights is None:
weights = [1.0]*12
return cls(ControlWeightTypeImport.T2IADAPTER, weights=weights,flip_weights=flip_weights)
@classmethod
def controlnet(cls, weights: list[float]=None, flip_weights: bool=False):
if weights is None:
weights = [1.0]*13
return cls(ControlWeightTypeImport.CONTROLNET, weights=weights, flip_weights=flip_weights)
@classmethod
def controllora(cls, weights: list[float]=None, flip_weights: bool=False):
if weights is None:
weights = [1.0]*10
return cls(ControlWeightTypeImport.CONTROLLORA, weights=weights, flip_weights=flip_weights)
@classmethod
def controllllite(cls, weights: list[float]=None, flip_weights: bool=False):
if weights is None:
# TODO: make this have a real value
weights = [1.0]*200
return cls(ControlWeightTypeImport.CONTROLLLLITE, weights=weights, flip_weights=flip_weights)
class StrengthInterpolationImport:
LINEAR = "linear"
EASE_IN = "ease-in"
EASE_OUT = "ease-out"
EASE_IN_OUT = "ease-in-out"
NONE = "none"
class LatentKeyframeImport:
def __init__(self, batch_index: int, strength: float) -> None:
self.batch_index = batch_index
self.strength = strength
# always maintain sorted state (by batch_index of LatentKeyframe)
class LatentKeyframeGroupImport:
def __init__(self) -> None:
self.keyframes: list[LatentKeyframeImport] = []
def add(self, keyframe: LatentKeyframeImport) -> None:
added = False
# replace existing keyframe if same batch_index
for i in range(len(self.keyframes)):
if self.keyframes[i].batch_index == keyframe.batch_index:
self.keyframes[i] = keyframe
added = True
break
if not added:
self.keyframes.append(keyframe)
self.keyframes.sort(key=lambda k: k.batch_index)
def get_index(self, index: int) -> Union[LatentKeyframeImport, None]:
try:
return self.keyframes[index]
except IndexError:
return None
def __getitem__(self, index) -> LatentKeyframeImport:
return self.keyframes[index]
def is_empty(self) -> bool:
return len(self.keyframes) == 0
def clone(self) -> 'LatentKeyframeGroupImport':
cloned = LatentKeyframeGroupImport()
for tk in self.keyframes:
cloned.add(tk)
return cloned
class TimestepKeyframeImport:
def __init__(self,
start_percent: float = 0.0,
strength: float = 1.0,
interpolation: str = StrengthInterpolationImport.NONE,
control_weights: ControlWeightsImport = None,
latent_keyframes: LatentKeyframeGroupImport = None,
null_latent_kf_strength: float = 0.0,
inherit_missing: bool = True,
guarantee_usage: bool = True,
mask_hint_orig: Tensor = None) -> None:
self.start_percent = start_percent
self.start_t = 999999999.9
self.strength = strength
self.interpolation = interpolation
self.control_weights = control_weights
self.latent_keyframes = latent_keyframes
self.null_latent_kf_strength = null_latent_kf_strength
self.inherit_missing = inherit_missing
self.guarantee_usage = guarantee_usage
self.mask_hint_orig = mask_hint_orig
def has_control_weights(self):
return self.control_weights is not None
def has_latent_keyframes(self):
return self.latent_keyframes is not None
def has_mask_hint(self):
return self.mask_hint_orig is not None
@classmethod
def default(cls) -> 'TimestepKeyframeImport':
return cls(0.0)
# always maintain sorted state (by start_percent of TimestepKeyFrame)
class TimestepKeyframeGroupImport:
def __init__(self) -> None:
self.keyframes: list[TimestepKeyframeImport] = []
self.keyframes.append(TimestepKeyframeImport.default())
def add(self, keyframe: TimestepKeyframeImport) -> None:
added = False
# replace existing keyframe if same start_percent
for i in range(len(self.keyframes)):
if self.keyframes[i].start_percent == keyframe.start_percent:
self.keyframes[i] = keyframe
added = True
break
if not added:
self.keyframes.append(keyframe)
self.keyframes.sort(key=lambda k: k.start_percent)
def get_index(self, index: int) -> Union[TimestepKeyframeImport, None]:
try:
return self.keyframes[index]
except IndexError:
return None
def has_index(self, index: int) -> int:
return index >=0 and index < len(self.keyframes)
def __getitem__(self, index) -> TimestepKeyframeImport:
return self.keyframes[index]
def __len__(self) -> int:
return len(self.keyframes)
def is_empty(self) -> bool:
return len(self.keyframes) == 0
def clone(self) -> 'TimestepKeyframeGroupImport':
cloned = TimestepKeyframeGroupImport()
for tk in self.keyframes:
cloned.add(tk)
return cloned
@classmethod
def default(cls, keyframe: TimestepKeyframeImport) -> 'TimestepKeyframeGroupImport':
group = cls()
group.keyframes[0] = keyframe
return group
# used to inject ControlNetAdvancedImport and T2IAdapterAdvancedImport control_merge function
class AdvancedControlBaseImport:
def __init__(self, base: ControlBase, timestep_keyframes: TimestepKeyframeGroupImport, weights_default: ControlWeightsImport):
self.base = base
self.compatible_weights = [ControlWeightTypeImport.UNIVERSAL]
self.add_compatible_weight(weights_default.weight_type)
# mask for which parts of controlnet output to keep
self.mask_cond_hint_original = None
self.mask_cond_hint = None
self.tk_mask_cond_hint_original = None
self.tk_mask_cond_hint = None
self.weight_mask_cond_hint = None
# actual index values
self.sub_idxs = None
self.full_latent_length = 0
self.context_length = 0
# timesteps
self.t: Tensor = None
self.batched_number: int = None
# weights + override
self.weights: ControlWeightsImport = None
self.weights_default: ControlWeightsImport = weights_default
self.weights_override: ControlWeightsImport = None
# latent keyframe + override
self.latent_keyframes: LatentKeyframeGroupImport = None
self.latent_keyframe_override: LatentKeyframeGroupImport = None
# initialize timestep_keyframes
self.set_timestep_keyframes(timestep_keyframes)
# override some functions
self.get_control = self.get_control_inject
self.control_merge = self.control_merge_inject#.__get__(self, type(self))
self.pre_run = self.pre_run_inject
self.cleanup = self.cleanup_inject
def add_compatible_weight(self, control_weight_type: str):
self.compatible_weights.append(control_weight_type)
def verify_all_weights(self, throw_error=True):
# first, check if override exists - if so, only need to check the override
if self.weights_override is not None:
if self.weights_override.weight_type not in self.compatible_weights:
msg = f"Weight override is type {self.weights_override.weight_type}, but loaded {type(self).__name__}" + \
f"only supports {self.compatible_weights} weights."
raise WeightTypeExceptionImport(msg)
# otherwise, check all timestep keyframe weights
else:
for tk in self.timestep_keyframes.keyframes:
if tk.has_control_weights() and tk.control_weights.weight_type not in self.compatible_weights:
msg = f"Weight on Timestep Keyframe with start_percent={tk.start_percent} is type" + \
f"{tk.control_weights.weight_type}, but loaded {type(self).__name__} only supports {self.compatible_weights} weights."
raise WeightTypeExceptionImport(msg)
def set_timestep_keyframes(self, timestep_keyframes: TimestepKeyframeGroupImport):
self.timestep_keyframes = timestep_keyframes if timestep_keyframes else TimestepKeyframeGroupImport()
# prepare first timestep_keyframe related stuff
self.current_timestep_keyframe = None
self.current_timestep_index = -1
self.next_timestep_keyframe = None
self.weights = None
self.latent_keyframes = None
def prepare_current_timestep(self, t: Tensor, batched_number: int):
self.t = t
self.batched_number = batched_number
# get current step percent
curr_t: float = t[0]
prev_index = self.current_timestep_index
# if has next index, loop through and see if need to switch
if self.timestep_keyframes.has_index(self.current_timestep_index+1):
for i in range(self.current_timestep_index+1, len(self.timestep_keyframes)):
eval_tk = self.timestep_keyframes[i]
# check if start percent is less or equal to curr_t
if eval_tk.start_t >= curr_t:
self.current_timestep_index = i
self.current_timestep_keyframe = eval_tk
# keep track of control weights, latent keyframes, and masks,
# accounting for inherit_missing
if self.current_timestep_keyframe.has_control_weights():
self.weights = self.current_timestep_keyframe.control_weights
elif not self.current_timestep_keyframe.inherit_missing:
self.weights = self.weights_default
if self.current_timestep_keyframe.has_latent_keyframes():
self.latent_keyframes = self.current_timestep_keyframe.latent_keyframes
elif not self.current_timestep_keyframe.inherit_missing:
self.latent_keyframes = None
if self.current_timestep_keyframe.has_mask_hint():
self.tk_mask_cond_hint_original = self.current_timestep_keyframe.mask_hint_orig
elif not self.current_timestep_keyframe.inherit_missing:
del self.tk_mask_cond_hint_original
self.tk_mask_cond_hint_original = None
# if guarantee_usage, stop searching for other TKs
if self.current_timestep_keyframe.guarantee_usage:
break
# if eval_tk is outside of percent range, stop looking further
else:
break
# if index changed, apply overrides
if prev_index != self.current_timestep_index:
if self.weights_override is not None:
self.weights = self.weights_override
if self.latent_keyframe_override is not None:
self.latent_keyframes = self.latent_keyframe_override
# make sure weights and latent_keyframes are in a workable state
# Note: each AdvancedControlBaseImport should create their own get_universal_weights class
self.prepare_weights()
def prepare_weights(self):
if self.weights is None or self.weights.weight_type == ControlWeightTypeImport.DEFAULT:
self.weights = self.weights_default
elif self.weights.weight_type == ControlWeightTypeImport.UNIVERSAL:
# if universal and weight_mask present, no need to convert
if self.weights.weight_mask is not None:
return
self.weights = self.get_universal_weights()
def get_universal_weights(self) -> ControlWeightsImport:
return self.weights
def set_cond_hint_mask(self, mask_hint):
self.mask_cond_hint_original = mask_hint
return self
def pre_run_inject(self, model, percent_to_timestep_function):
self.base.pre_run(model, percent_to_timestep_function)
self.pre_run_advanced(model, percent_to_timestep_function)
def pre_run_advanced(self, model, percent_to_timestep_function):
# for each timestep keyframe, calculate the start_t
for tk in self.timestep_keyframes.keyframes:
tk.start_t = percent_to_timestep_function(tk.start_percent)
# clear variables
self.cleanup_advanced()
def get_control_inject(self, x_noisy, t, cond, batched_number):
# prepare timestep and everything related
self.prepare_current_timestep(t=t, batched_number=batched_number)
# if should not perform any actions for the controlnet, exit without doing any work
if self.strength == 0.0 or self.current_timestep_keyframe.strength == 0.0:
control_prev = None
if self.previous_controlnet is not None:
control_prev = self.previous_controlnet.get_control(x_noisy, t, cond, batched_number)
if control_prev is not None:
return control_prev
else:
return None
# otherwise, perform normal function
return self.get_control_advanced(x_noisy, t, cond, batched_number)
def get_control_advanced(self, x_noisy, t, cond, batched_number):
pass
def calc_weight(self, idx: int, x: Tensor, layers: int) -> Union[float, Tensor]:
if self.weights.weight_mask is not None:
# prepare weight mask
self.prepare_weight_mask_cond_hint(x, self.batched_number)
# adjust mask for current layer and return
return torch.pow(self.weight_mask_cond_hint, self.get_calc_pow(idx=idx, layers=layers))
return self.weights.get(idx=idx)
def get_calc_pow(self, idx: int, layers: int) -> int:
return (layers-1)-idx
def apply_advanced_strengths_and_masks(self, x: Tensor, batched_number: int):
# apply strengths, and get batch indeces to null out
# AKA latents that should not be influenced by ControlNet
if self.latent_keyframes is not None:
latent_count = x.size(0)//batched_number
indeces_to_null = set(range(latent_count))
mapped_indeces = None
# if expecting subdivision, will need to translate between subset and actual idx values
if self.sub_idxs:
mapped_indeces = {}
for i, actual in enumerate(self.sub_idxs):
mapped_indeces[actual] = i
for keyframe in self.latent_keyframes:
real_index = keyframe.batch_index
# if negative, count from end
if real_index < 0:
real_index += latent_count if self.sub_idxs is None else self.full_latent_length
# if not mapping indeces, what you see is what you get
if mapped_indeces is None:
if real_index in indeces_to_null:
indeces_to_null.remove(real_index)
# otherwise, see if batch_index is even included in this set of latents
else:
real_index = mapped_indeces.get(real_index, None)
if real_index is None:
continue
indeces_to_null.remove(real_index)
# if real_index is outside the bounds of latents, don't apply
if real_index >= latent_count or real_index < 0:
continue
# apply strength for each batched cond/uncond
for b in range(batched_number):
x[(latent_count*b)+real_index] = x[(latent_count*b)+real_index] * keyframe.strength
# null them out by multiplying by null_latent_kf_strength
for batch_index in indeces_to_null:
# apply null for each batched cond/uncond
for b in range(batched_number):
x[(latent_count*b)+batch_index] = x[(latent_count*b)+batch_index] * self.current_timestep_keyframe.null_latent_kf_strength
# apply masks, resizing mask to required dims
if self.mask_cond_hint is not None:
masks = prepare_mask_batch(self.mask_cond_hint, x.shape)
x[:] = x[:] * masks
if self.tk_mask_cond_hint is not None:
masks = prepare_mask_batch(self.tk_mask_cond_hint, x.shape)
x[:] = x[:] * masks
# apply timestep keyframe strengths
if self.current_timestep_keyframe.strength != 1.0:
x[:] *= self.current_timestep_keyframe.strength
def control_merge_inject(self: 'AdvancedControlBaseImport', control_input, control_output, control_prev, output_dtype):
out = {'input':[], 'middle':[], 'output': []}
if control_input is not None:
for i in range(len(control_input)):
key = 'input'
x = control_input[i]
if x is not None:
self.apply_advanced_strengths_and_masks(x, self.batched_number)
x *= self.strength * self.calc_weight(i, x, len(control_input))
if x.dtype != output_dtype:
x = x.to(output_dtype)
out[key].insert(0, x)
if control_output is not None:
for i in range(len(control_output)):
if i == (len(control_output) - 1):
key = 'middle'
index = 0
else:
key = 'output'
index = i
x = control_output[i]
if x is not None:
self.apply_advanced_strengths_and_masks(x, self.batched_number)
if self.global_average_pooling:
x = torch.mean(x, dim=(2, 3), keepdim=True).repeat(1, 1, x.shape[2], x.shape[3])
x *= self.strength * self.calc_weight(i, x, len(control_output))
if x.dtype != output_dtype:
x = x.to(output_dtype)
out[key].append(x)
if control_prev is not None:
for x in ['input', 'middle', 'output']:
o = out[x]
for i in range(len(control_prev[x])):
prev_val = control_prev[x][i]
if i >= len(o):
o.append(prev_val)
elif prev_val is not None:
if o[i] is None:
o[i] = prev_val
else:
o[i] += prev_val
return out
def prepare_mask_cond_hint(self, x_noisy: Tensor, t, cond, batched_number, dtype=None):
self._prepare_mask("mask_cond_hint", self.mask_cond_hint_original, x_noisy, t, cond, batched_number, dtype)
self.prepare_tk_mask_cond_hint(x_noisy, t, cond, batched_number, dtype)
def prepare_tk_mask_cond_hint(self, x_noisy: Tensor, t, cond, batched_number, dtype=None):
return self._prepare_mask("tk_mask_cond_hint", self.current_timestep_keyframe.mask_hint_orig, x_noisy, t, cond, batched_number, dtype)
def prepare_weight_mask_cond_hint(self, x_noisy: Tensor, batched_number, dtype=None):
return self._prepare_mask("weight_mask_cond_hint", self.weights.weight_mask, x_noisy, t=None, cond=None, batched_number=batched_number, dtype=dtype, direct_attn=True)
def _prepare_mask(self, attr_name, orig_mask: Tensor, x_noisy: Tensor, t, cond, batched_number, dtype=None, direct_attn=False):
# make mask appropriate dimensions, if present
if orig_mask is not None:
out_mask = getattr(self, attr_name)
if self.sub_idxs is not None or out_mask is None or x_noisy.shape[2] * 8 != out_mask.shape[1] or x_noisy.shape[3] * 8 != out_mask.shape[2]:
self._reset_attr(attr_name)
del out_mask
# TODO: perform upscale on only the sub_idxs masks at a time instead of all to conserve RAM
# resize mask and match batch count
multiplier = 1 if direct_attn else 8
out_mask = prepare_mask_batch(orig_mask, x_noisy.shape, multiplier=multiplier)
actual_latent_length = x_noisy.shape[0] // batched_number
out_mask = comfy.utils.repeat_to_batch_size(out_mask, actual_latent_length if self.sub_idxs is None else self.full_latent_length)
if self.sub_idxs is not None:
out_mask = out_mask[self.sub_idxs]
# make cond_hint_mask length match x_noise
if x_noisy.shape[0] != out_mask.shape[0]:
out_mask = broadcast_image_to(out_mask, x_noisy.shape[0], batched_number)
# default dtype to be same as x_noisy
if dtype is None:
dtype = x_noisy.dtype
setattr(self, attr_name, out_mask.to(dtype=dtype).to(self.device))
del out_mask
def _reset_attr(self, attr_name, new_value=None):
if hasattr(self, attr_name):
delattr(self, attr_name)
setattr(self, attr_name, new_value)
def cleanup_inject(self):
self.base.cleanup()
self.cleanup_advanced()
def cleanup_advanced(self):
self.sub_idxs = None
self.full_latent_length = 0
self.context_length = 0
self.t = None
self.batched_number = None
self.weights = None
self.latent_keyframes = None
# timestep stuff
self.current_timestep_keyframe = None
self.next_timestep_keyframe = None
self.current_timestep_index = -1
# clear mask hints
if self.mask_cond_hint is not None:
del self.mask_cond_hint
self.mask_cond_hint = None
if self.tk_mask_cond_hint_original is not None:
del self.tk_mask_cond_hint_original
self.tk_mask_cond_hint_original = None
if self.tk_mask_cond_hint is not None:
del self.tk_mask_cond_hint
self.tk_mask_cond_hint = None
if self.weight_mask_cond_hint is not None:
del self.weight_mask_cond_hint
self.weight_mask_cond_hint = None
def copy_to_advanced(self, copied: 'AdvancedControlBaseImport'):
copied.mask_cond_hint_original = self.mask_cond_hint_original
copied.weights_override = self.weights_override
copied.latent_keyframe_override = self.latent_keyframe_override
class ControlNetAdvancedImport(ControlNet, AdvancedControlBaseImport):
def __init__(self, control_model, timestep_keyframes: TimestepKeyframeGroupImport, global_average_pooling=False, device=None, load_device=None, manual_cast_dtype=None):
super().__init__(control_model=control_model, global_average_pooling=global_average_pooling, device=device, load_device=load_device, manual_cast_dtype=manual_cast_dtype)
AdvancedControlBaseImport.__init__(self, super(), timestep_keyframes=timestep_keyframes, weights_default=ControlWeightsImport.controlnet())
def get_universal_weights(self) -> ControlWeightsImport:
raw_weights = [(self.weights.base_multiplier ** float(12 - i)) for i in range(13)]
return ControlWeightsImport.controlnet(raw_weights, self.weights.flip_weights)
def get_control_advanced(self, x_noisy, t, cond, batched_number):
# perform special version of get_control that supports sliding context and masks
return self.sliding_get_control(x_noisy, t, cond, batched_number)
def sliding_get_control(self, x_noisy: Tensor, t, cond, batched_number):
control_prev = None
if self.previous_controlnet is not None:
control_prev = self.previous_controlnet.get_control(x_noisy, t, cond, batched_number)
if self.timestep_range is not None:
if t[0] > self.timestep_range[0] or t[0] < self.timestep_range[1]:
if control_prev is not None:
return control_prev
else:
return None
dtype = self.control_model.dtype
if self.manual_cast_dtype is not None:
dtype = self.manual_cast_dtype
output_dtype = x_noisy.dtype
# make cond_hint appropriate dimensions
# TODO: change this to not require cond_hint upscaling every step when self.sub_idxs are present
if self.sub_idxs is not None or self.cond_hint is None or x_noisy.shape[2] * 8 != self.cond_hint.shape[2] or x_noisy.shape[3] * 8 != self.cond_hint.shape[3]:
if self.cond_hint is not None:
del self.cond_hint
self.cond_hint = None
# if self.cond_hint_original length greater or equal to real latent count, subdivide it before scaling
if self.sub_idxs is not None and self.cond_hint_original.size(0) >= self.full_latent_length:
self.cond_hint = comfy.utils.common_upscale(self.cond_hint_original[self.sub_idxs], x_noisy.shape[3] * 8, x_noisy.shape[2] * 8, 'nearest-exact', "center").to(dtype).to(self.device)
else:
self.cond_hint = comfy.utils.common_upscale(self.cond_hint_original, x_noisy.shape[3] * 8, x_noisy.shape[2] * 8, 'nearest-exact', "center").to(dtype).to(self.device)
if x_noisy.shape[0] != self.cond_hint.shape[0]:
self.cond_hint = broadcast_image_to(self.cond_hint, x_noisy.shape[0], batched_number)
# prepare mask_cond_hint
self.prepare_mask_cond_hint(x_noisy=x_noisy, t=t, cond=cond, batched_number=batched_number, dtype=dtype)
context = cond['c_crossattn']
# uses 'y' in new ComfyUI update
y = cond.get('y', None)
if y is None: # TODO: remove this in the future since no longer used by newest ComfyUI
y = cond.get('c_adm', None)
if y is not None:
y = y.to(dtype)
timestep = self.model_sampling_current.timestep(t)
x_noisy = self.model_sampling_current.calculate_input(t, x_noisy)
control = self.control_model(x=x_noisy.to(dtype), hint=self.cond_hint, timesteps=timestep.float(), context=context.to(dtype), y=y)
return self.control_merge(None, control, control_prev, output_dtype)
def copy(self):
c = ControlNetAdvancedImport(self.control_model, self.timestep_keyframes, global_average_pooling=self.global_average_pooling, load_device=self.load_device, manual_cast_dtype=self.manual_cast_dtype)
self.copy_to(c)
self.copy_to_advanced(c)
return c
@staticmethod
def from_vanilla(v: ControlNet, timestep_keyframe: TimestepKeyframeGroupImport=None) -> 'ControlNetAdvancedImport':
return ControlNetAdvancedImport(control_model=v.control_model, timestep_keyframes=timestep_keyframe,
global_average_pooling=v.global_average_pooling, device=v.device, load_device=v.load_device, manual_cast_dtype=v.manual_cast_dtype)
class T2IAdapterAdvancedImport(T2IAdapter, AdvancedControlBaseImport):
def __init__(self, t2i_model, timestep_keyframes: TimestepKeyframeGroupImport, channels_in, device=None):
super().__init__(t2i_model=t2i_model, channels_in=channels_in, device=device)
AdvancedControlBaseImport.__init__(self, super(), timestep_keyframes=timestep_keyframes, weights_default=ControlWeightsImport.t2iadapter())
def get_universal_weights(self) -> ControlWeightsImport:
raw_weights = [(self.weights.base_multiplier ** float(7 - i)) for i in range(8)]
raw_weights = [raw_weights[-8], raw_weights[-3], raw_weights[-2], raw_weights[-1]]
raw_weights = get_properly_arranged_t2i_weights(raw_weights)
return ControlWeightsImport.t2iadapter(raw_weights, self.weights.flip_weights)
def get_calc_pow(self, idx: int, layers: int) -> int:
# match how T2IAdapterAdvancedImport deals with universal weights
indeces = [7 - i for i in range(8)]
indeces = [indeces[-8], indeces[-3], indeces[-2], indeces[-1]]
indeces = get_properly_arranged_t2i_weights(indeces)
return indeces[idx]
def get_control_advanced(self, x_noisy, t, cond, batched_number):
# prepare timestep and everything related
self.prepare_current_timestep(t=t, batched_number=batched_number)
try:
# if sub indexes present, replace original hint with subsection
if self.sub_idxs is not None:
# cond hints
full_cond_hint_original = self.cond_hint_original
del self.cond_hint
self.cond_hint = None
self.cond_hint_original = full_cond_hint_original[self.sub_idxs]
# mask hints
self.prepare_mask_cond_hint(x_noisy=x_noisy, t=t, cond=cond, batched_number=batched_number)
return super().get_control(x_noisy, t, cond, batched_number)
finally:
if self.sub_idxs is not None:
# replace original cond hint
self.cond_hint_original = full_cond_hint_original
del full_cond_hint_original
def copy(self):
c = T2IAdapterAdvancedImport(self.t2i_model, self.timestep_keyframes, self.channels_in)
self.copy_to(c)
self.copy_to_advanced(c)
return c
def cleanup(self):
super().cleanup()
self.cleanup_advanced()
@staticmethod
def from_vanilla(v: T2IAdapter, timestep_keyframe: TimestepKeyframeGroupImport=None) -> 'T2IAdapterAdvancedImport':
return T2IAdapterAdvancedImport(t2i_model=v.t2i_model, timestep_keyframes=timestep_keyframe, channels_in=v.channels_in, device=v.device)
class ControlLoraAdvancedImport(ControlLora, AdvancedControlBaseImport):
def __init__(self, control_weights, timestep_keyframes: TimestepKeyframeGroupImport, global_average_pooling=False, device=None):
super().__init__(control_weights=control_weights, global_average_pooling=global_average_pooling, device=device)
AdvancedControlBaseImport.__init__(self, super(), timestep_keyframes=timestep_keyframes, weights_default=ControlWeightsImport.controllora())
# use some functions from ControlNetAdvancedImport
self.get_control_advanced = ControlNetAdvancedImport.get_control_advanced.__get__(self, type(self))
self.sliding_get_control = ControlNetAdvancedImport.sliding_get_control.__get__(self, type(self))
def get_universal_weights(self) -> ControlWeightsImport:
raw_weights = [(self.weights.base_multiplier ** float(9 - i)) for i in range(10)]
return ControlWeightsImport.controllora(raw_weights, self.weights.flip_weights)
def copy(self):
c = ControlLoraAdvancedImport(self.control_weights, self.timestep_keyframes, global_average_pooling=self.global_average_pooling)
self.copy_to(c)
self.copy_to_advanced(c)
return c
def cleanup(self):
super().cleanup()
self.cleanup_advanced()
@staticmethod
def from_vanilla(v: ControlLora, timestep_keyframe: TimestepKeyframeGroupImport=None) -> 'ControlLoraAdvancedImport':
return ControlLoraAdvancedImport(control_weights=v.control_weights, timestep_keyframes=timestep_keyframe,
global_average_pooling=v.global_average_pooling, device=v.device)
class ControlLLLiteAdvancedImport(ControlNet, AdvancedControlBaseImport):
def __init__(self, control_weights, timestep_keyframes: TimestepKeyframeGroupImport, device=None):
AdvancedControlBaseImport.__init__(self, super(), timestep_keyframes=timestep_keyframes, weights_default=ControlWeightsImport.controllllite())
def load_controlnet(ckpt_path, timestep_keyframe: TimestepKeyframeGroupImport=None, model=None):
control = comfy_cn.load_controlnet(ckpt_path, model=model)
# TODO: support controlnet-lllite
# if is None, see if is a non-vanilla ControlNet
# if control is None:
# controlnet_data = comfy.utils.load_torch_file(ckpt_path, safe_load=True)
# # check if lllite
# if "lllite_unet" in controlnet_data:
# pass
return convert_to_advanced(control, timestep_keyframe=timestep_keyframe)
def convert_to_advanced(control, timestep_keyframe: TimestepKeyframeGroupImport=None):
# if already advanced, leave it be
if is_advanced_controlnet(control):
return control
# if exactly ControlNet returned, transform it into ControlNetAdvancedImport
if type(control) == ControlNet:
return ControlNetAdvancedImport.from_vanilla(v=control, timestep_keyframe=timestep_keyframe)
# if exactly ControlLora returned, transform it into ControlLoraAdvancedImport
elif type(control) == ControlLora:
return ControlLoraAdvancedImport.from_vanilla(v=control, timestep_keyframe=timestep_keyframe)
# if T2IAdapter returned, transform it into T2IAdapterAdvancedImport
elif isinstance(control, T2IAdapter):
return T2IAdapterAdvancedImport.from_vanilla(v=control, timestep_keyframe=timestep_keyframe)
# otherwise, leave it be - might be something I am not supporting yet
return control
def is_advanced_controlnet(input_object):
return hasattr(input_object, "sub_idxs")
# adapted from comfy/sample.py
def prepare_mask_batch(mask: Tensor, shape: Tensor, multiplier: int=1, match_dim1=False):
mask = mask.clone()
mask = torch.nn.functional.interpolate(mask.reshape((-1, 1, mask.shape[-2], mask.shape[-1])), size=(shape[2]*multiplier, shape[3]*multiplier), mode="bilinear")
if match_dim1:
mask = torch.cat([mask] * shape[1], dim=1)
return mask
# applies min-max normalization, from:
# https://stackoverflow.com/questions/68791508/min-max-normalization-of-a-tensor-in-pytorch
def normalize_min_max(x: Tensor, new_min = 0.0, new_max = 1.0):
x_min, x_max = x.min(), x.max()
return (((x - x_min)/(x_max - x_min)) * (new_max - new_min)) + new_min
def linear_conversion(x, x_min=0.0, x_max=1.0, new_min=0.0, new_max=1.0):
return (((x - x_min)/(x_max - x_min)) * (new_max - new_min)) + new_min
class WeightTypeExceptionImport(TypeError):
"Raised when weight not compatible with AdvancedControlBaseImport object"
pass
@@ -1 +0,0 @@
@@ -1,103 +0,0 @@
import os
import torch
import numpy as np
from PIL import Image, ImageOps
from .control import ControlWeights, LatentKeyframeGroup, TimestepKeyframeGroup, TimestepKeyframe
from .logger import logger
class LoadImagesFromDirectory:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"directory": ("STRING", {"default": ""}),
},
"optional": {
"image_load_cap": ("INT", {"default": 0, "min": 0, "step": 1}),
"start_index": ("INT", {"default": 0, "min": 0, "step": 1}),
}
}
RETURN_TYPES = ("IMAGE", "MASK", "INT")
FUNCTION = "load_images"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/deprecated"
def load_images(self, directory: str, image_load_cap: int = 0, start_index: int = 0):
if not os.path.isdir(directory):
raise FileNotFoundError(f"Directory '{directory} cannot be found.'")
dir_files = os.listdir(directory)
if len(dir_files) == 0:
raise FileNotFoundError(f"No files in directory '{directory}'.")
dir_files = sorted(dir_files)
dir_files = [os.path.join(directory, x) for x in dir_files]
# start at start_index
dir_files = dir_files[start_index:]
images = []
masks = []
limit_images = False
if image_load_cap > 0:
limit_images = True
image_count = 0
for image_path in dir_files:
if os.path.isdir(image_path):
continue
if limit_images and image_count >= image_load_cap:
break
i = Image.open(image_path)
i = ImageOps.exif_transpose(i)
image = i.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
images.append(image)
masks.append(mask)
image_count += 1
if len(images) == 0:
raise FileNotFoundError(f"No images could be loaded from directory '{directory}'.")
return (torch.cat(images, dim=0), torch.stack(masks, dim=0), image_count)
class TimestepKeyframeNodeDeprecated:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}, ),
},
"optional": {
"control_net_weights": ("CONTROL_NET_WEIGHTS", ),
"t2i_adapter_weights": ("T2I_ADAPTER_WEIGHTS", ),
"latent_keyframe": ("LATENT_KEYFRAME", ),
"prev_timestep_keyframe": ("TIMESTEP_KEYFRAME", ),
}
}
RETURN_TYPES = ("TIMESTEP_KEYFRAME", )
FUNCTION = "load_keyframe"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/keyframes"
def load_keyframe(self,
start_percent: float,
control_net_weights: ControlWeights=None,
latent_keyframe: LatentKeyframeGroup=None,
prev_timestep_keyframe: TimestepKeyframeGroup=None):
if not prev_timestep_keyframe:
prev_timestep_keyframe = TimestepKeyframeGroup()
keyframe = TimestepKeyframe(start_percent, control_net_weights, latent_keyframe)
prev_timestep_keyframe.add(keyframe)
return (prev_timestep_keyframe,)
@@ -1,320 +0,0 @@
from typing import Union
import numpy as np
from collections.abc import Iterable
from .control import LatentKeyframeImport, LatentKeyframeGroupImport
from .control import StrengthInterpolationImport as SI
from .logger import logger
class LatentKeyframeNodeImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"batch_index": ("INT", {"default": 0, "min": -1000, "max": 1000, "step": 1}),
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
},
"optional": {
"prev_latent_kf": ("LATENT_KEYFRAME", ),
}
}
RETURN_NAMES = ("LATENT_KF", )
RETURN_TYPES = ("LATENT_KEYFRAME", )
FUNCTION = "load_keyframe"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/keyframes"
def load_keyframe(self,
batch_index: int,
strength: float,
prev_latent_kf: LatentKeyframeGroupImport=None,
prev_latent_keyframe: LatentKeyframeGroupImport=None, # old name
):
prev_latent_keyframe = prev_latent_keyframe if prev_latent_keyframe else prev_latent_kf
if not prev_latent_keyframe:
prev_latent_keyframe = LatentKeyframeGroupImport()
else:
prev_latent_keyframe = prev_latent_keyframe.clone()
keyframe = LatentKeyframeImport(batch_index, strength)
prev_latent_keyframe.add(keyframe)
return (prev_latent_keyframe,)
class LatentKeyframeGroupNodeImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"index_strengths": ("STRING", {"multiline": True, "default": ""}),
},
"optional": {
"prev_latent_kf": ("LATENT_KEYFRAME", ),
"latent_optional": ("LATENT", ),
"print_keyframes": ("BOOLEAN", {"default": False})
}
}
RETURN_NAMES = ("LATENT_KF", )
RETURN_TYPES = ("LATENT_KEYFRAME", )
FUNCTION = "load_keyframes"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/keyframes"
def validate_index(self, index: int, latent_count: int = 0, is_range: bool = False, allow_negative = False) -> int:
# if part of range, do nothing
if is_range:
return index
# otherwise, validate index
# validate not out of range - only when latent_count is passed in
if latent_count > 0 and index > latent_count-1:
raise IndexError(f"Index '{index}' out of range for the total {latent_count} latents.")
# if negative, validate not out of range
if index < 0:
if not allow_negative:
raise IndexError(f"Negative indeces not allowed, but was {index}.")
conv_index = latent_count+index
if conv_index < 0:
raise IndexError(f"Index '{index}', converted to '{conv_index}' out of range for the total {latent_count} latents.")
index = conv_index
return index
def convert_to_index_int(self, raw_index: str, latent_count: int = 0, is_range: bool = False, allow_negative = False) -> int:
try:
return self.validate_index(int(raw_index), latent_count=latent_count, is_range=is_range, allow_negative=allow_negative)
except ValueError as e:
raise ValueError(f"index '{raw_index}' must be an integer.", e)
def convert_to_latent_keyframes(self, latent_indeces: str, latent_count: int) -> set[LatentKeyframeImport]:
if not latent_indeces:
return set()
int_latent_indeces = [i for i in range(0, latent_count)]
allow_negative = latent_count > 0
chosen_indeces = set()
# parse string - allow positive ints, negative ints, and ranges separated by ':'
groups = latent_indeces.split(",")
groups = [g.strip() for g in groups]
for g in groups:
# parse strengths - default to 1.0 if no strength given
strength = 1.0
if '=' in g:
g, strength_str = g.split("=", 1)
g = g.strip()
try:
strength = float(strength_str.strip())
except ValueError as e:
raise ValueError(f"strength '{strength_str}' must be a float.", e)
if strength < 0:
raise ValueError(f"Strength '{strength}' cannot be negative.")
# parse range of indeces (e.g. 2:16)
if ':' in g:
index_range = g.split(":", 1)
index_range = [r.strip() for r in index_range]
start_index = self.convert_to_index_int(index_range[0], latent_count=latent_count, is_range=True, allow_negative=allow_negative)
end_index = self.convert_to_index_int(index_range[1], latent_count=latent_count, is_range=True, allow_negative=allow_negative)
# if latents were passed in, base indeces on known latent count
if len(int_latent_indeces) > 0:
for i in int_latent_indeces[start_index:end_index]:
chosen_indeces.add(LatentKeyframeImport(i, strength))
# otherwise, assume indeces are valid
else:
for i in range(start_index, end_index):
chosen_indeces.add(LatentKeyframeImport(i, strength))
# parse individual indeces
else:
chosen_indeces.add(LatentKeyframeImport(self.convert_to_index_int(g, latent_count=latent_count, allow_negative=allow_negative), strength))
return chosen_indeces
def load_keyframes(self,
index_strengths: str,
prev_latent_kf: LatentKeyframeGroupImport=None,
prev_latent_keyframe: LatentKeyframeGroupImport=None, # old name
latent_image_opt=None,
print_keyframes=False):
prev_latent_keyframe = prev_latent_keyframe if prev_latent_keyframe else prev_latent_kf
if not prev_latent_keyframe:
prev_latent_keyframe = LatentKeyframeGroupImport()
else:
prev_latent_keyframe = prev_latent_keyframe.clone()
curr_latent_keyframe = LatentKeyframeGroupImport()
latent_count = -1
if latent_image_opt:
latent_count = latent_image_opt['samples'].size()[0]
latent_keyframes = self.convert_to_latent_keyframes(index_strengths, latent_count=latent_count)
for latent_keyframe in latent_keyframes:
curr_latent_keyframe.add(latent_keyframe)
if print_keyframes:
for keyframe in curr_latent_keyframe.keyframes:
logger.info(f"keyframe {keyframe.batch_index}:{keyframe.strength}")
# replace values with prev_latent_keyframes
for latent_keyframe in prev_latent_keyframe.keyframes:
curr_latent_keyframe.add(latent_keyframe)
return (curr_latent_keyframe,)
class LatentKeyframeInterpolationNodeImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"batch_index_from": ("INT", {"default": 0, "min": -10000, "max": 10000, "step": 1}),
"batch_index_to_excl": ("INT", {"default": 0, "min": -10000, "max": 10000, "step": 1}),
"strength_from": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.0001}, ),
"strength_to": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.0001}, ),
"interpolation": (["linear", "ease-in", "ease-out", "ease-in-out"], ),
"revert_direction_at_midpoint": ("BOOLEAN", {"default": False}),
},
"optional": {
"prev_latent_keyframe": ("LATENT_KEYFRAME", ),
}
}
RETURN_TYPES = ("LATENT_KEYFRAME", )
FUNCTION = "load_keyframe"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/keyframes"
def load_keyframe(self,
batch_index_from: int,
strength_from: float,
batch_index_to_excl: int,
strength_to: float,
interpolation: str,
revert_direction_at_midpoint: bool=False,
last_key_frame_position: int=0,
i=0,
number_of_items=0,
buffer=0,
prev_latent_keyframe: LatentKeyframeGroupImport=None):
if not prev_latent_keyframe:
prev_latent_keyframe = LatentKeyframeGroupImport()
else:
prev_latent_keyframe = prev_latent_keyframe.clone()
curr_latent_keyframe = LatentKeyframeGroupImport()
weights, frame_numbers = calculate_weights(batch_index_from, batch_index_to_excl, strength_from, strength_to, interpolation, revert_direction_at_midpoint, last_key_frame_position,i,number_of_items, buffer)
for i, frame_number in enumerate(frame_numbers):
keyframe = LatentKeyframeImport(frame_number, float(weights[i]))
curr_latent_keyframe.add(keyframe)
for latent_keyframe in prev_latent_keyframe.keyframes:
curr_latent_keyframe.add(latent_keyframe)
return (weights, frame_numbers, curr_latent_keyframe,)
def calculate_weights(batch_index_from, batch_index_to, strength_from, strength_to, interpolation,revert_direction_at_midpoint, last_key_frame_position,i, number_of_items,buffer):
# Initialize variables based on the position of the keyframe
range_start = batch_index_from
range_end = batch_index_to
# if it's the first value, set influence range from 1.0 to 0.0
if buffer > 0:
if i == 0:
range_start = 0
elif i == 1:
range_start = buffer
else:
if i == 1:
range_start = 0
if i == number_of_items - 1:
range_end = last_key_frame_position
steps = range_end - range_start
diff = strength_to - strength_from
# Calculate index for interpolation
index = np.linspace(0, 1, steps // 2 + 1) if revert_direction_at_midpoint else np.linspace(0, 1, steps)
# Calculate weights based on interpolation type
if interpolation == "linear":
weights = np.linspace(strength_from, strength_to, len(index))
elif interpolation == "ease-in":
weights = diff * np.power(index, 2) + strength_from
elif interpolation == "ease-out":
weights = diff * (1 - np.power(1 - index, 2)) + strength_from
elif interpolation == "ease-in-out":
weights = diff * ((1 - np.cos(index * np.pi)) / 2) + strength_from
# If it's a middle keyframe, mirror the weights
if revert_direction_at_midpoint:
weights = np.concatenate([weights, weights[::-1]])
# Generate frame numbers
frame_numbers = np.arange(range_start, range_start + len(weights))
# "Dropper" component: For keyframes with negative start, drop the weights
if range_start < 0 and i > 0:
drop_count = abs(range_start)
weights = weights[drop_count:]
frame_numbers = frame_numbers[drop_count:]
# Dropper component: for keyframes a range_End is greater than last_key_frame_position, drop the weights
if range_end > last_key_frame_position and i < number_of_items - 1:
drop_count = range_end - last_key_frame_position
weights = weights[:-drop_count]
frame_numbers = frame_numbers[:-drop_count]
return weights, frame_numbers
class LatentKeyframeBatchedGroupNodeImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"float_strengths": ("FLOAT", {"default": -1, "min": -1, "step": 0.001, "forceInput": True}),
},
"optional": {
"prev_latent_kf": ("LATENT_KEYFRAME", ),
"print_keyframes": ("BOOLEAN", {"default": False})
}
}
RETURN_NAMES = ("LATENT_KF", )
RETURN_TYPES = ("LATENT_KEYFRAME", )
FUNCTION = "load_keyframe"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/keyframes"
def load_keyframe(self, float_strengths: Union[float, list[float]],
prev_latent_kf: LatentKeyframeGroupImport=None,
prev_latent_keyframe: LatentKeyframeGroupImport=None, # old name
print_keyframes=False):
prev_latent_keyframe = prev_latent_keyframe if prev_latent_keyframe else prev_latent_kf
if not prev_latent_keyframe:
prev_latent_keyframe = LatentKeyframeGroupImport()
else:
prev_latent_keyframe = prev_latent_keyframe.clone()
curr_latent_keyframe = LatentKeyframeGroupImport()
# if received a normal float input, do nothing
if type(float_strengths) in (float, int):
logger.info("No batched float_strengths passed into Latent Keyframe Batch Group node; will not create any new keyframes.")
# if iterable, attempt to create LatentKeyframes with chosen strengths
elif isinstance(float_strengths, Iterable):
for idx, strength in enumerate(float_strengths):
keyframe = LatentKeyframeImport(idx, strength)
curr_latent_keyframe.add(keyframe)
else:
raise ValueError(f"Expected strengths to be an iterable input, but was {type(float_strengths).__repr__}.")
if print_keyframes:
for keyframe in curr_latent_keyframe.keyframes:
logger.info(f"keyframe {keyframe.batch_index}:{keyframe.strength}")
# replace values with prev_latent_keyframes
for latent_keyframe in prev_latent_keyframe.keyframes:
curr_latent_keyframe.add(latent_keyframe)
return (curr_latent_keyframe,)
-36
View File
@@ -1,36 +0,0 @@
import sys
import copy
import logging
class ColoredFormatter(logging.Formatter):
COLORS = {
"DEBUG": "\033[0;36m", # CYAN
"INFO": "\033[0;32m", # GREEN
"WARNING": "\033[0;33m", # YELLOW
"ERROR": "\033[0;31m", # RED
"CRITICAL": "\033[0;37;41m", # WHITE ON RED
"RESET": "\033[0m", # RESET COLOR
}
def format(self, record):
colored_record = copy.copy(record)
levelname = colored_record.levelname
seq = self.COLORS.get(levelname, self.COLORS["RESET"])
colored_record.levelname = f"{seq}{levelname}{self.COLORS['RESET']}"
return super().format(colored_record)
# Create a new logger
logger = logging.getLogger("Advanced-ControlNet")
logger.propagate = False
# Add handler if we don't have one.
if not logger.handlers:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(ColoredFormatter("[%(name)s] - %(levelname)s - %(message)s"))
logger.addHandler(handler)
# Configure logger
loglevel = logging.INFO
logger.setLevel(loglevel)
-194
View File
@@ -1,194 +0,0 @@
import numpy as np
from torch import Tensor
import folder_paths
from .control import load_controlnet, convert_to_advanced, ControlWeightsImport, ControlWeightTypeImport,\
LatentKeyframeGroupImport, TimestepKeyframeImport, TimestepKeyframeGroupImport, is_advanced_controlnet
from .control import StrengthInterpolationImport as SI
from .weight_nodes import DefaultWeightsImport, ScaledSoftMaskedUniversalWeightsImport, ScaledSoftUniversalWeightsImport, SoftControlNetWeightsImport, CustomControlNetWeightsImport, \
SoftT2IAdapterWeightsImport, CustomT2IAdapterWeightsImport
from .latent_keyframe_nodes import LatentKeyframeGroupNodeImport, LatentKeyframeInterpolationNodeImport, LatentKeyframeBatchedGroupNodeImport, LatentKeyframeNodeImport
from .logger import logger
class TimestepKeyframeNodeImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}, ),
},
"optional": {
"prev_timestep_kf": ("TIMESTEP_KEYFRAME", ),
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"cn_weights": ("CONTROL_NET_WEIGHTS", ),
"latent_keyframe": ("LATENT_KEYFRAME", ),
"null_latent_kf_strength": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"inherit_missing": ("BOOLEAN", {"default": True}, ),
"guarantee_usage": ("BOOLEAN", {"default": True}, ),
"mask_optional": ("MASK", ),
#"interpolation": ([SI.LINEAR, SI.EASE_IN, SI.EASE_OUT, SI.EASE_IN_OUT, SI.NONE], {"default": SI.NONE}, ),
}
}
RETURN_NAMES = ("TIMESTEP_KF", )
RETURN_TYPES = ("TIMESTEP_KEYFRAME", )
FUNCTION = "load_keyframe"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/keyframes"
def load_keyframe(self,
start_percent: float,
strength: float=1.0,
cn_weights: ControlWeightsImport=None, control_net_weights: ControlWeightsImport=None, # old name
latent_keyframe: LatentKeyframeGroupImport=None,
prev_timestep_kf: TimestepKeyframeGroupImport=None, prev_timestep_keyframe: TimestepKeyframeGroupImport=None, # old name
null_latent_kf_strength: float=0.0,
inherit_missing=True,
guarantee_usage=True,
mask_optional=None,
interpolation: str=SI.NONE,):
control_net_weights = control_net_weights if control_net_weights else cn_weights
prev_timestep_keyframe = prev_timestep_keyframe if prev_timestep_keyframe else prev_timestep_kf
if not prev_timestep_keyframe:
prev_timestep_keyframe = TimestepKeyframeGroupImport()
else:
prev_timestep_keyframe = prev_timestep_keyframe.clone()
keyframe = TimestepKeyframeImport(start_percent=start_percent, strength=strength, interpolation=interpolation, null_latent_kf_strength=null_latent_kf_strength,
control_weights=control_net_weights, latent_keyframes=latent_keyframe, inherit_missing=inherit_missing, guarantee_usage=guarantee_usage,
mask_hint_orig=mask_optional)
prev_timestep_keyframe.add(keyframe)
return (prev_timestep_keyframe,)
class ControlNetLoaderAdvancedImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"control_net_name": (folder_paths.get_filename_list("controlnet"), ),
},
"optional": {
"timestep_keyframe": ("TIMESTEP_KEYFRAME", ),
}
}
RETURN_TYPES = ("CONTROL_NET", )
FUNCTION = "load_controlnet"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝"
def load_controlnet(self, control_net_name,
timestep_keyframe: TimestepKeyframeGroupImport=None
):
controlnet_path = folder_paths.get_full_path("controlnet", control_net_name)
controlnet = load_controlnet(controlnet_path, timestep_keyframe)
return (controlnet,)
class DiffControlNetLoaderAdvancedImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL",),
"control_net_name": (folder_paths.get_filename_list("controlnet"), )
},
"optional": {
"timestep_keyframe": ("TIMESTEP_KEYFRAME", ),
}
}
RETURN_TYPES = ("CONTROL_NET", )
FUNCTION = "load_controlnet"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝"
def load_controlnet(self, control_net_name, model,
timestep_keyframe: TimestepKeyframeGroupImport=None
):
controlnet_path = folder_paths.get_full_path("controlnet", control_net_name)
controlnet = load_controlnet(controlnet_path, timestep_keyframe, model)
if is_advanced_controlnet(controlnet):
controlnet.verify_all_weights()
return (controlnet,)
class AdvancedControlNetApplyImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"positive": ("CONDITIONING", ),
"negative": ("CONDITIONING", ),
"control_net": ("CONTROL_NET", ),
"image": ("IMAGE", ),
"strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}),
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001})
},
"optional": {
"mask_optional": ("MASK", ),
"timestep_kf": ("TIMESTEP_KEYFRAME", ),
"latent_kf_override": ("LATENT_KEYFRAME", ),
"weights_override": ("CONTROL_NET_WEIGHTS", ),
}
}
RETURN_TYPES = ("CONDITIONING","CONDITIONING")
RETURN_NAMES = ("positive", "negative")
FUNCTION = "apply_controlnet"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝"
def apply_controlnet(self, positive, negative, control_net, image, strength, start_percent, end_percent,
mask_optional: Tensor=None,
timestep_kf: TimestepKeyframeGroupImport=None, latent_kf_override: LatentKeyframeGroupImport=None,
weights_override: ControlWeightsImport=None):
if strength == 0:
return (positive, negative)
control_hint = image.movedim(-1,1)
cnets = {}
out = []
for conditioning in [positive, negative]:
c = []
for t in conditioning:
d = t[1].copy()
prev_cnet = d.get('control', None)
if prev_cnet in cnets:
c_net = cnets[prev_cnet]
else:
# copy, convert to advanced if needed, and set cond
c_net = convert_to_advanced(control_net.copy()).set_cond_hint(control_hint, strength, (start_percent, end_percent))
if is_advanced_controlnet(c_net):
# apply optional parameters and overrides, if provided
if timestep_kf is not None:
c_net.set_timestep_keyframes(timestep_kf)
if latent_kf_override is not None:
c_net.latent_keyframe_override = latent_kf_override
if weights_override is not None:
c_net.weights_override = weights_override
# verify weights are compatible
c_net.verify_all_weights()
# set cond hint mask
if mask_optional is not None:
mask_optional = mask_optional.clone()
# if not in the form of a batch, make it so
if len(mask_optional.shape) < 3:
mask_optional = mask_optional.unsqueeze(0)
c_net.set_cond_hint_mask(mask_optional)
c_net.set_previous_controlnet(prev_cnet)
cnets[prev_cnet] = c_net
d['control'] = c_net
d['control_apply_to_uncond'] = False
n = [t[0], d]
c.append(n)
out.append(c)
return (out[0], out[1])
@@ -1,12 +0,0 @@
class AnimateDiffLoaderWithContext:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL",),
"image": ("IMAGE",),
},
}
RETURN_TYPES = ("MODEL",)
CATEGORY = ""
-201
View File
@@ -1,201 +0,0 @@
from torch import Tensor
import torch
from .control import TimestepKeyframeImport, TimestepKeyframeGroupImport, ControlWeightsImport, get_properly_arranged_t2i_weights, linear_conversion
from .logger import logger
WEIGHTS_RETURN_NAMES = ("CN_WEIGHTS", "TK_SHORTCUT")
class DefaultWeightsImport:
@classmethod
def INPUT_TYPES(s):
return {
}
RETURN_TYPES = ("CONTROL_NET_WEIGHTS", "TIMESTEP_KEYFRAME",)
RETURN_NAMES = WEIGHTS_RETURN_NAMES
FUNCTION = "load_weights"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/weights"
def load_weights(self):
weights = ControlWeightsImport.default()
return (weights, TimestepKeyframeGroupImport.default(TimestepKeyframeImport(control_weights=weights)))
class ScaledSoftMaskedUniversalWeightsImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"mask": ("MASK", ),
"min_base_multiplier": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001}, ),
"max_base_multiplier": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001}, ),
#"lock_min": ("BOOLEAN", {"default": False}, ),
#"lock_max": ("BOOLEAN", {"default": False}, ),
},
}
RETURN_TYPES = ("CONTROL_NET_WEIGHTS", "TIMESTEP_KEYFRAME",)
RETURN_NAMES = WEIGHTS_RETURN_NAMES
FUNCTION = "load_weights"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/weights"
def load_weights(self, mask: Tensor, min_base_multiplier: float, max_base_multiplier: float, lock_min=False, lock_max=False):
# normalize mask
mask = mask.clone()
x_min = 0.0 if lock_min else mask.min()
x_max = 1.0 if lock_max else mask.max()
if x_min == x_max:
mask = torch.ones_like(mask) * max_base_multiplier
else:
mask = linear_conversion(mask, x_min, x_max, min_base_multiplier, max_base_multiplier)
weights = ControlWeightsImport.universal_mask(weight_mask=mask)
return (weights, TimestepKeyframeGroupImport.default(TimestepKeyframeImport(control_weights=weights)))
class ScaledSoftUniversalWeightsImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"base_multiplier": ("FLOAT", {"default": 0.825, "min": 0.0, "max": 1.0, "step": 0.001}, ),
"flip_weights": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("CONTROL_NET_WEIGHTS", "TIMESTEP_KEYFRAME",)
RETURN_NAMES = WEIGHTS_RETURN_NAMES
FUNCTION = "load_weights"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/weights"
def load_weights(self, base_multiplier, flip_weights):
weights = ControlWeightsImport.universal(base_multiplier=base_multiplier, flip_weights=flip_weights)
return (weights, TimestepKeyframeGroupImport.default(TimestepKeyframeImport(control_weights=weights)))
class SoftControlNetWeightsImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"weight_00": ("FLOAT", {"default": 0.09941396206337118, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_01": ("FLOAT", {"default": 0.12050177219802567, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_02": ("FLOAT", {"default": 0.14606275417942507, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_03": ("FLOAT", {"default": 0.17704576264172736, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_04": ("FLOAT", {"default": 0.214600924414215, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_05": ("FLOAT", {"default": 0.26012233262329093, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_06": ("FLOAT", {"default": 0.3152997971191405, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_07": ("FLOAT", {"default": 0.3821815722656249, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_08": ("FLOAT", {"default": 0.4632503906249999, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_09": ("FLOAT", {"default": 0.561515625, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_10": ("FLOAT", {"default": 0.6806249999999999, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_11": ("FLOAT", {"default": 0.825, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_12": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"flip_weights": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("CONTROL_NET_WEIGHTS", "TIMESTEP_KEYFRAME",)
RETURN_NAMES = WEIGHTS_RETURN_NAMES
FUNCTION = "load_weights"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/weights/ControlNet"
def load_weights(self, weight_00, weight_01, weight_02, weight_03, weight_04, weight_05, weight_06,
weight_07, weight_08, weight_09, weight_10, weight_11, weight_12, flip_weights):
weights = [weight_00, weight_01, weight_02, weight_03, weight_04, weight_05, weight_06,
weight_07, weight_08, weight_09, weight_10, weight_11, weight_12]
weights = ControlWeightsImport.controlnet(weights, flip_weights=flip_weights)
return (weights, TimestepKeyframeGroupImport.default(TimestepKeyframeImport(control_weights=weights)))
class CustomControlNetWeightsImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"weight_00": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_01": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_02": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_03": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_04": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_05": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_06": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_07": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_08": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_09": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_10": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_11": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_12": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"flip_weights": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("CONTROL_NET_WEIGHTS", "TIMESTEP_KEYFRAME",)
RETURN_NAMES = WEIGHTS_RETURN_NAMES
FUNCTION = "load_weights"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/weights/ControlNet"
def load_weights(self, weight_00, weight_01, weight_02, weight_03, weight_04, weight_05, weight_06,
weight_07, weight_08, weight_09, weight_10, weight_11, weight_12, flip_weights):
weights = [weight_00, weight_01, weight_02, weight_03, weight_04, weight_05, weight_06,
weight_07, weight_08, weight_09, weight_10, weight_11, weight_12]
weights = ControlWeightsImport.controlnet(weights, flip_weights=flip_weights)
return (weights, TimestepKeyframeGroupImport.default(TimestepKeyframeImport(control_weights=weights)))
class SoftT2IAdapterWeightsImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"weight_00": ("FLOAT", {"default": 0.25, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_01": ("FLOAT", {"default": 0.62, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_02": ("FLOAT", {"default": 0.825, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_03": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"flip_weights": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("CONTROL_NET_WEIGHTS", "TIMESTEP_KEYFRAME",)
RETURN_NAMES = WEIGHTS_RETURN_NAMES
FUNCTION = "load_weights"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/weights/T2IAdapter"
def load_weights(self, weight_00, weight_01, weight_02, weight_03, flip_weights):
weights = [weight_00, weight_01, weight_02, weight_03]
weights = get_properly_arranged_t2i_weights(weights)
weights = ControlWeightsImport.t2iadapter(weights, flip_weights=flip_weights)
return (weights, TimestepKeyframeGroupImport.default(TimestepKeyframeImport(control_weights=weights)))
class CustomT2IAdapterWeightsImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"weight_00": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_01": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_02": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"weight_03": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.001}, ),
"flip_weights": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("CONTROL_NET_WEIGHTS", "TIMESTEP_KEYFRAME",)
RETURN_NAMES = WEIGHTS_RETURN_NAMES
FUNCTION = "load_weights"
CATEGORY = "Adv-ControlNet 🛂🅐🅒🅝/weights/T2IAdapter"
def load_weights(self, weight_00, weight_01, weight_02, weight_03, flip_weights):
weights = [weight_00, weight_01, weight_02, weight_03]
weights = get_properly_arranged_t2i_weights(weights)
weights = ControlWeightsImport.t2iadapter(weights, flip_weights=flip_weights)
return (weights, TimestepKeyframeGroupImport.default(TimestepKeyframeImport(control_weights=weights)))
@@ -0,0 +1,3 @@
ckpts
__pycache__
test_result
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Fannovel16
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,194 @@
# ComfyUI Frame Interpolation (ComfyUI VFI) (WIP)
A custom node set for Video Frame Interpolation in ComfyUI.
**UPDATE** Memory management is improved. Now this extension takes less RAM and VRAM than before.
**UPDATE 2** VFI nodes now accept scheduling multipiler values
![](./interpolation_schedule.png)
![](./test_vfi_schedule.gif)
## Nodes
* KSampler Gradually Adding More Denoise (efficient)
* GMFSS Fortuna VFI
* IFRNet VFI
* IFUnet VFI
* M2M VFI
* RIFE VFI (4.0 - 4.9) (Note that option `fast_mode` won't do anything from v4.5+ as `contextnet` is removed)
* FILM VFI
* Sepconv VFI
* AMT VFI
* Make Interpolation State List
* STMFNet VFI (requires at least 4 frames, can only do 2x interpolation for now)
* FLAVR VFI (same conditions as STMFNet)
## Install
### ComfyUI Manager
Incompatibile issue with it is now fixed
Following this guide to install this extension
https://github.com/ltdrdata/ComfyUI-Manager#how-to-use
### Command-line
#### Windows
Run install.bat
For Window users, if you are having trouble with cupy, please run `install.bat` instead of `install-cupy.py` or `python install.py`.
#### Linux
Open your shell app and start venv if it is used for ComfyUI. Run:
```
python install.py
```
## Support for non-CUDA device (experimental)
If you don't have a NVidia card, you can try `taichi` ops backend powered by [Taichi Lang](https://www.taichi-lang.org/)
On Windows, you can install it by running `install.bat` or `pip install taichi` on Linux
Then change value of `ops_backend` from `cupy` to `taichi` in `config.yaml`
If `NotImplementedError` appears, a VFI node in the workflow isn't supported by taichi
## Usage
All VFI nodes can be accessed in **category** `ComfyUI-Frame-Interpolation/VFI` if the installation is successful and require a `IMAGE` containing frames (at least 2, or at least 4 for STMF-Net/FLAVR).
Regarding STMFNet and FLAVR, if you only have two or three frames, you should use: Load Images -> Other VFI node (FILM is recommended in this case) with `multiplier=4` -> STMFNet VFI/FLAVR VFI
`clear_cache_after_n_frames` is used to avoid out-of-memory. Decreasing it makes the chance lower but also increases processing time.
It is recommended to use LoadImages (LoadImagesFromDirectory) from [ComfyUI-Advanced-ControlNet](https://github.com/Kosinkadink/ComfyUI-Advanced-ControlNet/) and [ComfyUI-VideoHelperSuite](https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite) along side with this extension.
## Example
### Simple workflow
Workflow metadata isn't embeded
Download these two images [anime0.png](./demo_frames/anime0.png) and [anime1.png](./demo_frames/anime0.png) and put them into a folder like `E:\test` in this image.
![](./example.png)
### Complex workflow
It's used in AnimationDiff (can load workflow metadata)
![](All_in_one_v1_3.png)
## Credit
Big thanks for styler00dollar for making [VSGAN-tensorrt-docker](https://github.com/styler00dollar/VSGAN-tensorrt-docker). About 99% the code of this repo comes from it.
Citation for each VFI node:
### GMFSS Fortuna
The All-In-One GMFSS: Dedicated for Anime Video Frame Interpolation
https://github.com/98mxr/GMFSS_Fortuna
### IFRNet
```bibtex
@InProceedings{Kong_2022_CVPR,
author = {Kong, Lingtong and Jiang, Boyuan and Luo, Donghao and Chu, Wenqing and Huang, Xiaoming and Tai, Ying and Wang, Chengjie and Yang, Jie},
title = {IFRNet: Intermediate Feature Refine Network for Efficient Frame Interpolation},
booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
year = {2022}
}
```
### IFUnet
RIFE with IFUNet, FusionNet and RefineNet
https://github.com/98mxr/IFUNet
### M2M
```bibtex
@InProceedings{hu2022m2m,
title={Many-to-many Splatting for Efficient Video Frame Interpolation},
author={Hu, Ping and Niklaus, Simon and Sclaroff, Stan and Saenko, Kate},
journal={CVPR},
year={2022}
}
```
### RIFE
```bibtex
@inproceedings{huang2022rife,
title={Real-Time Intermediate Flow Estimation for Video Frame Interpolation},
author={Huang, Zhewei and Zhang, Tianyuan and Heng, Wen and Shi, Boxin and Zhou, Shuchang},
booktitle={Proceedings of the European Conference on Computer Vision (ECCV)},
year={2022}
}
```
### FILM
[Frame interpolation in PyTorch](https://github.com/dajes/frame-interpolation-pytorch)
```bibtex
@inproceedings{reda2022film,
title = {FILM: Frame Interpolation for Large Motion},
author = {Fitsum Reda and Janne Kontkanen and Eric Tabellion and Deqing Sun and Caroline Pantofaru and Brian Curless},
booktitle = {European Conference on Computer Vision (ECCV)},
year = {2022}
}
```
```bibtex
@misc{film-tf,
title = {Tensorflow 2 Implementation of "FILM: Frame Interpolation for Large Motion"},
author = {Fitsum Reda and Janne Kontkanen and Eric Tabellion and Deqing Sun and Caroline Pantofaru and Brian Curless},
year = {2022},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/google-research/frame-interpolation}}
}
```
### Sepconv
```bibtex
[1] @inproceedings{Niklaus_WACV_2021,
author = {Simon Niklaus and Long Mai and Oliver Wang},
title = {Revisiting Adaptive Convolutions for Video Frame Interpolation},
booktitle = {IEEE Winter Conference on Applications of Computer Vision},
year = {2021}
}
```
```bibtex
[2] @inproceedings{Niklaus_ICCV_2017,
author = {Simon Niklaus and Long Mai and Feng Liu},
title = {Video Frame Interpolation via Adaptive Separable Convolution},
booktitle = {IEEE International Conference on Computer Vision},
year = {2017}
}
```
```bibtex
[3] @inproceedings{Niklaus_CVPR_2017,
author = {Simon Niklaus and Long Mai and Feng Liu},
title = {Video Frame Interpolation via Adaptive Convolution},
booktitle = {IEEE Conference on Computer Vision and Pattern Recognition},
year = {2017}
}
```
### AMT
```bibtex
@inproceedings{licvpr23amt,
title={AMT: All-Pairs Multi-Field Transforms for Efficient Frame Interpolation},
author={Li, Zhen and Zhu, Zuo-Liang and Han, Ling-Hao and Hou, Qibin and Guo, Chun-Le and Cheng, Ming-Ming},
booktitle={IEEE Conference on Computer Vision and Pattern Recognition (CVPR)},
year={2023}
}
```
### ST-MFNet
```bibtex
@InProceedings{Danier_2022_CVPR,
author = {Danier, Duolikun and Zhang, Fan and Bull, David},
title = {ST-MFNet: A Spatio-Temporal Multi-Flow Network for Frame Interpolation},
booktitle = {Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR)},
month = {June},
year = {2022},
pages = {3521-3531}
}
```
### FLAVR
```bibtex
@article{kalluri2021flavr,
title={FLAVR: Flow-Agnostic Video Representations for Fast Frame Interpolation},
author={Kalluri, Tarun and Pathak, Deepak and Chandraker, Manmohan and Tran, Du},
booktitle={arxiv},
year={2021}
}
```
@@ -0,0 +1,4 @@
import os
import sys
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
@@ -0,0 +1,3 @@
#Plz don't delete this file, just edit it when neccessary.
ckpts_path: "./ckpts"
ops_backend: "cupy" #Either "taichi" or "cupy"
Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

@@ -0,0 +1,295 @@
import yaml
import os
from torch.hub import download_url_to_file, get_dir
from urllib.parse import urlparse
import torch
import typing
import traceback
import einops
import gc
import torchvision.transforms.functional as transform
from comfy.model_management import soft_empty_cache, get_torch_device
import numpy as np
BASE_MODEL_DOWNLOAD_URLS = [
"https://github.com/styler00dollar/VSGAN-tensorrt-docker/releases/download/models/",
"https://github.com/Fannovel16/ComfyUI-Frame-Interpolation/releases/download/models/",
"https://github.com/dajes/frame-interpolation-pytorch/releases/download/v1.0.0/"
]
config_path = os.path.join(os.path.dirname(__file__), "./config.yaml")
if os.path.exists(config_path):
config = yaml.load(open(config_path, "r"), Loader=yaml.FullLoader)
else:
raise Exception("config.yaml file is neccessary, plz recreate the config file by downloading it from https://github.com/Fannovel16/ComfyUI-Frame-Interpolation")
DEVICE = get_torch_device()
class InterpolationStateListImport():
def __init__(self, frame_indices: typing.List[int], is_skip_list: bool):
self.frame_indices = frame_indices
self.is_skip_list = is_skip_list
def is_frame_skipped(self, frame_index):
is_frame_in_list = frame_index in self.frame_indices
return self.is_skip_list and is_frame_in_list or not self.is_skip_list and not is_frame_in_list
class MakeInterpolationStateListImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"frame_indices": ("STRING", {"multiline": True, "default": "1,2,3"}),
"is_skip_list": ("BOOLEAN", {"default": True},),
},
}
RETURN_TYPES = ("INTERPOLATION_STATES",)
FUNCTION = "create_options"
CATEGORY = "ComfyUI-Frame-Interpolation/VFI"
def create_options(self, frame_indices: str, is_skip_list: bool):
frame_indices_list = [int(item) for item in frame_indices.split(',')]
interpolation_state_list = InterpolationStateListImport(
frame_indices=frame_indices_list,
is_skip_list=is_skip_list,
)
return (interpolation_state_list,)
def get_ckpt_container_path(model_type):
return os.path.abspath(os.path.join(os.path.dirname(__file__), config["ckpts_path"], model_type))
def load_file_from_url(url, model_dir=None, progress=True, file_name=None):
"""Load file form http url, will download models if necessary.
Ref:https://github.com/1adrianb/face-alignment/blob/master/face_alignment/utils.py
Args:
url (str): URL to be downloaded.
model_dir (str): The path to save the downloaded model. Should be a full path. If None, use pytorch hub_dir.
Default: None.
progress (bool): Whether to show the download progress. Default: True.
file_name (str): The downloaded file name. If None, use the file name in the url. Default: None.
Returns:
str: The path to the downloaded file.
"""
if model_dir is None: # use the pytorch hub_dir
hub_dir = get_dir()
model_dir = os.path.join(hub_dir, 'checkpoints')
os.makedirs(model_dir, exist_ok=True)
parts = urlparse(url)
file_name = os.path.basename(parts.path)
if file_name is not None:
file_name = file_name
cached_file = os.path.abspath(os.path.join(model_dir, file_name))
if not os.path.exists(cached_file):
print(f'Downloading: "{url}" to {cached_file}\n')
download_url_to_file(url, cached_file, hash_prefix=None, progress=progress)
return cached_file
def load_file_from_github_release(model_type, ckpt_name):
error_strs = []
for i, base_model_download_url in enumerate(BASE_MODEL_DOWNLOAD_URLS):
try:
return load_file_from_url(base_model_download_url + ckpt_name, get_ckpt_container_path(model_type))
except Exception:
traceback_str = traceback.format_exc()
if i < len(BASE_MODEL_DOWNLOAD_URLS) - 1:
print("Failed! Trying another endpoint.")
error_strs.append(f"Error when downloading from: {base_model_download_url + ckpt_name}\n\n{traceback_str}")
error_str = '\n\n'.join(error_strs)
raise Exception(f"Tried all GitHub base urls to download {ckpt_name} but no suceess. Below is the error log:\n\n{error_str}")
def load_file_from_direct_url(model_type, url):
return load_file_from_url(url, get_ckpt_container_path(model_type))
def preprocess_frames(frames):
return einops.rearrange(frames[..., :3], "n h w c -> n c h w")
def postprocess_frames(frames):
return einops.rearrange(frames, "n c h w -> n h w c")[..., :3].cpu()
def assert_batch_size(frames, batch_size=2, vfi_name=None):
subject_verb = "Most VFI models require" if vfi_name is None else f"VFI model {vfi_name} requires"
assert len(frames) >= batch_size, f"{subject_verb} at least {batch_size} frames to work with, only found {frames.shape[0]}. Please check the frame input using PreviewImage."
def _generic_frame_loop(
frames,
clear_cache_after_n_frames,
multiplier: typing.Union[typing.SupportsInt, typing.List],
return_middle_frame_function,
*return_middle_frame_function_args,
interpolation_states: InterpolationStateListImport = None,
use_timestep=True,
dtype=torch.float16,
final_logging=True):
#https://github.com/hzwer/Practical-RIFE/blob/main/inference_video.py#L169
def non_timestep_inference(frame0, frame1, n):
middle = return_middle_frame_function(frame0, frame1, None, *return_middle_frame_function_args)
if n == 1:
return [middle]
first_half = non_timestep_inference(frame0, middle, n=n//2)
second_half = non_timestep_inference(middle, frame1, n=n//2)
if n%2:
return [*first_half, middle, *second_half]
else:
return [*first_half, *second_half]
output_frames = torch.zeros(multiplier*frames.shape[0], *frames.shape[1:], dtype=dtype, device="cpu")
out_len = 0
number_of_frames_processed_since_last_cleared_cuda_cache = 0
for frame_itr in range(len(frames) - 1): # Skip the final frame since there are no frames after it
frame0 = frames[frame_itr:frame_itr+1]
output_frames[out_len] = frame0 # Start with first frame
out_len += 1
# Ensure that input frames are in fp32 - the same dtype as model
frame0 = frame0.to(dtype=torch.float32)
frame1 = frames[frame_itr+1:frame_itr+2].to(dtype=torch.float32)
if interpolation_states is not None and interpolation_states.is_frame_skipped(frame_itr):
continue
# Generate and append a batch of middle frames
middle_frame_batches = []
if use_timestep:
for middle_i in range(1, multiplier):
timestep = middle_i/multiplier
middle_frame = return_middle_frame_function(
frame0.to(DEVICE),
frame1.to(DEVICE),
timestep,
*return_middle_frame_function_args
).detach().cpu()
middle_frame_batches.append(middle_frame.to(dtype=dtype))
else:
middle_frames = non_timestep_inference(frame0.to(DEVICE), frame1.to(DEVICE), multiplier - 1)
middle_frame_batches.extend(torch.cat(middle_frames, dim=0).detach().cpu().to(dtype=dtype))
# Copy middle frames to output
for middle_frame in middle_frame_batches:
output_frames[out_len] = middle_frame
out_len += 1
number_of_frames_processed_since_last_cleared_cuda_cache += 1
# Try to avoid a memory overflow by clearing cuda cache regularly
if number_of_frames_processed_since_last_cleared_cuda_cache >= clear_cache_after_n_frames:
print("Comfy-VFI: Clearing cache...", end=' ')
soft_empty_cache()
number_of_frames_processed_since_last_cleared_cuda_cache = 0
print("Done cache clearing")
gc.collect()
if final_logging:
print(f"Comfy-VFI done! {len(output_frames)} frames generated at resolution: {output_frames[0].shape}")
# Append final frame
output_frames[out_len] = frames[-1:]
out_len += 1
# clear cache for courtesy
if final_logging:
print("Comfy-VFI: Final clearing cache...", end = ' ')
soft_empty_cache()
if final_logging:
print("Done cache clearing")
return output_frames[:out_len]
def generic_frame_loop(
model_name,
frames,
clear_cache_after_n_frames,
multiplier: typing.Union[typing.SupportsInt, typing.List],
return_middle_frame_function,
*return_middle_frame_function_args,
interpolation_states: InterpolationStateListImport = None,
use_timestep=True,
dtype=torch.float32):
assert_batch_size(frames, vfi_name=model_name.replace('_', ' ').replace('VFI', ''))
if type(multiplier) == int:
return _generic_frame_loop(
frames,
clear_cache_after_n_frames,
multiplier,
return_middle_frame_function,
*return_middle_frame_function_args,
interpolation_states=interpolation_states,
use_timestep=use_timestep,
dtype=dtype
)
if type(multiplier) == list:
multipliers = list(map(int, multiplier))
multipliers += [2] * (len(frames) - len(multipliers) - 1)
frame_batches = []
for frame_itr in range(len(frames) - 1):
multiplier = multipliers[frame_itr]
if multiplier == 0: continue
frame_batch = _generic_frame_loop(
frames[frame_itr:frame_itr+2],
clear_cache_after_n_frames,
multiplier,
return_middle_frame_function,
*return_middle_frame_function_args,
interpolation_states=interpolation_states,
use_timestep=use_timestep,
dtype=dtype,
final_logging=False
)
if frame_itr != len(frames) - 2: # Not append last frame unless this batch is the last one
frame_batch = frame_batch[:-1]
frame_batches.append(frame_batch)
output_frames = torch.cat(frame_batches)
print(f"Comfy-VFI done! {len(output_frames)} frames generated at resolution: {output_frames[0].shape}")
return output_frames
raise NotImplementedError(f"multipiler of {type(multiplier)}")
class FloatToIntImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"float": ("FLOAT", {"default": 0, 'min': 0, 'step': 0.01})
}
}
RETURN_TYPES = ("INT",)
FUNCTION = "convert"
CATEGORY = "ComfyUI-Frame-Interpolation"
def convert(self, float):
if hasattr(float, "__iter__"):
return (list(map(int, float)),)
return (int(float),)
""" def generic_4frame_loop(
frames,
clear_cache_after_n_frames,
multiplier: typing.SupportsInt,
return_middle_frame_function,
*return_middle_frame_function_args,
interpolation_states: InterpolationStateList = None,
use_timestep=False):
if use_timestep: raise NotImplementedError("Timestep 4 frame VFI model")
def non_timestep_inference(frame_0, frame_1, frame_2, frame_3, n):
middle = return_middle_frame_function(frame_0, frame_1, None, *return_middle_frame_function_args)
if n == 1:
return [middle]
first_half = non_timestep_inference(frame_0, middle, n=n//2)
second_half = non_timestep_inference(middle, frame_1, n=n//2)
if n%2:
return [*first_half, middle, *second_half]
else:
return [*first_half, *second_half] """
@@ -0,0 +1,11 @@
@echo off
echo Installing Taichi lang backend...
if exist "%python_exec%" (
%python_exec% -s -m pip install taichi
) else (
echo Installing with system Python
pip install taichi
)
pause
@@ -0,0 +1,16 @@
@echo off
set "requirements_txt=%~dp0\requirements-no-cupy.txt"
set "python_exec=..\..\..\python_embeded\python.exe"
echo Installing ComfyUI Frame Interpolation..
if exist "%python_exec%" (
echo Installing with ComfyUI Portable
%python_exec% -s install.py
) else (
echo Installing with system Python
python install.py
)
pause
@@ -0,0 +1,59 @@
import os
from pathlib import Path
import sys
import platform
def get_cuda_ver_from_dir(cuda_home):
nvrtc = filter(lambda lib_file: "nvrtc-builtins" in lib_file, os.listdir(cuda_home))
nvrtc = list(nvrtc)
if len(nvrtc) == 0:
return
nvrtc = nvrtc[0]
if ('102' in nvrtc) or ('10.2' in nvrtc):
return '102'
if '110' in nvrtc or ('11.0' in nvrtc):
return '110'
if '111' in nvrtc or ('11.1' in nvrtc):
return '111'
if '11' in nvrtc:
return '11x'
if '12' in nvrtc:
return '12x'
s_param = '-s' if "python_embeded" in sys.executable else ''
def get_cuda_home_path():
if "CUDA_HOME" in os.environ:
return os.environ["CUDA_HOME"]
import torch
torch_lib_path = Path(torch.__file__).parent / "lib"
torch_lib_path = str(torch_lib_path.resolve())
if os.path.exists(torch_lib_path):
nvrtc = filter(lambda lib_file: "nvrtc-builtins" in lib_file, os.listdir(torch_lib_path))
nvrtc = list(nvrtc)
return torch_lib_path if len(nvrtc) > 0 else None
def install_cupy():
cuda_home = get_cuda_home_path()
try:
if cuda_home is not None:
os.environ["CUDA_HOME"] = cuda_home
os.environ["CUDA_PATH"] = cuda_home
import cupy
print("CuPy is already installed.")
except:
print("Uninstall cupy if existed...")
os.system(f'"{sys.executable}" {s_param} -m pip uninstall -y cupy-wheel cupy-cuda102 cupy-cuda110 cupy-cuda111 cupy-cuda11x cupy-cuda12x')
print("Installing cupy...")
cuda_ver = get_cuda_ver_from_dir(cuda_home)
cupy_package = f"cupy-cuda{cuda_ver}" if cuda_ver is not None else "cupy-wheel"
os.system(f'"{sys.executable}" {s_param} -m pip install {cupy_package}')
with open(Path(__file__).parent / "requirements-no-cupy.txt", 'r') as f:
for package in f.readlines():
package = package.strip()
print(f"Installing {package}...")
os.system(f'"{sys.executable}" {s_param} -m pip install {package}')
print("Checking cupy...")
install_cupy()
Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

@@ -0,0 +1,88 @@
import latent_preview
import comfy
import einops
import torch
def common_ksampler(model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=1.0, disable_noise=False, start_step=None, last_step=None, force_full_denoise=False):
device = comfy.model_management.get_torch_device()
latent_image = latent["samples"]
if disable_noise:
noise = torch.zeros(latent_image.size(), dtype=latent_image.dtype, layout=latent_image.layout, device="cpu")
else:
batch_inds = latent["batch_index"] if "batch_index" in latent else None
noise = comfy.sample.prepare_noise(latent_image, seed, batch_inds)
noise_mask = None
if "noise_mask" in latent:
noise_mask = latent["noise_mask"]
preview_format = "JPEG"
if preview_format not in ["JPEG", "PNG"]:
preview_format = "JPEG"
previewer = latent_preview.get_previewer(device, model.model.latent_format)
pbar = comfy.utils.ProgressBar(steps)
def callback(step, x0, x, total_steps):
preview_bytes = None
if previewer:
preview_bytes = previewer.decode_latent_to_preview_image(preview_format, x0)
pbar.update_absolute(step + 1, total_steps, preview_bytes)
samples = comfy.sample.sample(model, noise, steps, cfg, sampler_name, scheduler, positive, negative, latent_image,
denoise=denoise, disable_noise=disable_noise, start_step=start_step, last_step=last_step,
force_full_denoise=force_full_denoise, noise_mask=noise_mask, callback=callback, seed=seed)
out = latent.copy()
out["samples"] = samples
return (out, )
class Gradually_More_Denoise_KSampler:
@classmethod
def INPUT_TYPES(s):
return {"required":
{"model": ("MODEL",),
"positive": ("CONDITIONING", ),
"negative": ("CONDITIONING", ),
"latent_image": ("LATENT", ),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0}),
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, ),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
"start_denoise": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"denoise_increment": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0, "step": 0.1}),
"denoise_increment_steps": ("INT", {"default": 20, "min": 1, "max": 10000})
},
"optional": { "optional_vae": ("VAE",) }
}
RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "LATENT", "VAE", )
RETURN_NAMES = ("MODEL", "CONDITIONING+", "CONDITIONING-", "LATENT", "VAE", )
OUTPUT_NODE = True
FUNCTION = "sample"
CATEGORY = "ComfyUI-Frame-Interpolation/others"
def sample(self, model, positive, negative, latent_image, optional_vae,
seed, steps, cfg, sampler_name, scheduler,start_denoise, denoise_increment, denoise_increment_steps):
if start_denoise + denoise_increment * denoise_increment_steps > 1.0:
raise Exception(f"Max denoise strength can't over 1.0 (start_denoise={start_denoise}, denoise_increment={denoise_increment}, denoise_increment_steps={denoise_increment_steps}")
copied_latent = latent_image.copy()
out_samples = []
for latent_sample in copied_latent["samples"]:
latent = {"samples": einops.rearrange(latent_sample, "c h w -> 1 c h w")}
#Latent's shape is NCHW
gradually_denoising_samples = [
common_ksampler(
model, seed, steps, cfg, sampler_name, scheduler, positive, negative, latent, denoise=start_denoise + denoise_increment * i
)[0]["samples"]
for i in range(denoise_increment_steps)
]
out_samples.extend(gradually_denoising_samples)
copied_latent["samples"] = torch.cat(out_samples, dim=0)
return (model, positive, negative, copied_latent, optional_vae)
@@ -0,0 +1,9 @@
torch
numpy
einops
opencv-contrib-python
kornia
scipy
Pillow
torchvision
tqdm
@@ -0,0 +1,10 @@
torch
numpy
einops
opencv-contrib-python
kornia
scipy
Pillow
torchvision
tqdm
cupy-wheel
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 MiB

@@ -0,0 +1,113 @@
import torch
from comfy.model_management import get_torch_device, soft_empty_cache
import bisect
import numpy as np
import typing
from import_vfi_utils import InterpolationStateListImport, load_file_from_github_release, preprocess_frames, postprocess_frames
import pathlib
import gc
MODEL_TYPE = pathlib.Path(__file__).parent.name
DEVICE = get_torch_device()
def inference(model, img_batch_1, img_batch_2, inter_frames):
results = [
img_batch_1,
img_batch_2
]
idxes = [0, inter_frames + 1]
remains = list(range(1, inter_frames + 1))
splits = torch.linspace(0, 1, inter_frames + 2)
for _ in range(len(remains)):
starts = splits[idxes[:-1]]
ends = splits[idxes[1:]]
distances = ((splits[None, remains] - starts[:, None]) / (ends[:, None] - starts[:, None]) - .5).abs()
matrix = torch.argmin(distances).item()
start_i, step = np.unravel_index(matrix, distances.shape)
end_i = start_i + 1
x0 = results[start_i].to(DEVICE)
x1 = results[end_i].to(DEVICE)
dt = x0.new_full((1, 1), (splits[remains[step]] - splits[idxes[start_i]])) / (splits[idxes[end_i]] - splits[idxes[start_i]])
with torch.no_grad():
prediction = model(x0, x1, dt)
insert_position = bisect.bisect_left(idxes, remains[step])
idxes.insert(insert_position, remains[step])
results.insert(insert_position, prediction.clamp(0, 1).float())
del remains[step]
return [tensor.flip(0) for tensor in results]
class FILM_VFIImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"ckpt_name": (["film_net_fp32.pt"], ),
"frames": ("IMAGE", ),
"clear_cache_after_n_frames": ("INT", {"default": 10, "min": 1, "max": 1000}),
"multiplier": ("INT", {"default": 2, "min": 2, "max": 1000}),
},
"optional": {
"optional_interpolation_states": ("INTERPOLATION_STATES", )
}
}
RETURN_TYPES = ("IMAGE", )
FUNCTION = "vfi"
CATEGORY = "ComfyUI-Frame-Interpolation/VFI"
def vfi(
self,
ckpt_name: typing.AnyStr,
frames: torch.Tensor,
clear_cache_after_n_frames = 10,
multiplier: typing.SupportsInt = 2,
optional_interpolation_states: InterpolationStateListImport = None,
**kwargs
):
interpolation_states = optional_interpolation_states
model_path = load_file_from_github_release(MODEL_TYPE, ckpt_name)
model = torch.jit.load(model_path, map_location='cpu')
model.eval()
model = model.to(DEVICE)
dtype = torch.float32
frames = preprocess_frames(frames)
number_of_frames_processed_since_last_cleared_cuda_cache = 0
output_frames = []
if type(multiplier) == int:
multipliers = [multiplier] * len(frames)
else:
multipliers = list(map(int, multiplier))
multipliers += [2] * (len(frames) - len(multipliers) - 1)
for frame_itr in range(len(frames) - 1): # Skip the final frame since there are no frames after it
if interpolation_states is not None and interpolation_states.is_frame_skipped(frame_itr):
continue
#Ensure that input frames are in fp32 - the same dtype as model
frame_0 = frames[frame_itr:frame_itr+1].to(DEVICE).float()
frame_1 = frames[frame_itr+1:frame_itr+2].to(DEVICE).float()
relust = inference(model, frame_0, frame_1, multipliers[frame_itr] - 1)
output_frames.extend([frame.detach().cpu().to(dtype=dtype) for frame in relust[:-1]])
number_of_frames_processed_since_last_cleared_cuda_cache += 1
# Try to avoid a memory overflow by clearing cuda cache regularly
if number_of_frames_processed_since_last_cleared_cuda_cache >= clear_cache_after_n_frames:
print("Comfy-VFI: Clearing cache...", end = ' ')
soft_empty_cache()
number_of_frames_processed_since_last_cleared_cuda_cache = 0
print("Done cache clearing")
gc.collect()
output_frames.append(frames[-1:].to(dtype=dtype)) # Append final frame
output_frames = [frame.cpu() for frame in output_frames] #Ensure all frames are in cpu
out = torch.cat(output_frames, dim=0)
# clear cache for courtesy
print("Comfy-VFI: Final clearing cache...", end = ' ')
soft_empty_cache()
print("Done cache clearing")
return (postprocess_frames(out), )
@@ -0,0 +1,788 @@
"""
https://github.com/dajes/frame-interpolation-pytorch/blob/main/feature_extractor.py
https://github.com/dajes/frame-interpolation-pytorch/blob/main/fusion.py
https://github.com/dajes/frame-interpolation-pytorch/blob/main/interpolator.py
https://github.com/dajes/frame-interpolation-pytorch/blob/main/pyramid_flow_estimator.py
https://github.com/dajes/frame-interpolation-pytorch/blob/main/util.py
"""
"""PyTorch layer for extracting image features for the film_net interpolator.
The feature extractor implemented here converts an image pyramid into a pyramid
of deep features. The feature pyramid serves a similar purpose as U-Net
architecture's encoder, but we use a special cascaded architecture described in
Multi-view Image Fusion [1].
For comprehensiveness, below is a short description of the idea. While the
description is a bit involved, the cascaded feature pyramid can be used just
like any image feature pyramid.
Why cascaded architeture?
=========================
To understand the concept it is worth reviewing a traditional feature pyramid
first: *A traditional feature pyramid* as in U-net or in many optical flow
networks is built by alternating between convolutions and pooling, starting
from the input image.
It is well known that early features of such architecture correspond to low
level concepts such as edges in the image whereas later layers extract
semantically higher level concepts such as object classes etc. In other words,
the meaning of the filters in each resolution level is different. For problems
such as semantic segmentation and many others this is a desirable property.
However, the asymmetric features preclude sharing weights across resolution
levels in the feature extractor itself and in any subsequent neural networks
that follow. This can be a downside, since optical flow prediction, for
instance is symmetric across resolution levels. The cascaded feature
architecture addresses this shortcoming.
How is it built?
================
The *cascaded* feature pyramid contains feature vectors that have constant
length and meaning on each resolution level, except few of the finest ones. The
advantage of this is that the subsequent optical flow layer can learn
synergically from many resolutions. This means that coarse level prediction can
benefit from finer resolution training examples, which can be useful with
moderately sized datasets to avoid overfitting.
The cascaded feature pyramid is built by extracting shallower subtree pyramids,
each one of them similar to the traditional architecture. Each subtree
pyramid S_i is extracted starting from each resolution level:
image resolution 0 -> S_0
image resolution 1 -> S_1
image resolution 2 -> S_2
...
If we denote the features at level j of subtree i as S_i_j, the cascaded pyramid
is constructed by concatenating features as follows (assuming subtree depth=3):
lvl
feat_0 = concat( S_0_0 )
feat_1 = concat( S_1_0 S_0_1 )
feat_2 = concat( S_2_0 S_1_1 S_0_2 )
feat_3 = concat( S_3_0 S_2_1 S_1_2 )
feat_4 = concat( S_4_0 S_3_1 S_2_2 )
feat_5 = concat( S_5_0 S_4_1 S_3_2 )
....
In above, all levels except feat_0 and feat_1 have the same number of features
with similar semantic meaning. This enables training a single optical flow
predictor module shared by levels 2,3,4,5... . For more details and evaluation
see [1].
[1] Multi-view Image Fusion, Trinidad et al. 2019
"""
from typing import List
import torch
from torch import nn
from torch.nn import functional as F
class SubTreeExtractorImport(nn.Module):
"""Extracts a hierarchical set of features from an image.
This is a conventional, hierarchical image feature extractor, that extracts
[k, k*2, k*4... ] filters for the image pyramid where k=options.sub_levels.
Each level is followed by average pooling.
"""
def __init__(self, in_channels=3, channels=64, n_layers=4):
super().__init__()
convs = []
for i in range(n_layers):
convs.append(nn.Sequential(
conv(in_channels, (channels << i), 3),
conv((channels << i), (channels << i), 3)
))
in_channels = channels << i
self.convs = nn.ModuleList(convs)
def forward(self, image: torch.Tensor, n: int) -> List[torch.Tensor]:
"""Extracts a pyramid of features from the image.
Args:
image: TORCH.Tensor with shape BATCH_SIZE x HEIGHT x WIDTH x CHANNELS.
n: number of pyramid levels to extract. This can be less or equal to
options.sub_levels given in the __init__.
Returns:
The pyramid of features, starting from the finest level. Each element
contains the output after the last convolution on the corresponding
pyramid level.
"""
head = image
pyramid = []
for i, layer in enumerate(self.convs):
head = layer(head)
pyramid.append(head)
if i < n - 1:
head = F.avg_pool2d(head, kernel_size=2, stride=2)
return pyramid
class FeatureExtractorImport(nn.Module):
"""Extracts features from an image pyramid using a cascaded architecture.
"""
def __init__(self, in_channels=3, channels=64, sub_levels=4):
super().__init__()
self.extract_sublevels = SubTreeExtractorImport(in_channels, channels, sub_levels)
self.sub_levels = sub_levels
def forward(self, image_pyramid: List[torch.Tensor]) -> List[torch.Tensor]:
"""Extracts a cascaded feature pyramid.
Args:
image_pyramid: Image pyramid as a list, starting from the finest level.
Returns:
A pyramid of cascaded features.
"""
sub_pyramids: List[List[torch.Tensor]] = []
for i in range(len(image_pyramid)):
# At each level of the image pyramid, creates a sub_pyramid of features
# with 'sub_levels' pyramid levels, re-using the same SubTreeExtractor.
# We use the same instance since we want to share the weights.
#
# However, we cap the depth of the sub_pyramid so we don't create features
# that are beyond the coarsest level of the cascaded feature pyramid we
# want to generate.
capped_sub_levels = min(len(image_pyramid) - i, self.sub_levels)
sub_pyramids.append(self.extract_sublevels(image_pyramid[i], capped_sub_levels))
# Below we generate the cascades of features on each level of the feature
# pyramid. Assuming sub_levels=3, The layout of the features will be
# as shown in the example on file documentation above.
feature_pyramid: List[torch.Tensor] = []
for i in range(len(image_pyramid)):
features = sub_pyramids[i][0]
for j in range(1, self.sub_levels):
if j <= i:
features = torch.cat([features, sub_pyramids[i - j][j]], dim=1)
feature_pyramid.append(features)
return feature_pyramid
"""The final fusion stage for the film_net frame interpolator.
The inputs to this module are the warped input images, image features and
flow fields, all aligned to the target frame (often midway point between the
two original inputs). The output is the final image. FILM has no explicit
occlusion handling -- instead using the abovementioned information this module
automatically decides how to best blend the inputs together to produce content
in areas where the pixels can only be borrowed from one of the inputs.
Similarly, this module also decides on how much to blend in each input in case
of fractional timestep that is not at the halfway point. For example, if the two
inputs images are at t=0 and t=1, and we were to synthesize a frame at t=0.1,
it often makes most sense to favor the first input. However, this is not
always the case -- in particular in occluded pixels.
The architecture of the Fusion module follows U-net [1] architecture's decoder
side, e.g. each pyramid level consists of concatenation with upsampled coarser
level output, and two 3x3 convolutions.
The upsampling is implemented as 'resize convolution', e.g. nearest neighbor
upsampling followed by 2x2 convolution as explained in [2]. The classic U-net
uses max-pooling which has a tendency to create checkerboard artifacts.
[1] Ronneberger et al. U-Net: Convolutional Networks for Biomedical Image
Segmentation, 2015, https://arxiv.org/pdf/1505.04597.pdf
[2] https://distill.pub/2016/deconv-checkerboard/
"""
from typing import List
import torch
from torch import nn
from torch.nn import functional as F
_NUMBER_OF_COLOR_CHANNELS = 3
def get_channels_at_level(level, filters):
n_images = 2
channels = _NUMBER_OF_COLOR_CHANNELS
flows = 2
return (sum(filters << i for i in range(level)) + channels + flows) * n_images
class FusionImport(nn.Module):
"""The decoder."""
def __init__(self, n_layers=4, specialized_layers=3, filters=64):
"""
Args:
m: specialized levels
"""
super().__init__()
# The final convolution that outputs RGB:
self.output_conv = nn.Conv2d(filters, 3, kernel_size=1)
# Each item 'convs[i]' will contain the list of convolutions to be applied
# for pyramid level 'i'.
self.convs = nn.ModuleList()
# Create the convolutions. Roughly following the feature extractor, we
# double the number of filters when the resolution halves, but only up to
# the specialized_levels, after which we use the same number of filters on
# all levels.
#
# We create the convs in fine-to-coarse order, so that the array index
# for the convs will correspond to our normal indexing (0=finest level).
# in_channels: tuple = (128, 202, 256, 522, 512, 1162, 1930, 2442)
in_channels = get_channels_at_level(n_layers, filters)
increase = 0
for i in range(n_layers)[::-1]:
num_filters = (filters << i) if i < specialized_layers else (filters << specialized_layers)
convs = nn.ModuleList([
conv(in_channels, num_filters, size=2, activation=None),
conv(in_channels + (increase or num_filters), num_filters, size=3),
conv(num_filters, num_filters, size=3)]
)
self.convs.append(convs)
in_channels = num_filters
increase = get_channels_at_level(i, filters) - num_filters // 2
def forward(self, pyramid: List[torch.Tensor]) -> torch.Tensor:
"""Runs the fusion module.
Args:
pyramid: The input feature pyramid as list of tensors. Each tensor being
in (B x H x W x C) format, with finest level tensor first.
Returns:
A batch of RGB images.
Raises:
ValueError, if len(pyramid) != config.fusion_pyramid_levels as provided in
the constructor.
"""
# As a slight difference to a conventional decoder (e.g. U-net), we don't
# apply any extra convolutions to the coarsest level, but just pass it
# to finer levels for concatenation. This choice has not been thoroughly
# evaluated, but is motivated by the educated guess that the fusion part
# probably does not need large spatial context, because at this point the
# features are spatially aligned by the preceding warp.
net = pyramid[-1]
# Loop starting from the 2nd coarsest level:
# for i in reversed(range(0, len(pyramid) - 1)):
for k, layers in enumerate(self.convs):
i = len(self.convs) - 1 - k
# Resize the tensor from coarser level to match for concatenation.
level_size = pyramid[i].shape[2:4]
net = F.interpolate(net, size=level_size, mode='nearest')
net = layers[0](net)
net = torch.cat([pyramid[i], net], dim=1)
net = layers[1](net)
net = layers[2](net)
net = self.output_conv(net)
return net
"""The film_net frame interpolator main model code.
Basics
======
The film_net is an end-to-end learned neural frame interpolator implemented as
a PyTorch model. It has the following inputs and outputs:
Inputs:
x0: image A.
x1: image B.
time: desired sub-frame time.
Outputs:
image: the predicted in-between image at the chosen time in range [0, 1].
Additional outputs include forward and backward warped image pyramids, flow
pyramids, etc., that can be visualized for debugging and analysis.
Note that many training sets only contain triplets with ground truth at
time=0.5. If a model has been trained with such training set, it will only work
well for synthesizing frames at time=0.5. Such models can only generate more
in-between frames using recursion.
Architecture
============
The inference consists of three main stages: 1) feature extraction 2) warping
3) fusion. On high-level, the architecture has similarities to Context-aware
Synthesis for Video Frame Interpolation [1], but the exact architecture is
closer to Multi-view Image Fusion [2] with some modifications for the frame
interpolation use-case.
Feature extraction stage employs the cascaded multi-scale architecture described
in [2]. The advantage of this architecture is that coarse level flow prediction
can be learned from finer resolution image samples. This is especially useful
to avoid overfitting with moderately sized datasets.
The warping stage uses a residual flow prediction idea that is similar to
PWC-Net [3], Multi-view Image Fusion [2] and many others.
The fusion stage is similar to U-Net's decoder where the skip connections are
connected to warped image and feature pyramids. This is described in [2].
Implementation Conventions
====================
Pyramids
--------
Throughtout the model, all image and feature pyramids are stored as python lists
with finest level first followed by downscaled versions obtained by successively
halving the resolution. The depths of all pyramids are determined by
options.pyramid_levels. The only exception to this is internal to the feature
extractor, where smaller feature pyramids are temporarily constructed with depth
options.sub_levels.
Color ranges & gamma
--------------------
The model code makes no assumptions on whether the images are in gamma or
linearized space or what is the range of RGB color values. So a model can be
trained with different choices. This does not mean that all the choices lead to
similar results. In practice the model has been proven to work well with RGB
scale = [0,1] with gamma-space images (i.e. not linearized).
[1] Context-aware Synthesis for Video Frame Interpolation, Niklaus and Liu, 2018
[2] Multi-view Image Fusion, Trinidad et al, 2019
[3] PWC-Net: CNNs for Optical Flow Using Pyramid, Warping, and Cost Volume
"""
from typing import Dict, List
import torch
from torch import nn
class InterpolatorImport(nn.Module):
def __init__(
self,
pyramid_levels=7,
fusion_pyramid_levels=5,
specialized_levels=3,
sub_levels=4,
filters=64,
flow_convs=(3, 3, 3, 3),
flow_filters=(32, 64, 128, 256),
):
super().__init__()
self.pyramid_levels = pyramid_levels
self.fusion_pyramid_levels = fusion_pyramid_levels
self.extract = FeatureExtractorImport(3, filters, sub_levels)
self.predict_flow = PyramidFlowEstimatorImport(filters, flow_convs, flow_filters)
self.fuse = FusionImport(sub_levels, specialized_levels, filters)
def shuffle_images(self, x0, x1):
return [
build_image_pyramid(x0, self.pyramid_levels),
build_image_pyramid(x1, self.pyramid_levels)
]
def debug_forward(self, x0, x1, batch_dt) -> Dict[str, List[torch.Tensor]]:
image_pyramids = self.shuffle_images(x0, x1)
# Siamese feature pyramids:
feature_pyramids = [self.extract(image_pyramids[0]), self.extract(image_pyramids[1])]
# Predict forward flow.
forward_residual_flow_pyramid = self.predict_flow(feature_pyramids[0], feature_pyramids[1])
# Predict backward flow.
backward_residual_flow_pyramid = self.predict_flow(feature_pyramids[1], feature_pyramids[0])
# Concatenate features and images:
# Note that we keep up to 'fusion_pyramid_levels' levels as only those
# are used by the fusion module.
forward_flow_pyramid = flow_pyramid_synthesis(forward_residual_flow_pyramid)[:self.fusion_pyramid_levels]
backward_flow_pyramid = flow_pyramid_synthesis(backward_residual_flow_pyramid)[:self.fusion_pyramid_levels]
# We multiply the flows with t and 1-t to warp to the desired fractional time.
#
# Note: In film_net we fix time to be 0.5, and recursively invoke the interpo-
# lator for multi-frame interpolation. Below, we create a constant tensor of
# shape [B]. We use the `time` tensor to infer the batch size.
mid_time = torch.full_like(batch_dt, .5)
backward_flow = multiply_pyramid(backward_flow_pyramid, mid_time[:, 0])
forward_flow = multiply_pyramid(forward_flow_pyramid, 1 - mid_time[:, 0])
pyramids_to_warp = [
concatenate_pyramids(image_pyramids[0][:self.fusion_pyramid_levels],
feature_pyramids[0][:self.fusion_pyramid_levels]),
concatenate_pyramids(image_pyramids[1][:self.fusion_pyramid_levels],
feature_pyramids[1][:self.fusion_pyramid_levels])
]
# Warp features and images using the flow. Note that we use backward warping
# and backward flow is used to read from image 0 and forward flow from
# image 1.
forward_warped_pyramid = pyramid_warp(pyramids_to_warp[0], backward_flow)
backward_warped_pyramid = pyramid_warp(pyramids_to_warp[1], forward_flow)
aligned_pyramid = concatenate_pyramids(forward_warped_pyramid,
backward_warped_pyramid)
aligned_pyramid = concatenate_pyramids(aligned_pyramid, backward_flow)
aligned_pyramid = concatenate_pyramids(aligned_pyramid, forward_flow)
return {
'image': [self.fuse(aligned_pyramid)],
'forward_residual_flow_pyramid': forward_residual_flow_pyramid,
'backward_residual_flow_pyramid': backward_residual_flow_pyramid,
'forward_flow_pyramid': forward_flow_pyramid,
'backward_flow_pyramid': backward_flow_pyramid,
}
def forward(self, x0, x1, batch_dt) -> torch.Tensor:
return self.debug_forward(x0, x1, batch_dt)['image'][0]
"""PyTorch layer for estimating optical flow by a residual flow pyramid.
This approach of estimating optical flow between two images can be traced back
to [1], but is also used by later neural optical flow computation methods such
as SpyNet [2] and PWC-Net [3].
The basic idea is that the optical flow is first estimated in a coarse
resolution, then the flow is upsampled to warp the higher resolution image and
then a residual correction is computed and added to the estimated flow. This
process is repeated in a pyramid on coarse to fine order to successively
increase the resolution of both optical flow and the warped image.
In here, the optical flow predictor is used as an internal component for the
film_net frame interpolator, to warp the two input images into the inbetween,
target frame.
[1] F. Glazer, Hierarchical motion detection. PhD thesis, 1987.
[2] A. Ranjan and M. J. Black, Optical Flow Estimation using a Spatial Pyramid
Network. 2016
[3] D. Sun X. Yang, M-Y. Liu and J. Kautz, PWC-Net: CNNs for Optical Flow Using
Pyramid, Warping, and Cost Volume, 2017
"""
from typing import List
import torch
from torch import nn
from torch.nn import functional as F
class FlowEstimatorImport(nn.Module):
"""Small-receptive field predictor for computing the flow between two images.
This is used to compute the residual flow fields in PyramidFlowEstimator.
Note that while the number of 3x3 convolutions & filters to apply is
configurable, two extra 1x1 convolutions are appended to extract the flow in
the end.
Attributes:
name: The name of the layer
num_convs: Number of 3x3 convolutions to apply
num_filters: Number of filters in each 3x3 convolution
"""
def __init__(self, in_channels: int, num_convs: int, num_filters: int):
super(FlowEstimatorImport, self).__init__()
self._convs = nn.ModuleList()
for i in range(num_convs):
self._convs.append(conv(in_channels=in_channels, out_channels=num_filters, size=3))
in_channels = num_filters
self._convs.append(conv(in_channels, num_filters // 2, size=1))
in_channels = num_filters // 2
# For the final convolution, we want no activation at all to predict the
# optical flow vector values. We have done extensive testing on explicitly
# bounding these values using sigmoid, but it turned out that having no
# activation gives better results.
self._convs.append(conv(in_channels, 2, size=1, activation=None))
def forward(self, features_a: torch.Tensor, features_b: torch.Tensor) -> torch.Tensor:
"""Estimates optical flow between two images.
Args:
features_a: per pixel feature vectors for image A (B x H x W x C)
features_b: per pixel feature vectors for image B (B x H x W x C)
Returns:
A tensor with optical flow from A to B
"""
net = torch.cat([features_a, features_b], dim=1)
for conv in self._convs:
net = conv(net)
return net
class PyramidFlowEstimatorImport(nn.Module):
"""Predicts optical flow by coarse-to-fine refinement.
"""
def __init__(self, filters: int = 64,
flow_convs: tuple = (3, 3, 3, 3),
flow_filters: tuple = (32, 64, 128, 256)):
super(PyramidFlowEstimatorImport, self).__init__()
in_channels = filters << 1
predictors = []
for i in range(len(flow_convs)):
predictors.append(
FlowEstimatorImport(
in_channels=in_channels,
num_convs=flow_convs[i],
num_filters=flow_filters[i]))
in_channels += filters << (i + 2)
self._predictor = predictors[-1]
self._predictors = nn.ModuleList(predictors[:-1][::-1])
def forward(self, feature_pyramid_a: List[torch.Tensor],
feature_pyramid_b: List[torch.Tensor]) -> List[torch.Tensor]:
"""Estimates residual flow pyramids between two image pyramids.
Each image pyramid is represented as a list of tensors in fine-to-coarse
order. Each individual image is represented as a tensor where each pixel is
a vector of image features.
flow_pyramid_synthesis can be used to convert the residual flow
pyramid returned by this method into a flow pyramid, where each level
encodes the flow instead of a residual correction.
Args:
feature_pyramid_a: image pyramid as a list in fine-to-coarse order
feature_pyramid_b: image pyramid as a list in fine-to-coarse order
Returns:
List of flow tensors, in fine-to-coarse order, each level encoding the
difference against the bilinearly upsampled version from the coarser
level. The coarsest flow tensor, e.g. the last element in the array is the
'DC-term', e.g. not a residual (alternatively you can think of it being a
residual against zero).
"""
levels = len(feature_pyramid_a)
v = self._predictor(feature_pyramid_a[-1], feature_pyramid_b[-1])
residuals = [v]
for i in range(levels - 2, len(self._predictors) - 1, -1):
# Upsamples the flow to match the current pyramid level. Also, scales the
# magnitude by two to reflect the new size.
level_size = feature_pyramid_a[i].shape[2:4]
v = F.interpolate(2 * v, size=level_size, mode='bilinear')
# Warp feature_pyramid_b[i] image based on the current flow estimate.
warped = warp(feature_pyramid_b[i], v)
# Estimate the residual flow between pyramid_a[i] and warped image:
v_residual = self._predictor(feature_pyramid_a[i], warped)
residuals.insert(0, v_residual)
v = v_residual + v
for k, predictor in enumerate(self._predictors):
i = len(self._predictors) - 1 - k
# Upsamples the flow to match the current pyramid level. Also, scales the
# magnitude by two to reflect the new size.
level_size = feature_pyramid_a[i].shape[2:4]
v = F.interpolate(2 * v, size=level_size, mode='bilinear')
# Warp feature_pyramid_b[i] image based on the current flow estimate.
warped = warp(feature_pyramid_b[i], v)
# Estimate the residual flow between pyramid_a[i] and warped image:
v_residual = predictor(feature_pyramid_a[i], warped)
residuals.insert(0, v_residual)
v = v_residual + v
return residuals
"""Various utilities used in the film_net frame interpolator model."""
from typing import List, Optional
import cv2
import numpy as np
import torch
from torch import nn
from torch.nn import functional as F
def pad_batch(batch, align):
height, width = batch.shape[1:3]
height_to_pad = (align - height % align) if height % align != 0 else 0
width_to_pad = (align - width % align) if width % align != 0 else 0
crop_region = [height_to_pad >> 1, width_to_pad >> 1, height + (height_to_pad >> 1), width + (width_to_pad >> 1)]
batch = np.pad(batch, ((0, 0), (height_to_pad >> 1, height_to_pad - (height_to_pad >> 1)),
(width_to_pad >> 1, width_to_pad - (width_to_pad >> 1)), (0, 0)), mode='constant')
return batch, crop_region
def load_image(path, align=64):
image = cv2.cvtColor(cv2.imread(path), cv2.COLOR_BGR2RGB).astype(np.float32) / np.float32(255)
image_batch, crop_region = pad_batch(np.expand_dims(image, axis=0), align)
return image_batch, crop_region
def build_image_pyramid(image: torch.Tensor, pyramid_levels: int = 3) -> List[torch.Tensor]:
"""Builds an image pyramid from a given image.
The original image is included in the pyramid and the rest are generated by
successively halving the resolution.
Args:
image: the input image.
options: film_net options object
Returns:
A list of images starting from the finest with options.pyramid_levels items
"""
pyramid = []
for i in range(pyramid_levels):
pyramid.append(image)
if i < pyramid_levels - 1:
image = F.avg_pool2d(image, 2, 2)
return pyramid
def warp(image: torch.Tensor, flow: torch.Tensor) -> torch.Tensor:
"""Backward warps the image using the given flow.
Specifically, the output pixel in batch b, at position x, y will be computed
as follows:
(flowed_y, flowed_x) = (y+flow[b, y, x, 1], x+flow[b, y, x, 0])
output[b, y, x] = bilinear_lookup(image, b, flowed_y, flowed_x)
Note that the flow vectors are expected as [x, y], e.g. x in position 0 and
y in position 1.
Args:
image: An image with shape BxHxWxC.
flow: A flow with shape BxHxWx2, with the two channels denoting the relative
offset in order: (dx, dy).
Returns:
A warped image.
"""
flow = -flow.flip(1)
dtype = flow.dtype
device = flow.device
# warped = tfa_image.dense_image_warp(image, flow)
# Same as above but with pytorch
ls1 = 1 - 1 / flow.shape[3]
ls2 = 1 - 1 / flow.shape[2]
normalized_flow2 = flow.permute(0, 2, 3, 1) / torch.tensor(
[flow.shape[2] * .5, flow.shape[3] * .5], dtype=dtype, device=device)[None, None, None]
normalized_flow2 = torch.stack([
torch.linspace(-ls1, ls1, flow.shape[3], dtype=dtype, device=device)[None, None, :] - normalized_flow2[..., 1],
torch.linspace(-ls2, ls2, flow.shape[2], dtype=dtype, device=device)[None, :, None] - normalized_flow2[..., 0],
], dim=3)
warped = F.grid_sample(image, normalized_flow2,
mode='bilinear', padding_mode='border', align_corners=False)
return warped.reshape(image.shape)
def multiply_pyramid(pyramid: List[torch.Tensor],
scalar: torch.Tensor) -> List[torch.Tensor]:
"""Multiplies all image batches in the pyramid by a batch of scalars.
Args:
pyramid: Pyramid of image batches.
scalar: Batch of scalars.
Returns:
An image pyramid with all images multiplied by the scalar.
"""
# To multiply each image with its corresponding scalar, we first transpose
# the batch of images from BxHxWxC-format to CxHxWxB. This can then be
# multiplied with a batch of scalars, then we transpose back to the standard
# BxHxWxC form.
return [image * scalar for image in pyramid]
def flow_pyramid_synthesis(
residual_pyramid: List[torch.Tensor]) -> List[torch.Tensor]:
"""Converts a residual flow pyramid into a flow pyramid."""
flow = residual_pyramid[-1]
flow_pyramid: List[torch.Tensor] = [flow]
for residual_flow in residual_pyramid[:-1][::-1]:
level_size = residual_flow.shape[2:4]
flow = F.interpolate(2 * flow, size=level_size, mode='bilinear')
flow = residual_flow + flow
flow_pyramid.insert(0, flow)
return flow_pyramid
def pyramid_warp(feature_pyramid: List[torch.Tensor],
flow_pyramid: List[torch.Tensor]) -> List[torch.Tensor]:
"""Warps the feature pyramid using the flow pyramid.
Args:
feature_pyramid: feature pyramid starting from the finest level.
flow_pyramid: flow fields, starting from the finest level.
Returns:
Reverse warped feature pyramid.
"""
warped_feature_pyramid = []
for features, flow in zip(feature_pyramid, flow_pyramid):
warped_feature_pyramid.append(warp(features, flow))
return warped_feature_pyramid
def concatenate_pyramids(pyramid1: List[torch.Tensor],
pyramid2: List[torch.Tensor]) -> List[torch.Tensor]:
"""Concatenates each pyramid level together in the channel dimension."""
result = []
for features1, features2 in zip(pyramid1, pyramid2):
result.append(torch.cat([features1, features2], dim=1))
return result
def conv(in_channels, out_channels, size, activation: Optional[str] = 'relu'):
# Since PyTorch doesn't have an in-built activation in Conv2d, we use a
# Sequential layer to combine Conv2d and Leaky ReLU in one module.
_conv = nn.Conv2d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=size,
padding='same')
if activation is None:
return _conv
assert activation == 'relu'
return nn.Sequential(
_conv,
nn.LeakyReLU(.2)
)
+4
View File
@@ -0,0 +1,4 @@
# These are supported funding model platforms
github: cubiq
custom: ['https://www.paypal.com/paypalme/matt3o']
@@ -0,0 +1,4 @@
/__pycache__/
/models/*.bin
/models/*.safetensors
.directory
@@ -0,0 +1,395 @@
import torch
import math
import torch.nn.functional as F
from comfy.ldm.modules.attention import optimized_attention
from .utils import tensor_to_size
class Attn2ReplaceImport:
def __init__(self, callback=None, **kwargs):
self.callback = [callback]
self.kwargs = [kwargs]
def add(self, callback, **kwargs):
self.callback.append(callback)
self.kwargs.append(kwargs)
for key, value in kwargs.items():
setattr(self, key, value)
def __call__(self, q, k, v, extra_options):
dtype = q.dtype
out = optimized_attention(q, k, v, extra_options["n_heads"])
sigma = extra_options["sigmas"].detach().cpu()[0].item() if 'sigmas' in extra_options else 999999999.9
for i, callback in enumerate(self.callback):
if sigma <= self.kwargs[i]["sigma_start"] and sigma >= self.kwargs[i]["sigma_end"]:
out = out + callback(out, q, k, v, extra_options, **self.kwargs[i])
return out.to(dtype=dtype)
def ipadapter_attention_import(out, q, k, v, extra_options, module_key='', ipadapter=None, weight=1.0, cond=None, cond_alt=None, uncond=None, weight_type="linear", mask=None, sigma_start=0.0, sigma_end=1.0, unfold_batch=False, image_schedule=None, embeds_scaling='V only', **kwargs):
dtype = q.dtype
cond_or_uncond = extra_options["cond_or_uncond"]
block_type = extra_options["block"][0]
#block_id = extra_options["block"][1]
t_idx = extra_options["transformer_index"]
layers = 11 if '101_to_k_ip' in ipadapter.ip_layers.to_kvs else 16
k_key = module_key + "_to_k_ip"
v_key = module_key + "_to_v_ip"
# extra options for AnimateDiff
ad_params = extra_options['ad_params'] if "ad_params" in extra_options else None
b = q.shape[0]
seq_len = q.shape[1]
batch_prompt = b // len(cond_or_uncond)
_, _, oh, ow = extra_options["original_shape"]
if weight_type == 'ease in':
weight = weight * (0.05 + 0.95 * (1 - t_idx / layers))
elif weight_type == 'ease out':
weight = weight * (0.05 + 0.95 * (t_idx / layers))
elif weight_type == 'ease in-out':
weight = weight * (0.05 + 0.95 * (1 - abs(t_idx - (layers/2)) / (layers/2)))
elif weight_type == 'reverse in-out':
weight = weight * (0.05 + 0.95 * (abs(t_idx - (layers/2)) / (layers/2)))
elif weight_type == 'weak input' and block_type == 'input':
weight = weight * 0.2
elif weight_type == 'weak middle' and block_type == 'middle':
weight = weight * 0.2
elif weight_type == 'weak output' and block_type == 'output':
weight = weight * 0.2
elif weight_type == 'strong middle' and (block_type == 'input' or block_type == 'output'):
weight = weight * 0.2
elif isinstance(weight, dict):
if t_idx not in weight:
return 0
weight = weight[t_idx]
if cond_alt is not None and t_idx in cond_alt:
cond = cond_alt[t_idx]
del cond_alt
if unfold_batch:
# Check AnimateDiff context window
if ad_params is not None and ad_params["sub_idxs"] is not None:
if isinstance(weight, torch.Tensor):
weight = tensor_to_size(weight, ad_params["full_length"])
weight = torch.Tensor(weight[ad_params["sub_idxs"]])
if torch.all(weight == 0):
return 0
weight = weight.repeat(len(cond_or_uncond), 1, 1) # repeat for cond and uncond
elif weight == 0:
return 0
if image_schedule is not None:
# Use the image_schedule as a lookup table to get the embedded image corresponding to each sub_idx
# If image_schedule isn't long enough then use the last image
cond_idxs = [image_schedule[i if i < len(image_schedule) else -1] for i in ad_params["sub_idxs"]]
cond = torch.Tensor(cond[cond_idxs])
uncond = torch.Tensor(uncond[cond_idxs])
else:
# if image length matches or exceeds full_length get sub_idx images
if cond.shape[0] >= ad_params["full_length"]:
cond = torch.Tensor(cond[ad_params["sub_idxs"]])
uncond = torch.Tensor(uncond[ad_params["sub_idxs"]])
# otherwise get sub_idxs images
else:
cond = tensor_to_size(cond, ad_params["full_length"])
uncond = tensor_to_size(uncond, ad_params["full_length"])
cond = cond[ad_params["sub_idxs"]]
uncond = uncond[ad_params["sub_idxs"]]
else:
if isinstance(weight, torch.Tensor):
weight = tensor_to_size(weight, batch_prompt)
if torch.all(weight == 0):
return 0
weight = weight.repeat(len(cond_or_uncond), 1, 1) # repeat for cond and uncond
elif weight == 0:
return 0
cond = tensor_to_size(cond, batch_prompt)
uncond = tensor_to_size(uncond, batch_prompt)
k_cond = ipadapter.ip_layers.to_kvs[k_key](cond)
k_uncond = ipadapter.ip_layers.to_kvs[k_key](uncond)
v_cond = ipadapter.ip_layers.to_kvs[v_key](cond)
v_uncond = ipadapter.ip_layers.to_kvs[v_key](uncond)
else:
# TODO: should we always convert the weights to a tensor?
if isinstance(weight, torch.Tensor):
weight = tensor_to_size(weight, batch_prompt)
if torch.all(weight == 0):
return 0
weight = weight.repeat(len(cond_or_uncond), 1, 1) # repeat for cond and uncond
elif weight == 0:
return 0
k_cond = ipadapter.ip_layers.to_kvs[k_key](cond).repeat(batch_prompt, 1, 1)
k_uncond = ipadapter.ip_layers.to_kvs[k_key](uncond).repeat(batch_prompt, 1, 1)
v_cond = ipadapter.ip_layers.to_kvs[v_key](cond).repeat(batch_prompt, 1, 1)
v_uncond = ipadapter.ip_layers.to_kvs[v_key](uncond).repeat(batch_prompt, 1, 1)
ip_k = torch.cat([(k_cond, k_uncond)[i] for i in cond_or_uncond], dim=0)
ip_v = torch.cat([(v_cond, v_uncond)[i] for i in cond_or_uncond], dim=0)
if embeds_scaling == 'K+mean(V) w/ C penalty':
scaling = float(ip_k.shape[2]) / 1280.0
weight = weight * scaling
ip_k = ip_k * weight
ip_v_mean = torch.mean(ip_v, dim=1, keepdim=True)
ip_v = (ip_v - ip_v_mean) + ip_v_mean * weight
out_ip = optimized_attention(q, ip_k, ip_v, extra_options["n_heads"])
del ip_v_mean
elif embeds_scaling == 'K+V w/ C penalty':
scaling = float(ip_k.shape[2]) / 1280.0
weight = weight * scaling
ip_k = ip_k * weight
ip_v = ip_v * weight
out_ip = optimized_attention(q, ip_k, ip_v, extra_options["n_heads"])
elif embeds_scaling == 'K+V':
ip_k = ip_k * weight
ip_v = ip_v * weight
out_ip = optimized_attention(q, ip_k, ip_v, extra_options["n_heads"])
else:
#ip_v = ip_v * weight
out_ip = optimized_attention(q, ip_k, ip_v, extra_options["n_heads"])
out_ip = out_ip * weight # I'm doing this to get the same results as before
if mask is not None:
mask_h = oh / math.sqrt(oh * ow / seq_len)
mask_h = int(mask_h) + int((seq_len % int(mask_h)) != 0)
mask_w = seq_len // mask_h
# check if using AnimateDiff and sliding context window
if (mask.shape[0] > 1 and ad_params is not None and ad_params["sub_idxs"] is not None):
# if mask length matches or exceeds full_length, get sub_idx masks
if mask.shape[0] >= ad_params["full_length"]:
mask = torch.Tensor(mask[ad_params["sub_idxs"]])
mask = F.interpolate(mask.unsqueeze(1), size=(mask_h, mask_w), mode="bilinear").squeeze(1)
else:
mask = F.interpolate(mask.unsqueeze(1), size=(mask_h, mask_w), mode="bilinear").squeeze(1)
mask = tensor_to_size(mask, ad_params["full_length"])
mask = mask[ad_params["sub_idxs"]]
else:
mask = F.interpolate(mask.unsqueeze(1), size=(mask_h, mask_w), mode="bilinear").squeeze(1)
mask = tensor_to_size(mask, batch_prompt)
mask = mask.repeat(len(cond_or_uncond), 1, 1)
mask = mask.view(mask.shape[0], -1, 1).repeat(1, 1, out.shape[2])
# covers cases where extreme aspect ratios can cause the mask to have a wrong size
mask_len = mask_h * mask_w
if mask_len < seq_len:
pad_len = seq_len - mask_len
pad1 = pad_len // 2
pad2 = pad_len - pad1
mask = F.pad(mask, (0, 0, pad1, pad2), value=0.0)
elif mask_len > seq_len:
crop_start = (mask_len - seq_len) // 2
mask = mask[:, crop_start:crop_start+seq_len, :]
out_ip = out_ip * mask
#out = out + out_ip
return out_ip.to(dtype=dtype)
"""
class CrossAttentionPatch:
# forward for patching
def __init__(self, ipadapter=None, number=0, weight=1.0, cond=None, cond_alt=None, uncond=None, weight_type="linear", mask=None, sigma_start=0.0, sigma_end=1.0, unfold_batch=False, embeds_scaling='V only'):
self.weights = [weight]
self.ipadapters = [ipadapter]
self.conds = [cond]
self.conds_alt = [cond_alt]
self.unconds = [uncond]
self.weight_types = [weight_type]
self.masks = [mask]
self.sigma_starts = [sigma_start]
self.sigma_ends = [sigma_end]
self.unfold_batch = [unfold_batch]
self.embeds_scaling = [embeds_scaling]
self.number = number
self.layers = 11 if '101_to_k_ip' in ipadapter.ip_layers.to_kvs else 16 # TODO: check if this is a valid condition to detect all models
self.k_key = str(self.number*2+1) + "_to_k_ip"
self.v_key = str(self.number*2+1) + "_to_v_ip"
def set_new_condition(self, ipadapter=None, number=0, weight=1.0, cond=None, cond_alt=None, uncond=None, weight_type="linear", mask=None, sigma_start=0.0, sigma_end=1.0, unfold_batch=False, embeds_scaling='V only'):
self.weights.append(weight)
self.ipadapters.append(ipadapter)
self.conds.append(cond)
self.conds_alt.append(cond_alt)
self.unconds.append(uncond)
self.weight_types.append(weight_type)
self.masks.append(mask)
self.sigma_starts.append(sigma_start)
self.sigma_ends.append(sigma_end)
self.unfold_batch.append(unfold_batch)
self.embeds_scaling.append(embeds_scaling)
def __call__(self, q, k, v, extra_options):
dtype = q.dtype
cond_or_uncond = extra_options["cond_or_uncond"]
sigma = extra_options["sigmas"].detach().cpu()[0].item() if 'sigmas' in extra_options else 999999999.9
block_type = extra_options["block"][0]
#block_id = extra_options["block"][1]
t_idx = extra_options["transformer_index"]
# extra options for AnimateDiff
ad_params = extra_options['ad_params'] if "ad_params" in extra_options else None
b = q.shape[0]
seq_len = q.shape[1]
batch_prompt = b // len(cond_or_uncond)
out = optimized_attention(q, k, v, extra_options["n_heads"])
_, _, oh, ow = extra_options["original_shape"]
for weight, cond, cond_alt, uncond, ipadapter, mask, weight_type, sigma_start, sigma_end, unfold_batch, embeds_scaling in zip(self.weights, self.conds, self.conds_alt, self.unconds, self.ipadapters, self.masks, self.weight_types, self.sigma_starts, self.sigma_ends, self.unfold_batch, self.embeds_scaling):
if sigma <= sigma_start and sigma >= sigma_end:
if weight_type == 'ease in':
weight = weight * (0.05 + 0.95 * (1 - t_idx / self.layers))
elif weight_type == 'ease out':
weight = weight * (0.05 + 0.95 * (t_idx / self.layers))
elif weight_type == 'ease in-out':
weight = weight * (0.05 + 0.95 * (1 - abs(t_idx - (self.layers/2)) / (self.layers/2)))
elif weight_type == 'reverse in-out':
weight = weight * (0.05 + 0.95 * (abs(t_idx - (self.layers/2)) / (self.layers/2)))
elif weight_type == 'weak input' and block_type == 'input':
weight = weight * 0.2
elif weight_type == 'weak middle' and block_type == 'middle':
weight = weight * 0.2
elif weight_type == 'weak output' and block_type == 'output':
weight = weight * 0.2
elif weight_type == 'strong middle' and (block_type == 'input' or block_type == 'output'):
weight = weight * 0.2
elif isinstance(weight, dict):
if t_idx not in weight:
continue
weight = weight[t_idx]
if cond_alt is not None and t_idx in cond_alt:
cond = cond_alt[t_idx]
del cond_alt
if unfold_batch:
# Check AnimateDiff context window
if ad_params is not None and ad_params["sub_idxs"] is not None:
if isinstance(weight, torch.Tensor):
weight = tensor_to_size(weight, ad_params["full_length"])
weight = torch.Tensor(weight[ad_params["sub_idxs"]])
if torch.all(weight == 0):
continue
weight = weight.repeat(len(cond_or_uncond), 1, 1) # repeat for cond and uncond
elif weight == 0:
continue
# if image length matches or exceeds full_length get sub_idx images
if cond.shape[0] >= ad_params["full_length"]:
cond = torch.Tensor(cond[ad_params["sub_idxs"]])
uncond = torch.Tensor(uncond[ad_params["sub_idxs"]])
# otherwise get sub_idxs images
else:
cond = tensor_to_size(cond, ad_params["full_length"])
uncond = tensor_to_size(uncond, ad_params["full_length"])
cond = cond[ad_params["sub_idxs"]]
uncond = uncond[ad_params["sub_idxs"]]
else:
if isinstance(weight, torch.Tensor):
weight = tensor_to_size(weight, batch_prompt)
if torch.all(weight == 0):
continue
weight = weight.repeat(len(cond_or_uncond), 1, 1) # repeat for cond and uncond
elif weight == 0:
continue
cond = tensor_to_size(cond, batch_prompt)
uncond = tensor_to_size(uncond, batch_prompt)
k_cond = ipadapter.ip_layers.to_kvs[self.k_key](cond)
k_uncond = ipadapter.ip_layers.to_kvs[self.k_key](uncond)
v_cond = ipadapter.ip_layers.to_kvs[self.v_key](cond)
v_uncond = ipadapter.ip_layers.to_kvs[self.v_key](uncond)
else:
# TODO: should we always convert the weights to a tensor?
if isinstance(weight, torch.Tensor):
weight = tensor_to_size(weight, batch_prompt)
if torch.all(weight == 0):
continue
weight = weight.repeat(len(cond_or_uncond), 1, 1) # repeat for cond and uncond
elif weight == 0:
continue
k_cond = ipadapter.ip_layers.to_kvs[self.k_key](cond).repeat(batch_prompt, 1, 1)
k_uncond = ipadapter.ip_layers.to_kvs[self.k_key](uncond).repeat(batch_prompt, 1, 1)
v_cond = ipadapter.ip_layers.to_kvs[self.v_key](cond).repeat(batch_prompt, 1, 1)
v_uncond = ipadapter.ip_layers.to_kvs[self.v_key](uncond).repeat(batch_prompt, 1, 1)
ip_k = torch.cat([(k_cond, k_uncond)[i] for i in cond_or_uncond], dim=0)
ip_v = torch.cat([(v_cond, v_uncond)[i] for i in cond_or_uncond], dim=0)
if embeds_scaling == 'K+mean(V) w/ C penalty':
scaling = float(ip_k.shape[2]) / 1280.0
weight = weight * scaling
ip_k = ip_k * weight
ip_v_mean = torch.mean(ip_v, dim=1, keepdim=True)
ip_v = (ip_v - ip_v_mean) + ip_v_mean * weight
out_ip = optimized_attention(q, ip_k, ip_v, extra_options["n_heads"])
del ip_v_mean
elif embeds_scaling == 'K+V w/ C penalty':
scaling = float(ip_k.shape[2]) / 1280.0
weight = weight * scaling
ip_k = ip_k * weight
ip_v = ip_v * weight
out_ip = optimized_attention(q, ip_k, ip_v, extra_options["n_heads"])
elif embeds_scaling == 'K+V':
ip_k = ip_k * weight
ip_v = ip_v * weight
out_ip = optimized_attention(q, ip_k, ip_v, extra_options["n_heads"])
else:
#ip_v = ip_v * weight
out_ip = optimized_attention(q, ip_k, ip_v, extra_options["n_heads"])
out_ip = out_ip * weight # I'm doing this to get the same results as before
if mask is not None:
mask_h = oh / math.sqrt(oh * ow / seq_len)
mask_h = int(mask_h) + int((seq_len % int(mask_h)) != 0)
mask_w = seq_len // mask_h
# check if using AnimateDiff and sliding context window
if (mask.shape[0] > 1 and ad_params is not None and ad_params["sub_idxs"] is not None):
# if mask length matches or exceeds full_length, get sub_idx masks
if mask.shape[0] >= ad_params["full_length"]:
mask = torch.Tensor(mask[ad_params["sub_idxs"]])
mask = F.interpolate(mask.unsqueeze(1), size=(mask_h, mask_w), mode="bilinear").squeeze(1)
else:
mask = F.interpolate(mask.unsqueeze(1), size=(mask_h, mask_w), mode="bilinear").squeeze(1)
mask = tensor_to_size(mask, ad_params["full_length"])
mask = mask[ad_params["sub_idxs"]]
else:
mask = F.interpolate(mask.unsqueeze(1), size=(mask_h, mask_w), mode="bilinear").squeeze(1)
mask = tensor_to_size(mask, batch_prompt)
mask = mask.repeat(len(cond_or_uncond), 1, 1)
mask = mask.view(mask.shape[0], -1, 1).repeat(1, 1, out.shape[2])
# covers cases where extreme aspect ratios can cause the mask to have a wrong size
mask_len = mask_h * mask_w
if mask_len < seq_len:
pad_len = seq_len - mask_len
pad1 = pad_len // 2
pad2 = pad_len - pad1
mask = F.pad(mask, (0, 0, pad1, pad2), value=0.0)
elif mask_len > seq_len:
crop_start = (mask_len - seq_len) // 2
mask = mask[:, crop_start:crop_start+seq_len, :]
out_ip = out_ip * mask
out = out + out_ip
return out.to(dtype=dtype)
"""
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
# Nodes reference
Below I'm trying to document all the nodes. It's still very incomplete, be sure to check back later.
## Loaders
### :knot: IPAdapter Unified Loader
Loads the full stack of models needed for IPAdapter to function. The returned object will contain information regarding the **ipadapter** and **clip vision models**.
Multiple unified loaders should always be daisy chained through the `ipadapter` in/out. **Failing to do so will cause all models to be loaded twice.** For **the first** unified loader the `ipadapter` input **should never be connected**.
#### Inputs
- **model**, main ComfyUI model pipeline
#### Optional Inputs
- **ipadapter**, it's important to note that this is optional and used exclusively to daisy chain unified loaders. **The `ipadapter` input is never connected in the first `IPAdapter Unified Loader` of the chain.**
#### Outputs
- **model**, the model pipeline is used exclusively for configuration, the model comes out of this node untouched and it can be considered a reroute. Note that this is different from the Unified Loader FaceID that actually alters the model with a LoRA.
- **ipadapter**, connect this to any ipadater node. Each node will automatically detect if the `ipadapter` object contains the full stack of models or just one (like in the case [IPAdapter Model Loader](#ipadapter-model-loader)).
### :knot: IPAdapter Model Loader
Loads the IPAdapter model only. The returned object will be the IPAdapter model contrary to the [Unified loader](#ipadapter-unified-loader) that contains the full stack of models.
#### Configuration parameters
- **ipadapter_file**, the main IPAdapter model. It must be located into `ComfyUI/models/ipadapter` or in any path specified in the `extra_model_paths.yaml` configuration file.
#### Outputs
- **IPADAPTER**, contains the loaded model only. Note that `IPADAPTER` will have a different structure when loaded by the [Unified Loader](#ipadapter-unified-loader).
## Main IPAdapter Apply Nodes
### :knot: IPAdapter Advanced
This node contains all the options to fine tune the IPAdapter models. It is a drop in replacement for the old `IPAdapter Apply` that is no longer available. If you have an old workflow, delete the existing `IPadapter Apply` node, add `IPAdapter Advanced` and connect all the pipes as before.
#### Inputs
- **model**, main model pipeline.
- **ipadapter**, the IPAdapter model. It can be connected to the [IPAdapter Model Loader](#ipadapter-model-loader) or any of the Unified Loaders. If a Unified loader is used anywhere in the workflow and you don't need a different model, it's always adviced to reuse the previous `ipadapter` pipeline.
- **image**, the reference image used to generate the positive conditioning. It should be a square image, other aspect ratios are automatically cropped in the center.
#### Optional inputs
- **image_negative**, image used to generate the negative conditioning. This is optional and normally handled by the code. It is possible to send noise or actually any image to instruct the model about what we don't want to see in the composition.
- **attn_mask**, a mask that will be applied during the image generation. **The mask should have the same size or at least the same aspect ratio of the latent**. The mask will define the area of influence of the IPAdapter models on the final image. Black zones won't be affected, white zones will get maximum influence. It can be a grayscale mask.
- **clip_vision**, this is optional if using any of the Unified loaders. If using the [IPAdapter Model Loader](#knot-ipadapter-model-loader) you also have to provide the clip vision model with a `Load CLIP Vision` node.
#### Configuration parameters
- **weight**, weight of the IPAdapter model. For `linear` `weight_type` (the default), a good starting point is 0.8. If you use other weight types you can experiment with higher values.
- **weight_type**, this is how the IPAdapter is applied to the UNet block. For example `ease-in` means that the input blocks have higher weight than the output ones. `week input` means that the whole input block has lower weight. `style transfer (SDXL)` only works with SDXL and it's a very powerful tool to tranfer only the style of an image but not its content. This parameter hugely impacts how the composition reacts to the text prompting.
- **combine_embeds**, when sending more than one reference image the embeddings can be sent one after the other (`concat`) or combined in various ways. For low spec GPUs it is adviced to `average` the embeds if you send multiple images. `subtract` subtracts the embeddings of the second image to the first; in case of 3 or more images they are averaged and subtracted to the first.
- **start_at/end_at**, this is the timestepping. Defines at what percentage point of the generation to start applying the IPAdapter model. The initial steps are the most important so if you start later (eg: `start_at=0.3`) the generated image will have a very light conditioning.
- **embeds_scaling**, the way the IPAdapter models are applied to the K,V. This parameter has a small impact on how the model reacts to text prompting. `K+mean(V) w/ C penalty` grants good quality at high weights (>1.0) without burning the image.
+181
View File
@@ -0,0 +1,181 @@
# ComfyUI IPAdapter plus
[ComfyUI](https://github.com/comfyanonymous/ComfyUI) reference implementation for [IPAdapter](https://github.com/tencent-ailab/IP-Adapter/) models.
The IPAdapter are very powerful models for image-to-image conditioning. The subject or even just the style of the reference image(s) can be easily transferred to a generation. Think of it as a 1-image lora.
# Sponsorship
<div align="center">
**[:heart: Github Sponsor](https://github.com/sponsors/cubiq) | [:coin: Paypal](https://paypal.me/matt3o)**
</div>
If you like my work and wish to see updates and new features please consider sponsoring my projects.
- [ComfyUI IPAdapter Plus](https://github.com/cubiq/ComfyUI_IPAdapter_plus)
- [ComfyUI InstantID (Native)](https://github.com/cubiq/ComfyUI_InstantID)
- [ComfyUI Essentials](https://github.com/cubiq/ComfyUI_essentials)
- [ComfyUI FaceAnalysis](https://github.com/cubiq/ComfyUI_FaceAnalysis)
- [Comfy Dungeon](https://github.com/cubiq/Comfy_Dungeon)
Not to mention the documentation and videos tutorials. Check my **ComfyUI Advanced Understanding** videos on YouTube for example, [part 1](https://www.youtube.com/watch?v=_C7kR2TFIX0) and [part 2](https://www.youtube.com/watch?v=ijqXnW_9gzc)
The only way to keep the code open and free is by sponsoring its development. The more sponsorships the more time I can dedicate to my open source projects.
Please consider a [Github Sponsorship](https://github.com/sponsors/cubiq) or [PayPal donation](https://paypal.me/matt3o) (Matteo "matt3o" Spinelli). For sponsorships of $50+, let me know if you'd like to be mentioned in this readme file, you can find me on [Discord](https://latent.vision/discord) or _matt3o :snail: gmail.com_.
## Important updates
**2024/05/02**: Add `encode_batch_size` to the Advanced batch node. This can be useful for animations with a lot of frames to reduce the VRAM usage during the image encoding. Please note that results will be slightly different based on the batch size.
**2024/04/27**: Refactored the IPAdapterWeights mostly useful for AnimateDiff animations.
**2024/04/21**: Added Regional Conditioning nodes to simplify attention masking and masked text conditioning.
**2024/04/16**: Added support for the new SDXL portrait unnorm model (link below). It's very strong and tends to ignore the text conditioning. Lower the CFG to 3-4 or use a RescaleCFG node.
**2024/04/12**: Added scheduled weights. Useful for animations.
**2024/04/09**: Added experimental Style/Composition transfer for SD1.5. The results are often not as good as SDXL. Optimal weight seems to be from 0.8 to 2.0. The **Style+Composition node doesn't work for SD1.5** at the moment, you can only alter either the Style or the Composition, I need more time for testing. Old workflows will still work **but you may need to refresh the page and re-select the weight type!**
**2024/04/04**: Added Style & Composition node. It's now possible to apply both Style and Composition from the same node
**2024/04/01**: Added Composition only transfer weight type for SDXL
**2024/03/27**: Added Style transfer weight type for SDXL
**2024/03/23**: Complete code rewrite! **This is a breaking update!** Your previous workflows won't work and you'll need to recreate them. You've been warned! After the update, refresh your browser, delete the old IPAdapter nodes and create the new ones.
*(I removed old updates related to the previous version of the extension)*
## Example workflows
The [examples directory](./examples/) has many workflows that cover all IPAdapter functionalities.
![IPAdapter Example workflow](./examples/demo_workflow.jpg)
## Video Tutorials
<a href="https://youtu.be/_JzDcgKgghY" target="_blank">
<img src="https://img.youtube.com/vi/_JzDcgKgghY/hqdefault.jpg" alt="Watch the video" />
</a>
- **:star: [New IPAdapter features](https://youtu.be/_JzDcgKgghY)**
- **:art: [IPAdapter Style and Composition](https://www.youtube.com/watch?v=czcgJnoDVd4)**
The following videos are about the previous version of IPAdapter, but they still contain valuable information.
:nerd_face: [Basic usage video](https://youtu.be/7m9ZZFU3HWo), :rocket: [Advanced features video](https://www.youtube.com/watch?v=mJQ62ly7jrg), :japanese_goblin: [Attention Masking video](https://www.youtube.com/watch?v=vqG1VXKteQg), :movie_camera: [Animation Features video](https://www.youtube.com/watch?v=ddYbhv3WgWw)
## Installation
Download or git clone this repository inside `ComfyUI/custom_nodes/` directory or use the Manager. IPAdapter always requires the latest version of ComfyUI. If something doesn't work be sure to upgrade. Beware that the automatic update of the manager sometimes doesn't work and you may need to upgrade manually.
There's now a *Unified Model Loader*, for it to work you need to name the files exactly as described below. The legacy loaders work with any file name but you have to select them manually. The models can be placed into sub-directories.
Remember you can also use any custom location setting an `ipadapter` entry in the `extra_model_paths.yaml` file.
- `/ComfyUI/models/clip_vision`
- [CLIP-ViT-H-14-laion2B-s32B-b79K.safetensors](https://huggingface.co/h94/IP-Adapter/resolve/main/models/image_encoder/model.safetensors), download and rename
- [CLIP-ViT-bigG-14-laion2B-39B-b160k.safetensors](https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/image_encoder/model.safetensors), download and rename
- `/ComfyUI/models/ipadapter`, create it if not present
- [ip-adapter_sd15.safetensors](https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter_sd15.safetensors), Basic model, average strength
- [ip-adapter_sd15_light_v11.bin](https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter_sd15_light_v11.bin), Light impact model
- [ip-adapter-plus_sd15.safetensors](https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter-plus_sd15.safetensors), Plus model, very strong
- [ip-adapter-plus-face_sd15.safetensors](https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter-plus-face_sd15.safetensors), Face model, portraits
- [ip-adapter-full-face_sd15.safetensors](https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter-full-face_sd15.safetensors), Stronger face model, not necessarily better
- [ip-adapter_sd15_vit-G.safetensors](https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter_sd15_vit-G.safetensors), Base model, **requires bigG clip vision encoder**
- [ip-adapter_sdxl_vit-h.safetensors](https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter_sdxl_vit-h.safetensors), SDXL model
- [ip-adapter-plus_sdxl_vit-h.safetensors](https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter-plus_sdxl_vit-h.safetensors), SDXL plus model
- [ip-adapter-plus-face_sdxl_vit-h.safetensors](https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter-plus-face_sdxl_vit-h.safetensors), SDXL face model
- [ip-adapter_sdxl.safetensors](https://huggingface.co/h94/IP-Adapter/resolve/main/sdxl_models/ip-adapter_sdxl.safetensors), vit-G SDXL model, **requires bigG clip vision encoder**
- **Deprecated** [ip-adapter_sd15_light.safetensors](https://huggingface.co/h94/IP-Adapter/resolve/main/models/ip-adapter_sd15_light.safetensors), v1.0 Light impact model
**FaceID** models require `insightface`, you need to install it in your ComfyUI environment. Check [this issue](https://github.com/cubiq/ComfyUI_IPAdapter_plus/issues/162) for help. Remember that most FaceID models also need a LoRA.
For the Unified Loader to work the files need to be named exactly as shown in the list below.
- `/ComfyUI/models/ipadapter`
- [ip-adapter-faceid_sd15.bin](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid_sd15.bin), base FaceID model
- [ip-adapter-faceid-plusv2_sd15.bin](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-plusv2_sd15.bin), FaceID plus v2
- [ip-adapter-faceid-portrait-v11_sd15.bin](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-portrait-v11_sd15.bin), text prompt style transfer for portraits
- [ip-adapter-faceid_sdxl.bin](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid_sdxl.bin), SDXL base FaceID
- [ip-adapter-faceid-plusv2_sdxl.bin](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-plusv2_sdxl.bin), SDXL plus v2
- [ip-adapter-faceid-portrait_sdxl.bin](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-portrait_sdxl.bin), SDXL text prompt style transfer
- [ip-adapter-faceid-portrait_sdxl_unnorm.bin](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-portrait_sdxl_unnorm.bin), very strong style transfer SDXL only
- **Deprecated** [ip-adapter-faceid-plus_sd15.bin](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-plus_sd15.bin), FaceID plus v1
- **Deprecated** [ip-adapter-faceid-portrait_sd15.bin](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-portrait_sd15.bin), v1 of the portrait model
Most FaceID models require a LoRA. If you use the `IPAdapter Unified Loader FaceID` it will be loaded automatically if you follow the naming convention. Otherwise you have to load them manually, be careful each FaceID model has to be paired with its own specific LoRA.
- `/ComfyUI/models/loras`
- [ip-adapter-faceid_sd15_lora.safetensors](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid_sd15_lora.safetensors)
- [ip-adapter-faceid-plusv2_sd15_lora.safetensors](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-plusv2_sd15_lora.safetensors)
- [ip-adapter-faceid_sdxl_lora.safetensors](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid_sdxl_lora.safetensors), SDXL FaceID LoRA
- [ip-adapter-faceid-plusv2_sdxl_lora.safetensors](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-plusv2_sdxl_lora.safetensors), SDXL plus v2 LoRA
- **Deprecated** [ip-adapter-faceid-plus_sd15_lora.safetensors](https://huggingface.co/h94/IP-Adapter-FaceID/resolve/main/ip-adapter-faceid-plus_sd15_lora.safetensors), LoRA for the deprecated FaceID plus v1 model
All models can be found on [huggingface](https://huggingface.co/h94).
### Community's models
The community has baked some interesting IPAdapter models.
- `/ComfyUI/models/ipadapter`
- [ip_plus_composition_sd15.safetensors](https://huggingface.co/ostris/ip-composition-adapter/resolve/main/ip_plus_composition_sd15.safetensors), general composition ignoring style and content, more about it [here](https://huggingface.co/ostris/ip-composition-adapter)
- [ip_plus_composition_sdxl.safetensors](https://huggingface.co/ostris/ip-composition-adapter/resolve/main/ip_plus_composition_sdxl.safetensors), SDXL version
if you know of other models please let me know and I will add them to the unified loader.
## Generic suggestions
There are many workflows included in the [examples](./examples/) directory. Please check them before asking for support.
Usually it's a good idea to lower the `weight` to at least `0.8` and increase the number steps. To increase adherece to the prompt you may try to change the **weight type** in the `IPAdapter Advanced` node.
## Nodes reference
I'm (slowly) documenting all nodes. Please check the [Nodes reference](./NODES.md).
## Troubleshooting
Please check the [troubleshooting](https://github.com/cubiq/ComfyUI_IPAdapter_plus/issues/108) before posting a new issue. Also remember to check the previous closed issues.
## Current sponsors
It's only thanks to generous sponsors that **the whole community** can enjoy open and free software. Please join me in thanking the following companies and individuals!
### :trophy: Gold sponsors
[![Kaiber.ai](https://f.latent.vision/imgs/kaiber.png)](https://kaiber.ai/)&nbsp; &nbsp;[![Kaiber.ai](https://f.latent.vision/imgs/replicate.png)](https://replicate.com/)
### :tada: Silver sponsors
[![OperArt.ai](https://f.latent.vision/imgs/openart.png?r=1)](https://openart.ai/workflows)
### Companies supporting my projects
- [RunComfy](https://www.runcomfy.com/) (ComfyUI Cloud)
### Esteemed individuals
- [Jack Gane](https://github.com/ganeJackS)
- [Nathan Shipley](https://www.nathanshipley.com/)
- [Dkdnzia](https://github.com/Dkdnzia)
### One-time Extraordinaires
- [Eric Rollei](https://github.com/EricRollei)
- [francaleu](https://github.com/francaleu)
- [Neta.art](https://github.com/talesofai)
- [Samwise Wang](https://github.com/tzwm)
- _And all private sponsors, you know who you are!_
## Credits
- [IPAdapter](https://github.com/tencent-ailab/IP-Adapter/)
- [InstantStyle](https://github.com/InstantStyle/InstantStyle)
- [B-Lora](https://github.com/yardenfren1996/B-LoRA/)
- [ComfyUI](https://github.com/comfyanonymous/ComfyUI)
- [laksjdjf](https://github.com/laksjdjf/)
Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

@@ -0,0 +1,619 @@
{
"last_node_id": 17,
"last_link_id": 26,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
50,
730
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
20
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"sd15/realisticVisionV51_v51VAE.safetensors"
]
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1770,
710
],
"size": {
"0": 529.7760009765625,
"1": 582.3048095703125
},
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"properties": {},
"widgets_values": [
"IPAdapter"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
690,
840
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"blurry, noisy, messy, lowres, jpeg, artifacts, ill, distorted, malformed"
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1570,
700
],
"size": {
"0": 140,
"1": 46
},
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
801,
1097
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
690,
610
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"in a peaceful spring morning a woman wearing a white shirt is sitting in a park on a bench\n\nhigh quality, detailed, diffuse light"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
1210,
700
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 23
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
30,
6.5,
"ddpm",
"karras",
1
]
},
{
"id": 14,
"type": "IPAdapterAdvanced",
"pos": [
801,
256
],
"size": {
"0": 315,
"1": 278
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 20
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 21,
"slot_index": 1
},
{
"name": "image",
"type": "IMAGE",
"link": 26
},
{
"name": "image_negative",
"type": "IMAGE",
"link": null
},
{
"name": "attn_mask",
"type": "MASK",
"link": null
},
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": 24,
"slot_index": 5
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
23
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapterAdvanced"
},
"widgets_values": [
0.8,
"linear",
"concat",
0,
1,
"V only"
]
},
{
"id": 17,
"type": "PrepImageForClipVision",
"pos": [
797,
87
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 25
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
26
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "PrepImageForClipVision"
},
"widgets_values": [
"LANCZOS",
"top",
0.15
]
},
{
"id": 15,
"type": "IPAdapterModelLoader",
"pos": [
308,
52
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "IPADAPTER",
"type": "IPADAPTER",
"links": [
21
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "IPAdapterModelLoader"
},
"widgets_values": [
"ip-adapter-plus_sd15.safetensors"
]
},
{
"id": 16,
"type": "CLIPVisionLoader",
"pos": [
308,
161
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "CLIP_VISION",
"type": "CLIP_VISION",
"links": [
24
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "CLIPVisionLoader"
},
"widgets_values": [
"CLIP-ViT-H-14-laion2B-s32B-b79K.safetensors"
]
},
{
"id": 12,
"type": "LoadImage",
"pos": [
311,
270
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 4,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
25
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"warrior_woman.png",
"image"
]
}
],
"links": [
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
20,
4,
0,
14,
0,
"MODEL"
],
[
21,
15,
0,
14,
1,
"IPADAPTER"
],
[
23,
14,
0,
3,
0,
"MODEL"
],
[
24,
16,
0,
14,
5,
"CLIP_VISION"
],
[
25,
12,
0,
17,
0,
"IMAGE"
],
[
26,
17,
0,
14,
2,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,566 @@
{
"last_node_id": 20,
"last_link_id": 36,
"nodes": [
{
"id": 8,
"type": "VAEDecode",
"pos": [
1640,
710
],
"size": {
"0": 140,
"1": 46
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
870,
1100
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
1280,
710
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 32
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
30,
6.5,
"ddpm",
"karras",
1
]
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1830,
700
],
"size": {
"0": 529.7760009765625,
"1": 582.3048095703125
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"properties": {},
"widgets_values": [
"IPAdapter"
]
},
{
"id": 12,
"type": "LoadImage",
"pos": [
450,
240
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
29
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"rosario_4.jpg",
"image"
]
},
{
"id": 20,
"type": "IPAdapterUnifiedLoaderFaceID",
"pos": [
460,
60
],
"size": {
"0": 315,
"1": 126
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 36
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": null
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
35
],
"shape": 3,
"slot_index": 0
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"links": [
34
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "IPAdapterUnifiedLoaderFaceID"
},
"widgets_values": [
"FACEID PLUS V2",
0.6,
"CPU"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
760,
850
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"blurry, noisy, messy, lowres, jpeg, artifacts, ill, distorted, malformed, naked"
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
760,
620
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"closeup of a beautiful woman wearing a black dress on the seaside\n\nserene, sunset, spring, high quality, detailed, diffuse light"
]
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
10,
680
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
36
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"sd15/realisticVisionV51_v51VAE.safetensors"
]
},
{
"id": 18,
"type": "IPAdapterFaceID",
"pos": [
850,
190
],
"size": {
"0": 315,
"1": 298
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 35
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 34,
"slot_index": 1
},
{
"name": "image",
"type": "IMAGE",
"link": 29
},
{
"name": "image_negative",
"type": "IMAGE",
"link": null
},
{
"name": "attn_mask",
"type": "MASK",
"link": null
},
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": null
},
{
"name": "insightface",
"type": "INSIGHTFACE",
"link": null
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
32
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapterFaceID"
},
"widgets_values": [
1,
2,
"linear",
"concat",
0,
1
]
}
],
"links": [
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
29,
12,
0,
18,
2,
"IMAGE"
],
[
32,
18,
0,
3,
0,
"MODEL"
],
[
34,
20,
1,
18,
1,
"IPADAPTER"
],
[
35,
20,
0,
18,
0,
"MODEL"
],
[
36,
4,
0,
20,
0,
"MODEL"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,728 @@
{
"last_node_id": 23,
"last_link_id": 44,
"nodes": [
{
"id": 8,
"type": "VAEDecode",
"pos": [
1640,
710
],
"size": {
"0": 140,
"1": 46
},
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
870,
1100
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
1280,
710
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 42
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
30,
6.5,
"ddpm",
"karras",
1
]
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1830,
700
],
"size": {
"0": 529.7760009765625,
"1": 582.3048095703125
},
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"properties": {},
"widgets_values": [
"IPAdapter"
]
},
{
"id": 12,
"type": "LoadImage",
"pos": [
450,
240
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
29
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"rosario_4.jpg",
"image"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
760,
850
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"blurry, noisy, messy, lowres, jpeg, artifacts, ill, distorted, malformed, naked"
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
760,
620
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"closeup of a beautiful woman wearing a black dress on the seaside\n\nserene, sunset, spring, high quality, detailed, diffuse light"
]
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
10,
680
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
36
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"sd15/realisticVisionV51_v51VAE.safetensors"
]
},
{
"id": 20,
"type": "IPAdapterUnifiedLoaderFaceID",
"pos": [
460,
60
],
"size": {
"0": 315,
"1": 126
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 36
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": null
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
35
],
"shape": 3,
"slot_index": 0
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"links": [
34,
38
],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "IPAdapterUnifiedLoaderFaceID"
},
"widgets_values": [
"FACEID PLUS V2",
0.6,
"CPU"
]
},
{
"id": 22,
"type": "IPAdapterUnifiedLoader",
"pos": [
855,
51
],
"size": {
"0": 315,
"1": 78
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 43
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 38
}
],
"outputs": [
{
"name": "model",
"type": "MODEL",
"links": [
40
],
"shape": 3,
"slot_index": 0
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"links": [
37
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "IPAdapterUnifiedLoader"
},
"widgets_values": [
"FULL FACE - SD1.5 only (portraits stronger)"
]
},
{
"id": 21,
"type": "IPAdapter",
"pos": [
1280,
170
],
"size": {
"0": 315,
"1": 190
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 40
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 37,
"slot_index": 1
},
{
"name": "image",
"type": "IMAGE",
"link": 44
},
{
"name": "attn_mask",
"type": "MASK",
"link": null
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
42
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapter"
},
"widgets_values": [
0.4,
0,
1,
"standard"
]
},
{
"id": 18,
"type": "IPAdapterFaceID",
"pos": [
850,
190
],
"size": {
"0": 315,
"1": 322
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 35
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 34,
"slot_index": 1
},
{
"name": "image",
"type": "IMAGE",
"link": 29
},
{
"name": "image_negative",
"type": "IMAGE",
"link": null
},
{
"name": "attn_mask",
"type": "MASK",
"link": null
},
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": null
},
{
"name": "insightface",
"type": "INSIGHTFACE",
"link": null
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
43
],
"shape": 3,
"slot_index": 0
},
{
"name": "face_image",
"type": "IMAGE",
"links": [
44
],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "IPAdapterFaceID"
},
"widgets_values": [
0.8,
2,
"linear",
"concat",
0,
1,
"V only"
]
}
],
"links": [
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
29,
12,
0,
18,
2,
"IMAGE"
],
[
34,
20,
1,
18,
1,
"IPADAPTER"
],
[
35,
20,
0,
18,
0,
"MODEL"
],
[
36,
4,
0,
20,
0,
"MODEL"
],
[
37,
22,
1,
21,
1,
"IPADAPTER"
],
[
38,
20,
1,
22,
1,
"IPADAPTER"
],
[
40,
22,
0,
21,
0,
"MODEL"
],
[
42,
21,
0,
3,
0,
"MODEL"
],
[
43,
18,
0,
22,
0,
"MODEL"
],
[
44,
18,
1,
21,
2,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
@@ -0,0 +1,956 @@
{
"last_node_id": 24,
"last_link_id": 49,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
50,
730
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
20,
44
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8,
42
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"sd15/realisticVisionV51_v51VAE.safetensors"
]
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1770,
710
],
"size": {
"0": 529.7760009765625,
"1": 582.3048095703125
},
"flags": {},
"order": 15,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"properties": {},
"widgets_values": [
"IPAdapter"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
690,
840
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6,
39
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"blurry, noisy, messy, lowres, jpeg, artifacts, ill, distorted, malformed"
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
801,
1097
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2,
40
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 16,
"type": "CLIPVisionLoader",
"pos": [
308,
161
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "CLIP_VISION",
"type": "CLIP_VISION",
"links": [
24,
48
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "CLIPVisionLoader"
},
"widgets_values": [
"IPAdapter_image_encoder_sd15.safetensors"
]
},
{
"id": 15,
"type": "IPAdapterModelLoader",
"pos": [
308,
52
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "IPADAPTER",
"type": "IPADAPTER",
"links": [
33,
45
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "IPAdapterModelLoader"
},
"widgets_values": [
"ip-adapter-plus_sd15.safetensors"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
1210,
700
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 23
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
30,
6.5,
"dpmpp_2m_sde_gpu",
"exponential",
1
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1575,
705
],
"size": {
"0": 140,
"1": 46
},
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
690,
610
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4,
38
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"a castle on a cliff\n\nhigh quality, detailed, diffuse light"
]
},
{
"id": 24,
"type": "IPAdapterAdvanced",
"pos": [
1800,
330
],
"size": {
"0": 315,
"1": 254
},
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 44
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 45,
"slot_index": 1
},
{
"name": "image",
"type": "IMAGE",
"link": 46
},
{
"name": "image_negative",
"type": "IMAGE",
"link": 49
},
{
"name": "attn_mask",
"type": "MASK",
"link": null
},
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": 48,
"slot_index": 5
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
37
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapterAdvanced"
},
"widgets_values": [
0.7000000000000001,
"linear",
"concat",
0,
1
]
},
{
"id": 20,
"type": "PrepImageForClipVision",
"pos": [
775,
347
],
"size": [
210,
106
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 35
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
36,
46
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "PrepImageForClipVision"
},
"widgets_values": [
"LANCZOS",
"top",
0
]
},
{
"id": 21,
"type": "KSampler",
"pos": [
2190,
330
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 37
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 38
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 39
},
{
"name": "latent_image",
"type": "LATENT",
"link": 40
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
41
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
30,
6.5,
"dpmpp_2m_sde_gpu",
"exponential",
1
]
},
{
"id": 14,
"type": "IPAdapterAdvanced",
"pos": [
1199,
346
],
"size": {
"0": 315,
"1": 254
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 20
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 33,
"slot_index": 1
},
{
"name": "image",
"type": "IMAGE",
"link": 36
},
{
"name": "image_negative",
"type": "IMAGE",
"link": null
},
{
"name": "attn_mask",
"type": "MASK",
"link": null
},
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": 24,
"slot_index": 5
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
23
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapterAdvanced"
},
"widgets_values": [
0.7000000000000001,
"linear",
"concat",
0,
1
]
},
{
"id": 23,
"type": "SaveImage",
"pos": [
2333,
711
],
"size": {
"0": 529.7760009765625,
"1": 582.3048095703125
},
"flags": {},
"order": 16,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 43
}
],
"properties": {},
"widgets_values": [
"IPAdapter"
]
},
{
"id": 19,
"type": "LoadImage",
"pos": [
1206,
-41
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 4,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
49
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"trees.jpg",
"image"
]
},
{
"id": 12,
"type": "LoadImage",
"pos": [
313,
291
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 5,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
35
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"castle.jpg",
"image"
]
},
{
"id": 22,
"type": "VAEDecode",
"pos": [
2581,
331
],
"size": {
"0": 140,
"1": 46
},
"flags": {},
"order": 14,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 41
},
{
"name": "vae",
"type": "VAE",
"link": 42
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
43
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
}
],
"links": [
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
20,
4,
0,
14,
0,
"MODEL"
],
[
23,
14,
0,
3,
0,
"MODEL"
],
[
24,
16,
0,
14,
5,
"CLIP_VISION"
],
[
33,
15,
0,
14,
1,
"IPADAPTER"
],
[
35,
12,
0,
20,
0,
"IMAGE"
],
[
36,
20,
0,
14,
2,
"IMAGE"
],
[
37,
24,
0,
21,
0,
"MODEL"
],
[
38,
6,
0,
21,
1,
"CONDITIONING"
],
[
39,
7,
0,
21,
2,
"CONDITIONING"
],
[
40,
5,
0,
21,
3,
"LATENT"
],
[
41,
21,
0,
22,
0,
"LATENT"
],
[
42,
4,
2,
22,
1,
"VAE"
],
[
43,
22,
0,
23,
0,
"IMAGE"
],
[
44,
4,
0,
24,
0,
"MODEL"
],
[
45,
15,
0,
24,
1,
"IPADAPTER"
],
[
46,
20,
0,
24,
2,
"IMAGE"
],
[
48,
16,
0,
24,
5,
"CLIP_VISION"
],
[
49,
19,
0,
24,
3,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
@@ -0,0 +1,677 @@
{
"last_node_id": 18,
"last_link_id": 30,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
50,
730
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
20
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"sd15/realisticVisionV51_v51VAE.safetensors"
]
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1770,
710
],
"size": {
"0": 529.7760009765625,
"1": 582.3048095703125
},
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"properties": {},
"widgets_values": [
"IPAdapter"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
690,
840
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"blurry, noisy, messy, lowres, jpeg, artifacts, ill, distorted, malformed"
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
801,
1097
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 15,
"type": "IPAdapterModelLoader",
"pos": [
308,
52
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "IPADAPTER",
"type": "IPADAPTER",
"links": [
21
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "IPAdapterModelLoader"
},
"widgets_values": [
"ip-adapter-plus_sd15.safetensors"
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
690,
610
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"in a peaceful spring morning a woman wearing a white shirt is sitting in a park on a bench\n\nhigh quality, detailed, diffuse light"
]
},
{
"id": 12,
"type": "LoadImage",
"pos": [
311,
270
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
25
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"girl_sitting.png",
"image"
]
},
{
"id": 17,
"type": "PrepImageForClipVision",
"pos": [
728,
290
],
"size": {
"0": 210,
"1": 106
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 25
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
26,
29
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "PrepImageForClipVision"
},
"widgets_values": [
"LANCZOS",
"top",
0.15
]
},
{
"id": 14,
"type": "IPAdapterAdvanced",
"pos": [
1351,
214
],
"size": {
"0": 315,
"1": 278
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 20
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 21,
"slot_index": 1
},
{
"name": "image",
"type": "IMAGE",
"link": 26
},
{
"name": "image_negative",
"type": "IMAGE",
"link": 30
},
{
"name": "attn_mask",
"type": "MASK",
"link": null
},
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": 24,
"slot_index": 5
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
23
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapterAdvanced"
},
"widgets_values": [
0.7000000000000001,
"linear",
"concat",
0,
1,
"V only"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
1210,
700
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 23
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
30,
6.5,
"dpmpp_2m_sde_gpu",
"exponential",
1
]
},
{
"id": 18,
"type": "IPAdapterNoise",
"pos": [
1019,
405
],
"size": {
"0": 210,
"1": 106
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "image_optional",
"type": "IMAGE",
"link": 29
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
30
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapterNoise"
},
"widgets_values": [
"fade",
0.3,
5
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1575,
705
],
"size": {
"0": 140,
"1": 46
},
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 16,
"type": "CLIPVisionLoader",
"pos": [
308,
161
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 4,
"mode": 0,
"outputs": [
{
"name": "CLIP_VISION",
"type": "CLIP_VISION",
"links": [
24
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "CLIPVisionLoader"
},
"widgets_values": [
"CLIP-ViT-H-14-laion2B-s32B-b79K.safetensors"
]
}
],
"links": [
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
20,
4,
0,
14,
0,
"MODEL"
],
[
21,
15,
0,
14,
1,
"IPADAPTER"
],
[
23,
14,
0,
3,
0,
"MODEL"
],
[
24,
16,
0,
14,
5,
"CLIP_VISION"
],
[
25,
12,
0,
17,
0,
"IMAGE"
],
[
26,
17,
0,
14,
2,
"IMAGE"
],
[
29,
17,
0,
18,
0,
"IMAGE"
],
[
30,
18,
0,
14,
3,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
@@ -0,0 +1,567 @@
{
"last_node_id": 20,
"last_link_id": 36,
"nodes": [
{
"id": 8,
"type": "VAEDecode",
"pos": [
1640,
710
],
"size": {
"0": 140,
"1": 46
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 3,
"type": "KSampler",
"pos": [
1280,
710
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 32
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
30,
6.5,
"ddpm",
"karras",
1
]
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1830,
700
],
"size": {
"0": 529.7760009765625,
"1": 582.3048095703125
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"properties": {},
"widgets_values": [
"IPAdapter"
]
},
{
"id": 20,
"type": "IPAdapterUnifiedLoaderFaceID",
"pos": [
460,
60
],
"size": {
"0": 315,
"1": 126
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 36
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": null
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
35
],
"shape": 3,
"slot_index": 0
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"links": [
34
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "IPAdapterUnifiedLoaderFaceID"
},
"widgets_values": [
"FACEID PORTRAIT (style transfer)",
0.6,
"CPU"
]
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
10,
680
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
36
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"sdxl/juggernautXL_version8Rundiffusion.safetensors"
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
870,
1100
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
1024,
1024,
1
]
},
{
"id": 12,
"type": "LoadImage",
"pos": [
450,
240
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
29
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"face2.jpg",
"image"
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
760,
620
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"a watercolor painting of a woman on the beach\n\nhigh quality artistry"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
760,
850
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"photo, blurry, noisy, messy, lowres, jpeg, artifacts, ill, distorted, malformed, naked"
]
},
{
"id": 18,
"type": "IPAdapterFaceID",
"pos": [
850,
190
],
"size": {
"0": 315,
"1": 322
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 35
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 34,
"slot_index": 1
},
{
"name": "image",
"type": "IMAGE",
"link": 29
},
{
"name": "image_negative",
"type": "IMAGE",
"link": null
},
{
"name": "attn_mask",
"type": "MASK",
"link": null
},
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": null
},
{
"name": "insightface",
"type": "INSIGHTFACE",
"link": null
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
32
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapterFaceID"
},
"widgets_values": [
0.65,
1,
"linear",
"concat",
0,
1,
"V only"
]
}
],
"links": [
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
29,
12,
0,
18,
2,
"IMAGE"
],
[
32,
18,
0,
3,
0,
"MODEL"
],
[
34,
20,
1,
18,
1,
"IPADAPTER"
],
[
35,
20,
0,
18,
0,
"MODEL"
],
[
36,
4,
0,
20,
0,
"MODEL"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,546 @@
{
"last_node_id": 13,
"last_link_id": 17,
"nodes": [
{
"id": 11,
"type": "IPAdapterUnifiedLoader",
"pos": [
440,
440
],
"size": {
"0": 315,
"1": 78
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 10
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": null
}
],
"outputs": [
{
"name": "model",
"type": "MODEL",
"links": [
11
],
"shape": 3,
"slot_index": 0
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"links": [
12
],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "IPAdapterUnifiedLoader"
},
"widgets_values": [
"PLUS (high strength)"
]
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
50,
730
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
10
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"sd15/realisticVisionV51_v51VAE.safetensors"
]
},
{
"id": 12,
"type": "LoadImage",
"pos": [
440,
60
],
"size": [
315,
314
],
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
17
],
"shape": 3
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"warrior_woman.png",
"image"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
1210,
700
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 13
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
30,
6.5,
"dpmpp_2m",
"karras",
1
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
690,
610
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"closeup of a fierce warrior woman wearing a full armor at the end of a battle\n\nhigh quality, detailed"
]
},
{
"id": 10,
"type": "IPAdapter",
"pos": [
820,
350
],
"size": {
"0": 315,
"1": 166
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 11
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 12
},
{
"name": "image",
"type": "IMAGE",
"link": 17,
"slot_index": 2
},
{
"name": "attn_mask",
"type": "MASK",
"link": null
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
13
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapter"
},
"widgets_values": [
0.8,
0,
1
]
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1770,
710
],
"size": [
529.7760009765616,
582.3048192804504
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"properties": {},
"widgets_values": [
"IPAdapter"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
690,
840
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"blurry, noisy, messy, lowres, jpeg, artifacts, ill, distorted, malformed"
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1570,
700
],
"size": [
140,
46
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
801,
1097
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
}
],
"links": [
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
10,
4,
0,
11,
0,
"MODEL"
],
[
11,
11,
0,
10,
0,
"MODEL"
],
[
12,
11,
1,
10,
1,
"IPADAPTER"
],
[
13,
10,
0,
3,
0,
"MODEL"
],
[
17,
12,
0,
10,
2,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
@@ -0,0 +1,612 @@
{
"last_node_id": 16,
"last_link_id": 25,
"nodes": [
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
690,
840
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"blurry, noisy, messy, lowres, jpeg, artifacts, ill, distorted, malformed"
]
},
{
"id": 11,
"type": "IPAdapterUnifiedLoader",
"pos": [
335,
430
],
"size": {
"0": 315,
"1": 78
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 10
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": null
}
],
"outputs": [
{
"name": "model",
"type": "MODEL",
"links": [
21
],
"shape": 3,
"slot_index": 0
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"links": [
22
],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "IPAdapterUnifiedLoader"
},
"widgets_values": [
"PLUS (high strength)"
]
},
{
"id": 12,
"type": "LoadImage",
"pos": [
-102,
-46
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
25
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"black_car.jpg",
"image"
]
},
{
"id": 16,
"type": "LoadImage",
"pos": [
310,
-40
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
24
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"bw_texture_waves.jpg",
"image"
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
801,
1097
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
1024,
1024,
1
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
690,
610
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"sports car running fast on the highway\n\nhigh quality, detailed"
]
},
{
"id": 15,
"type": "IPAdapterStyleComposition",
"pos": [
772,
219
],
"size": {
"0": 315,
"1": 322
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 21
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 22
},
{
"name": "image_style",
"type": "IMAGE",
"link": 24
},
{
"name": "image_composition",
"type": "IMAGE",
"link": 25
},
{
"name": "image_negative",
"type": "IMAGE",
"link": null
},
{
"name": "attn_mask",
"type": "MASK",
"link": null
},
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": null
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
23
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapterStyleComposition"
},
"widgets_values": [
1.2,
1,
false,
"average",
0,
1,
"V only"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
1247,
586
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 23
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
30,
6.5,
"dpmpp_2m",
"karras",
1
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1615,
586
],
"size": {
"0": 140,
"1": 46
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1822,
588
],
"size": [
691.0159878487498,
716.6239849908982
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"properties": {},
"widgets_values": [
"IPAdapter"
]
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
-72,
657
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
10
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"sdxl/AlbedoBaseXL.safetensors"
]
}
],
"links": [
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
10,
4,
0,
11,
0,
"MODEL"
],
[
21,
11,
0,
15,
0,
"MODEL"
],
[
22,
11,
1,
15,
1,
"IPADAPTER"
],
[
23,
15,
0,
3,
0,
"MODEL"
],
[
24,
16,
0,
15,
2,
"IMAGE"
],
[
25,
12,
0,
15,
3,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
@@ -0,0 +1,583 @@
{
"last_node_id": 18,
"last_link_id": 32,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
50,
730
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
29
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"sd15/realisticVisionV51_v51VAE.safetensors"
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
690,
840
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"blurry, noisy, messy, lowres, jpeg, artifacts, ill, distorted, malformed"
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1570,
700
],
"size": {
"0": 140,
"1": 46
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 12,
"type": "LoadImage",
"pos": [
250,
290
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
27
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"girl_sitting.png",
"image"
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
690,
610
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"in a peaceful spring morning a woman wearing a white shirt is sitting in a park on a bench\n\nhigh quality, detailed, diffuse light"
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
801,
1097
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
768,
1
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
1210,
700
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 30
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
2,
"fixed",
30,
6.5,
"ddpm",
"karras",
1
]
},
{
"id": 18,
"type": "IPAdapterTiled",
"pos": [
700,
230
],
"size": {
"0": 315,
"1": 302
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 29
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 31
},
{
"name": "image",
"type": "IMAGE",
"link": 27
},
{
"name": "image_negative",
"type": "IMAGE",
"link": null
},
{
"name": "attn_mask",
"type": "MASK",
"link": null
},
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": 32
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
30
],
"shape": 3,
"slot_index": 0
},
{
"name": "tiles",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "masks",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "IPAdapterTiled"
},
"widgets_values": [
0.7000000000000001,
"ease in",
"concat",
0,
1,
0,
"V only"
]
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1768,
700
],
"size": {
"0": 529.7760009765625,
"1": 582.3048095703125
},
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"properties": {},
"widgets_values": [
"IPAdapter"
]
},
{
"id": 15,
"type": "IPAdapterModelLoader",
"pos": [
250,
70
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "IPADAPTER",
"type": "IPADAPTER",
"links": [
31
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapterModelLoader"
},
"widgets_values": [
"ip-adapter-plus_sd15.safetensors"
]
},
{
"id": 16,
"type": "CLIPVisionLoader",
"pos": [
250,
180
],
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 4,
"mode": 0,
"outputs": [
{
"name": "CLIP_VISION",
"type": "CLIP_VISION",
"links": [
32
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPVisionLoader"
},
"widgets_values": [
"CLIP-ViT-H-14-laion2B-s32B-b79K.safetensors"
]
}
],
"links": [
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
27,
12,
0,
18,
2,
"IMAGE"
],
[
29,
4,
0,
18,
0,
"MODEL"
],
[
30,
18,
0,
3,
0,
"MODEL"
],
[
31,
15,
0,
18,
1,
"IPADAPTER"
],
[
32,
16,
0,
18,
5,
"CLIP_VISION"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,836 @@
{
"last_node_id": 18,
"last_link_id": 29,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
50,
730
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
10
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"sd15/realisticVisionV51_v51VAE.safetensors"
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
690,
610
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"closeup of a fierce warrior woman wearing a full armor at the end of a battle\n\nhigh quality, detailed"
]
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1770,
710
],
"size": {
"0": 529.7760009765625,
"1": 582.3048095703125
},
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"properties": {},
"widgets_values": [
"IPAdapter"
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1570,
700
],
"size": {
"0": 140,
"1": 46
},
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
801,
1097
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 12,
"type": "LoadImage",
"pos": [
453,
-296
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
21
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"warrior_woman.png",
"image"
]
},
{
"id": 15,
"type": "LoadImage",
"pos": [
458,
70
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 3,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
23
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"anime_illustration.png",
"image"
]
},
{
"id": 11,
"type": "IPAdapterUnifiedLoader",
"pos": [
440,
440
],
"size": {
"0": 315,
"1": 78
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 10
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": null
}
],
"outputs": [
{
"name": "model",
"type": "MODEL",
"links": [
19
],
"shape": 3,
"slot_index": 0
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"links": [
20,
22,
27
],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "IPAdapterUnifiedLoader"
},
"widgets_values": [
"PLUS (high strength)"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
1210,
700
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 28
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
30,
6.5,
"dpmpp_2m",
"karras",
1
]
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
690,
840
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"blurry, noisy, messy, lowres, jpeg, artifacts, ill, distorted, malformed, hat, hood, scars, blood"
]
},
{
"id": 17,
"type": "IPAdapterEncoder",
"pos": [
859,
69
],
"size": [
210,
118
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 22
},
{
"name": "image",
"type": "IMAGE",
"link": 23
},
{
"name": "mask",
"type": "MASK",
"link": null
},
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": null
}
],
"outputs": [
{
"name": "pos_embed",
"type": "EMBEDS",
"links": [
25
],
"shape": 3,
"slot_index": 0
},
{
"name": "neg_embed",
"type": "EMBEDS",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "IPAdapterEncoder"
},
"widgets_values": [
1.5
]
},
{
"id": 18,
"type": "IPAdapterCombineEmbeds",
"pos": [
1136,
-95
],
"size": [
210,
138
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "embed1",
"type": "EMBEDS",
"link": 24
},
{
"name": "embed2",
"type": "EMBEDS",
"link": 25
},
{
"name": "embed3",
"type": "EMBEDS",
"link": null
},
{
"name": "embed4",
"type": "EMBEDS",
"link": null
},
{
"name": "embed5",
"type": "EMBEDS",
"link": null
}
],
"outputs": [
{
"name": "EMBEDS",
"type": "EMBEDS",
"links": [
26
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapterCombineEmbeds"
},
"widgets_values": [
"average"
]
},
{
"id": 14,
"type": "IPAdapterEmbeds",
"pos": [
1143,
160
],
"size": {
"0": 315,
"1": 230
},
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 19
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 27
},
{
"name": "pos_embed",
"type": "EMBEDS",
"link": 26
},
{
"name": "neg_embed",
"type": "EMBEDS",
"link": 29
},
{
"name": "attn_mask",
"type": "MASK",
"link": null
},
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": null
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
28
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapterEmbeds"
},
"widgets_values": [
0.8,
"linear",
0,
1
]
},
{
"id": 16,
"type": "IPAdapterEncoder",
"pos": [
863,
-285
],
"size": [
210,
118
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 20
},
{
"name": "image",
"type": "IMAGE",
"link": 21
},
{
"name": "mask",
"type": "MASK",
"link": null
},
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": null
}
],
"outputs": [
{
"name": "pos_embed",
"type": "EMBEDS",
"links": [
24
],
"shape": 3,
"slot_index": 0
},
{
"name": "neg_embed",
"type": "EMBEDS",
"links": [
29
],
"shape": 3,
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "IPAdapterEncoder"
},
"widgets_values": [
0.6
]
}
],
"links": [
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
10,
4,
0,
11,
0,
"MODEL"
],
[
19,
11,
0,
14,
0,
"MODEL"
],
[
20,
11,
1,
16,
0,
"IPADAPTER"
],
[
21,
12,
0,
16,
1,
"IMAGE"
],
[
22,
11,
1,
17,
0,
"IPADAPTER"
],
[
23,
15,
0,
17,
1,
"IMAGE"
],
[
24,
16,
0,
18,
0,
"EMBEDS"
],
[
25,
17,
0,
18,
1,
"EMBEDS"
],
[
26,
18,
0,
14,
2,
"EMBEDS"
],
[
27,
11,
1,
14,
1,
"IPADAPTER"
],
[
28,
14,
0,
3,
0,
"MODEL"
],
[
29,
16,
1,
14,
3,
"EMBEDS"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
@@ -0,0 +1,764 @@
{
"last_node_id": 22,
"last_link_id": 40,
"nodes": [
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
690,
840
],
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"blurry, noisy, messy, lowres, jpeg, artifacts, ill, distorted, malformed"
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1570,
700
],
"size": {
"0": 140,
"1": 46
},
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
690,
610
],
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"in a peaceful spring morning a woman wearing a white shirt is sitting in a park on a bench\n\nhigh quality, detailed, diffuse light"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
1210,
700
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 31
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
30,
6.5,
"ddpm",
"karras",
1
]
},
{
"id": 19,
"type": "IPAdapterBatch",
"pos": [
1173,
251
],
"size": {
"0": 315,
"1": 254
},
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 37
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": 29
},
{
"name": "image",
"type": "IMAGE",
"link": 30
},
{
"name": "image_negative",
"type": "IMAGE",
"link": null
},
{
"name": "attn_mask",
"type": "MASK",
"link": null
},
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": null
},
{
"name": "weight",
"type": "FLOAT",
"link": 38,
"widget": {
"name": "weight"
},
"slot_index": 6
}
],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
31
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "IPAdapterBatch"
},
"widgets_values": [
1,
"linear",
0,
1,
"V only"
]
},
{
"id": 18,
"type": "IPAdapterUnifiedLoader",
"pos": [
303,
132
],
"size": {
"0": 315,
"1": 78
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 36
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"link": null
}
],
"outputs": [
{
"name": "model",
"type": "MODEL",
"links": [
37
],
"shape": 3,
"slot_index": 0
},
{
"name": "ipadapter",
"type": "IPADAPTER",
"links": [
29
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "IPAdapterUnifiedLoader"
},
"widgets_values": [
"PLUS (high strength)"
]
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
-79,
712
],
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
36
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"sd15/realisticVisionV51_v51VAE.safetensors"
]
},
{
"id": 17,
"type": "PrepImageForClipVision",
"pos": [
788,
43
],
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 25
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
30
],
"shape": 3,
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "PrepImageForClipVision"
},
"widgets_values": [
"LANCZOS",
"top",
0.15
]
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1770,
710
],
"size": {
"0": 556.2374267578125,
"1": 892.1895751953125
},
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"properties": {},
"widgets_values": [
"IPAdapter"
]
},
{
"id": 12,
"type": "LoadImage",
"pos": [
311,
270
],
"size": {
"0": 315,
"1": 314
},
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
25
],
"shape": 3,
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"warrior_woman.png",
"image"
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
801,
1097
],
"size": [
309.1109879864148,
82
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "batch_size",
"type": "INT",
"link": 35,
"widget": {
"name": "batch_size"
}
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
6
]
},
{
"id": 21,
"type": "PrimitiveNode",
"pos": [
340,
1093
],
"size": {
"0": 210,
"1": 82
},
"flags": {},
"order": 2,
"mode": 0,
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
35,
40
],
"widget": {
"name": "batch_size"
},
"slot_index": 0
}
],
"title": "frames",
"properties": {
"Run widget replace on values": false
},
"widgets_values": [
6,
"fixed"
]
},
{
"id": 22,
"type": "IPAdapterWeights",
"pos": [
761,
208
],
"size": [
299.9049990375719,
324.00000762939453
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null
},
{
"name": "frames",
"type": "INT",
"link": 40,
"widget": {
"name": "frames"
}
}
],
"outputs": [
{
"name": "weights",
"type": "FLOAT",
"links": [
38
],
"shape": 3,
"slot_index": 0
},
{
"name": "weights_invert",
"type": "FLOAT",
"links": null,
"shape": 3
},
{
"name": "total_frames",
"type": "INT",
"links": null,
"shape": 3
},
{
"name": "image_1",
"type": "IMAGE",
"links": null,
"shape": 3
},
{
"name": "image_2",
"type": "IMAGE",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "IPAdapterWeights"
},
"widgets_values": [
"1.0, 0.0",
"linear",
6,
0,
9999,
0,
0,
"full batch"
]
}
],
"links": [
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
],
[
25,
12,
0,
17,
0,
"IMAGE"
],
[
29,
18,
1,
19,
1,
"IPADAPTER"
],
[
30,
17,
0,
19,
2,
"IMAGE"
],
[
31,
19,
0,
3,
0,
"MODEL"
],
[
35,
21,
0,
5,
0,
"INT"
],
[
36,
4,
0,
18,
0,
"MODEL"
],
[
37,
18,
0,
19,
0,
"MODEL"
],
[
38,
22,
0,
19,
6,
"FLOAT"
],
[
40,
21,
0,
22,
1,
"INT"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}
@@ -0,0 +1,275 @@
import math
import torch
import torch.nn as nn
from einops import rearrange
from einops.layers.torch import Rearrange
# FFN
def FeedForwardImport(dim, mult=4):
inner_dim = int(dim * mult)
return nn.Sequential(
nn.LayerNorm(dim),
nn.Linear(dim, inner_dim, bias=False),
nn.GELU(),
nn.Linear(inner_dim, dim, bias=False),
)
def reshape_tensor(x, heads):
bs, length, width = x.shape
# (bs, length, width) --> (bs, length, n_heads, dim_per_head)
x = x.view(bs, length, heads, -1)
# (bs, length, n_heads, dim_per_head) --> (bs, n_heads, length, dim_per_head)
x = x.transpose(1, 2)
# (bs, n_heads, length, dim_per_head) --> (bs*n_heads, length, dim_per_head)
x = x.reshape(bs, heads, length, -1)
return x
class PerceiverAttentionImport(nn.Module):
def __init__(self, *, dim, dim_head=64, heads=8):
super().__init__()
self.scale = dim_head**-0.5
self.dim_head = dim_head
self.heads = heads
inner_dim = dim_head * heads
self.norm1 = nn.LayerNorm(dim)
self.norm2 = nn.LayerNorm(dim)
self.to_q = nn.Linear(dim, inner_dim, bias=False)
self.to_kv = nn.Linear(dim, inner_dim * 2, bias=False)
self.to_out = nn.Linear(inner_dim, dim, bias=False)
def forward(self, x, latents):
"""
Args:
x (torch.Tensor): image features
shape (b, n1, D)
latent (torch.Tensor): latent features
shape (b, n2, D)
"""
x = self.norm1(x)
latents = self.norm2(latents)
b, l, _ = latents.shape
q = self.to_q(latents)
kv_input = torch.cat((x, latents), dim=-2)
k, v = self.to_kv(kv_input).chunk(2, dim=-1)
q = reshape_tensor(q, self.heads)
k = reshape_tensor(k, self.heads)
v = reshape_tensor(v, self.heads)
# attention
scale = 1 / math.sqrt(math.sqrt(self.dim_head))
weight = (q * scale) @ (k * scale).transpose(-2, -1) # More stable with f16 than dividing afterwards
weight = torch.softmax(weight.float(), dim=-1).type(weight.dtype)
out = weight @ v
out = out.permute(0, 2, 1, 3).reshape(b, l, -1)
return self.to_out(out)
class ResamplerImport(nn.Module):
def __init__(
self,
dim=1024,
depth=8,
dim_head=64,
heads=16,
num_queries=8,
embedding_dim=768,
output_dim=1024,
ff_mult=4,
max_seq_len: int = 257, # CLIP tokens + CLS token
apply_pos_emb: bool = False,
num_latents_mean_pooled: int = 0, # number of latents derived from mean pooled representation of the sequence
):
super().__init__()
self.pos_emb = nn.Embedding(max_seq_len, embedding_dim) if apply_pos_emb else None
self.latents = nn.Parameter(torch.randn(1, num_queries, dim) / dim**0.5)
self.proj_in = nn.Linear(embedding_dim, dim)
self.proj_out = nn.Linear(dim, output_dim)
self.norm_out = nn.LayerNorm(output_dim)
self.to_latents_from_mean_pooled_seq = (
nn.Sequential(
nn.LayerNorm(dim),
nn.Linear(dim, dim * num_latents_mean_pooled),
Rearrange("b (n d) -> b n d", n=num_latents_mean_pooled),
)
if num_latents_mean_pooled > 0
else None
)
self.layers = nn.ModuleList([])
for _ in range(depth):
self.layers.append(
nn.ModuleList(
[
PerceiverAttentionImport(dim=dim, dim_head=dim_head, heads=heads),
FeedForwardImport(dim=dim, mult=ff_mult),
]
)
)
def forward(self, x):
if self.pos_emb is not None:
n, device = x.shape[1], x.device
pos_emb = self.pos_emb(torch.arange(n, device=device))
x = x + pos_emb
latents = self.latents.repeat(x.size(0), 1, 1)
x = self.proj_in(x)
if self.to_latents_from_mean_pooled_seq:
meanpooled_seq = masked_mean(x, dim=1, mask=torch.ones(x.shape[:2], device=x.device, dtype=torch.bool))
meanpooled_latents = self.to_latents_from_mean_pooled_seq(meanpooled_seq)
latents = torch.cat((meanpooled_latents, latents), dim=-2)
for attn, ff in self.layers:
latents = attn(x, latents) + latents
latents = ff(latents) + latents
latents = self.proj_out(latents)
return self.norm_out(latents)
def masked_mean(t, *, dim, mask=None):
if mask is None:
return t.mean(dim=dim)
denom = mask.sum(dim=dim, keepdim=True)
mask = rearrange(mask, "b n -> b n 1")
masked_t = t.masked_fill(~mask, 0.0)
return masked_t.sum(dim=dim) / denom.clamp(min=1e-5)
class FacePerceiverResamplerImport(nn.Module):
def __init__(
self,
*,
dim=768,
depth=4,
dim_head=64,
heads=16,
embedding_dim=1280,
output_dim=768,
ff_mult=4,
):
super().__init__()
self.proj_in = nn.Linear(embedding_dim, dim)
self.proj_out = nn.Linear(dim, output_dim)
self.norm_out = nn.LayerNorm(output_dim)
self.layers = nn.ModuleList([])
for _ in range(depth):
self.layers.append(
nn.ModuleList(
[
PerceiverAttentionImport(dim=dim, dim_head=dim_head, heads=heads),
FeedForwardImport(dim=dim, mult=ff_mult),
]
)
)
def forward(self, latents, x):
x = self.proj_in(x)
for attn, ff in self.layers:
latents = attn(x, latents) + latents
latents = ff(latents) + latents
latents = self.proj_out(latents)
return self.norm_out(latents)
class MLPProjModelImport(nn.Module):
def __init__(self, cross_attention_dim=1024, clip_embeddings_dim=1024):
super().__init__()
self.proj = nn.Sequential(
nn.Linear(clip_embeddings_dim, clip_embeddings_dim),
nn.GELU(),
nn.Linear(clip_embeddings_dim, cross_attention_dim),
nn.LayerNorm(cross_attention_dim)
)
def forward(self, image_embeds):
clip_extra_context_tokens = self.proj(image_embeds)
return clip_extra_context_tokens
class MLPProjModelFaceIdImport(nn.Module):
def __init__(self, cross_attention_dim=768, id_embeddings_dim=512, num_tokens=4):
super().__init__()
self.cross_attention_dim = cross_attention_dim
self.num_tokens = num_tokens
self.proj = nn.Sequential(
nn.Linear(id_embeddings_dim, id_embeddings_dim*2),
nn.GELU(),
nn.Linear(id_embeddings_dim*2, cross_attention_dim*num_tokens),
)
self.norm = nn.LayerNorm(cross_attention_dim)
def forward(self, id_embeds):
x = self.proj(id_embeds)
x = x.reshape(-1, self.num_tokens, self.cross_attention_dim)
x = self.norm(x)
return x
class ProjModelFaceIdPlusImport(nn.Module):
def __init__(self, cross_attention_dim=768, id_embeddings_dim=512, clip_embeddings_dim=1280, num_tokens=4):
super().__init__()
self.cross_attention_dim = cross_attention_dim
self.num_tokens = num_tokens
self.proj = nn.Sequential(
nn.Linear(id_embeddings_dim, id_embeddings_dim*2),
nn.GELU(),
nn.Linear(id_embeddings_dim*2, cross_attention_dim*num_tokens),
)
self.norm = nn.LayerNorm(cross_attention_dim)
self.perceiver_resampler = FacePerceiverResamplerImport(
dim=cross_attention_dim,
depth=4,
dim_head=64,
heads=cross_attention_dim // 64,
embedding_dim=clip_embeddings_dim,
output_dim=cross_attention_dim,
ff_mult=4,
)
def forward(self, id_embeds, clip_embeds, scale=1.0, shortcut=False):
x = self.proj(id_embeds)
x = x.reshape(-1, self.num_tokens, self.cross_attention_dim)
x = self.norm(x)
out = self.perceiver_resampler(x, clip_embeds)
if shortcut:
out = x + scale * out
return out
class ImageProjModelImport(nn.Module):
def __init__(self, cross_attention_dim=1024, clip_embeddings_dim=1024, clip_extra_context_tokens=4):
super().__init__()
self.cross_attention_dim = cross_attention_dim
self.clip_extra_context_tokens = clip_extra_context_tokens
self.proj = nn.Linear(clip_embeddings_dim, self.clip_extra_context_tokens * cross_attention_dim)
self.norm = nn.LayerNorm(cross_attention_dim)
def forward(self, image_embeds):
embeds = image_embeds
x = self.proj(embeds).reshape(-1, self.clip_extra_context_tokens, self.cross_attention_dim)
x = self.norm(x)
return x
+265
View File
@@ -0,0 +1,265 @@
import re
import torch
import os
import folder_paths
from comfy.clip_vision import clip_preprocess, Output
import comfy.utils
import comfy.model_management as model_management
try:
import torchvision.transforms.v2 as T
except ImportError:
import torchvision.transforms as T
def get_clipvision_file(preset):
preset = preset.lower()
clipvision_list = folder_paths.get_filename_list("clip_vision")
if preset.startswith("vit-g"):
pattern = r'(ViT.bigG.14.*39B.b160k|ipadapter.*sdxl|sdxl.*model\.(bin|safetensors))'
else:
pattern = r'(ViT.H.14.*s32B.b79K|ipadapter.*sd15|sd1.?5.*model\.(bin|safetensors))'
clipvision_file = [e for e in clipvision_list if re.search(pattern, e, re.IGNORECASE)]
clipvision_file = folder_paths.get_full_path("clip_vision", clipvision_file[0]) if clipvision_file else None
return clipvision_file
def get_ipadapter_file(preset, is_sdxl):
preset = preset.lower()
ipadapter_list = folder_paths.get_filename_list("ipadapter")
is_insightface = False
lora_pattern = None
if preset.startswith("light"):
if is_sdxl:
raise Exception("light model is not supported for SDXL")
pattern = r'sd15.light.v11\.(safetensors|bin)$'
# if v11 is not found, try with the old version
if not [e for e in ipadapter_list if re.search(pattern, e, re.IGNORECASE)]:
pattern = r'sd15.light\.(safetensors|bin)$'
elif preset.startswith("standard"):
if is_sdxl:
pattern = r'ip.adapter.sdxl.vit.h\.(safetensors|bin)$'
else:
pattern = r'ip.adapter.sd15\.(safetensors|bin)$'
elif preset.startswith("vit-g"):
if is_sdxl:
pattern = r'ip.adapter.sdxl\.(safetensors|bin)$'
else:
pattern = r'sd15.vit.g\.(safetensors|bin)$'
elif preset.startswith("plus ("):
if is_sdxl:
pattern = r'plus.sdxl.vit.h\.(safetensors|bin)$'
else:
pattern = r'ip.adapter.plus.sd15\.(safetensors|bin)$'
elif preset.startswith("plus face"):
if is_sdxl:
pattern = r'plus.face.sdxl.vit.h\.(safetensors|bin)$'
else:
pattern = r'plus.face.sd15\.(safetensors|bin)$'
elif preset.startswith("full"):
if is_sdxl:
raise Exception("full face model is not supported for SDXL")
pattern = r'full.face.sd15\.(safetensors|bin)$'
elif preset.startswith("faceid portrait ("):
if is_sdxl:
pattern = r'portrait.sdxl\.(safetensors|bin)$'
else:
pattern = r'portrait.v11.sd15\.(safetensors|bin)$'
# if v11 is not found, try with the old version
if not [e for e in ipadapter_list if re.search(pattern, e, re.IGNORECASE)]:
pattern = r'portrait.sd15\.(safetensors|bin)$'
is_insightface = True
elif preset.startswith("faceid portrait unnorm"):
if is_sdxl:
pattern = r'portrait.sdxl.unnorm\.(safetensors|bin)$'
else:
raise Exception("portrait unnorm model is not supported for SD1.5")
is_insightface = True
elif preset == "faceid":
if is_sdxl:
pattern = r'faceid.sdxl\.(safetensors|bin)$'
lora_pattern = r'faceid.sdxl.lora\.safetensors$'
else:
pattern = r'faceid.sd15\.(safetensors|bin)$'
lora_pattern = r'faceid.sd15.lora\.safetensors$'
is_insightface = True
elif preset.startswith("faceid plus -"):
if is_sdxl:
raise Exception("faceid plus model is not supported for SDXL")
pattern = r'faceid.plus.sd15\.(safetensors|bin)$'
lora_pattern = r'faceid.plus.sd15.lora\.safetensors$'
is_insightface = True
elif preset.startswith("faceid plus v2"):
if is_sdxl:
pattern = r'faceid.plusv2.sdxl\.(safetensors|bin)$'
lora_pattern = r'faceid.plusv2.sdxl.lora\.safetensors$'
else:
pattern = r'faceid.plusv2.sd15\.(safetensors|bin)$'
lora_pattern = r'faceid.plusv2.sd15.lora\.safetensors$'
is_insightface = True
# Community's models
elif preset.startswith("composition"):
if is_sdxl:
pattern = r'plus.composition.sdxl\.safetensors$'
else:
pattern = r'plus.composition.sd15\.safetensors$'
else:
raise Exception(f"invalid type '{preset}'")
ipadapter_file = [e for e in ipadapter_list if re.search(pattern, e, re.IGNORECASE)]
ipadapter_file = folder_paths.get_full_path("ipadapter", ipadapter_file[0]) if ipadapter_file else None
return ipadapter_file, is_insightface, lora_pattern
def get_lora_file(pattern):
lora_list = folder_paths.get_filename_list("loras")
lora_file = [e for e in lora_list if re.search(pattern, e, re.IGNORECASE)]
lora_file = folder_paths.get_full_path("loras", lora_file[0]) if lora_file else None
return lora_file
def ipadapter_model_loader(file):
model = comfy.utils.load_torch_file(file, safe_load=True)
if file.lower().endswith(".safetensors"):
st_model = {"image_proj": {}, "ip_adapter": {}}
for key in model.keys():
if key.startswith("image_proj."):
st_model["image_proj"][key.replace("image_proj.", "")] = model[key]
elif key.startswith("ip_adapter."):
st_model["ip_adapter"][key.replace("ip_adapter.", "")] = model[key]
model = st_model
del st_model
if not "ip_adapter" in model.keys() or not model["ip_adapter"]:
raise Exception("invalid IPAdapter model {}".format(file))
if 'plusv2' in file.lower():
model["faceidplusv2"] = True
if 'unnorm' in file.lower():
model["portraitunnorm"] = True
return model
def insightface_loader(provider):
try:
from insightface.app import FaceAnalysis
except ImportError as e:
raise Exception(e)
path = os.path.join(folder_paths.models_dir, "insightface")
model = FaceAnalysis(name="buffalo_l", root=path, providers=[provider + 'ExecutionProvider',])
model.prepare(ctx_id=0, det_size=(640, 640))
return model
def encode_image_masked(clip_vision, image, mask=None, batch_size=0):
model_management.load_model_gpu(clip_vision.patcher)
outputs = Output()
if batch_size == 0:
batch_size = image.shape[0]
elif batch_size > image.shape[0]:
batch_size = image.shape[0]
image_batch = torch.split(image, batch_size, dim=0)
for img in image_batch:
img = img.to(clip_vision.load_device)
pixel_values = clip_preprocess(img.to(clip_vision.load_device)).float()
# TODO: support for multiple masks
if mask is not None:
pixel_values = pixel_values * mask.to(clip_vision.load_device)
out = clip_vision.model(pixel_values=pixel_values, intermediate_output=-2)
if not hasattr(outputs, "last_hidden_state"):
outputs["last_hidden_state"] = out[0].to(model_management.intermediate_device())
outputs["image_embeds"] = out[2].to(model_management.intermediate_device())
outputs["penultimate_hidden_states"] = out[1].to(model_management.intermediate_device())
else:
outputs["last_hidden_state"] = torch.cat((outputs["last_hidden_state"], out[0].to(model_management.intermediate_device())), dim=0)
outputs["image_embeds"] = torch.cat((outputs["image_embeds"], out[2].to(model_management.intermediate_device())), dim=0)
outputs["penultimate_hidden_states"] = torch.cat((outputs["penultimate_hidden_states"], out[1].to(model_management.intermediate_device())), dim=0)
del img, pixel_values, out
return outputs
def tensor_to_size(source, dest_size):
if isinstance(dest_size, torch.Tensor):
dest_size = dest_size.shape[0]
source_size = source.shape[0]
if source_size < dest_size:
shape = [dest_size - source_size] + [1]*(source.dim()-1)
source = torch.cat((source, source[-1:].repeat(shape)), dim=0)
elif source_size > dest_size:
source = source[:dest_size]
return source
def min_(tensor_list):
# return the element-wise min of the tensor list.
x = torch.stack(tensor_list)
mn = x.min(axis=0)[0]
return torch.clamp(mn, min=0)
def max_(tensor_list):
# return the element-wise max of the tensor list.
x = torch.stack(tensor_list)
mx = x.max(axis=0)[0]
return torch.clamp(mx, max=1)
# From https://github.com/Jamy-L/Pytorch-Contrast-Adaptive-Sharpening/
def contrast_adaptive_sharpening(image, amount):
img = T.functional.pad(image, (1, 1, 1, 1)).cpu()
a = img[..., :-2, :-2]
b = img[..., :-2, 1:-1]
c = img[..., :-2, 2:]
d = img[..., 1:-1, :-2]
e = img[..., 1:-1, 1:-1]
f = img[..., 1:-1, 2:]
g = img[..., 2:, :-2]
h = img[..., 2:, 1:-1]
i = img[..., 2:, 2:]
# Computing contrast
cross = (b, d, e, f, h)
mn = min_(cross)
mx = max_(cross)
diag = (a, c, g, i)
mn2 = min_(diag)
mx2 = max_(diag)
mx = mx + mx2
mn = mn + mn2
# Computing local weight
inv_mx = torch.reciprocal(mx)
amp = inv_mx * torch.minimum(mn, (2 - mx))
# scaling
amp = torch.sqrt(amp)
w = - amp * (amount * (1/5 - 1/8) + 1/8)
div = torch.reciprocal(1 + 4*w)
output = ((b + d + f + h)*w + e) * div
output = torch.nan_to_num(output)
output = output.clamp(0, 1)
return output
def tensor_to_image(tensor):
image = tensor.mul(255).clamp(0, 255).byte().cpu()
image = image[..., [2, 1, 0]].numpy()
return image
def image_to_tensor(image):
tensor = torch.clamp(torch.from_numpy(image).float() / 255., 0, 1)
tensor = tensor[..., [2, 1, 0]]
return tensor
-751
View File
@@ -1,751 +0,0 @@
import torch
import contextlib
import os
import math
import comfy.utils
import comfy.model_management
from comfy.clip_vision import clip_preprocess
from comfy.ldm.modules.attention import optimized_attention
import folder_paths
from torch import nn
from PIL import Image
import torch.nn.functional as F
import torchvision.transforms as TT
# set the models directory backward compatible
GLOBAL_MODELS_DIR = os.path.join(folder_paths.models_dir, "ipadapter")
MODELS_DIR = GLOBAL_MODELS_DIR if os.path.isdir(GLOBAL_MODELS_DIR) else os.path.join(os.path.dirname(os.path.realpath(__file__)), "models")
if "ipadapter" not in folder_paths.folder_names_and_paths:
folder_paths.folder_names_and_paths["ipadapter"] = ([MODELS_DIR], folder_paths.supported_pt_extensions)
else:
folder_paths.folder_names_and_paths["ipadapter"][1].update(folder_paths.supported_pt_extensions)
class MLPProjModelImport(torch.nn.Module):
"""SD model with image prompt"""
def __init__(self, cross_attention_dim=1024, clip_embeddings_dim=1024):
super().__init__()
self.proj = torch.nn.Sequential(
torch.nn.Linear(clip_embeddings_dim, clip_embeddings_dim),
torch.nn.GELU(),
torch.nn.Linear(clip_embeddings_dim, cross_attention_dim),
torch.nn.LayerNorm(cross_attention_dim)
)
def forward(self, image_embeds):
clip_extra_context_tokens = self.proj(image_embeds)
return clip_extra_context_tokens
class ImageProjModelImport(nn.Module):
def __init__(self, cross_attention_dim=1024, clip_embeddings_dim=1024, clip_extra_context_tokens=4):
super().__init__()
self.cross_attention_dim = cross_attention_dim
self.clip_extra_context_tokens = clip_extra_context_tokens
self.proj = nn.Linear(clip_embeddings_dim, self.clip_extra_context_tokens * cross_attention_dim)
self.norm = nn.LayerNorm(cross_attention_dim)
def forward(self, image_embeds):
embeds = image_embeds
clip_extra_context_tokens = self.proj(embeds).reshape(-1, self.clip_extra_context_tokens, self.cross_attention_dim)
clip_extra_context_tokens = self.norm(clip_extra_context_tokens)
return clip_extra_context_tokens
class To_KVImport(nn.Module):
def __init__(self, state_dict):
super().__init__()
self.to_kvs = nn.ModuleDict()
for key, value in state_dict.items():
self.to_kvs[key.replace(".weight", "").replace(".", "_")] = nn.Linear(value.shape[1], value.shape[0], bias=False)
self.to_kvs[key.replace(".weight", "").replace(".", "_")].weight.data = value
def FeedForward(dim, mult=4):
inner_dim = int(dim * mult)
return nn.Sequential(
nn.LayerNorm(dim),
nn.Linear(dim, inner_dim, bias=False),
nn.GELU(),
nn.Linear(inner_dim, dim, bias=False),
)
class PerceiverAttention(nn.Module):
def __init__(self, *, dim, dim_head=64, heads=8):
super().__init__()
self.scale = dim_head**-0.5
self.dim_head = dim_head
self.heads = heads
inner_dim = dim_head * heads
self.norm1 = nn.LayerNorm(dim)
self.norm2 = nn.LayerNorm(dim)
self.to_q = nn.Linear(dim, inner_dim, bias=False)
self.to_kv = nn.Linear(dim, inner_dim * 2, bias=False)
self.to_out = nn.Linear(inner_dim, dim, bias=False)
def forward(self, x, latents):
"""
Args:
x (torch.Tensor): image features
shape (b, n1, D)
latent (torch.Tensor): latent features
shape (b, n2, D)
"""
x = self.norm1(x)
latents = self.norm2(latents)
b, l, _ = latents.shape
q = self.to_q(latents)
kv_input = torch.cat((x, latents), dim=-2)
k, v = self.to_kv(kv_input).chunk(2, dim=-1)
q = reshape_tensor(q, self.heads)
k = reshape_tensor(k, self.heads)
v = reshape_tensor(v, self.heads)
# attention
scale = 1 / math.sqrt(math.sqrt(self.dim_head))
weight = (q * scale) @ (k * scale).transpose(-2, -1) # More stable with f16 than dividing afterwards
weight = torch.softmax(weight.float(), dim=-1).type(weight.dtype)
out = weight @ v
out = out.permute(0, 2, 1, 3).reshape(b, l, -1)
return self.to_out(out)
def reshape_tensor(x, heads):
bs, length, width = x.shape
#(bs, length, width) --> (bs, length, n_heads, dim_per_head)
x = x.view(bs, length, heads, -1)
# (bs, length, n_heads, dim_per_head) --> (bs, n_heads, length, dim_per_head)
x = x.transpose(1, 2)
# (bs, n_heads, length, dim_per_head) --> (bs*n_heads, length, dim_per_head)
x = x.reshape(bs, heads, length, -1)
return x
def set_model_patch_replace(model, patch_kwargs, key):
to = model.model_options["transformer_options"]
if "patches_replace" not in to:
to["patches_replace"] = {}
if "attn2" not in to["patches_replace"]:
to["patches_replace"]["attn2"] = {}
if key not in to["patches_replace"]["attn2"]:
patch = CrossAttentionPatchImport(**patch_kwargs)
to["patches_replace"]["attn2"][key] = patch
else:
to["patches_replace"]["attn2"][key].set_new_condition(**patch_kwargs)
def image_add_noise(image, noise):
image = image.permute([0,3,1,2])
torch.manual_seed(0) # use a fixed random for reproducible results
transforms = TT.Compose([
TT.CenterCrop(min(image.shape[2], image.shape[3])),
TT.Resize((224, 224), interpolation=TT.InterpolationMode.BICUBIC, antialias=True),
TT.ElasticTransform(alpha=75.0, sigma=noise*3.5), # shuffle the image
TT.RandomVerticalFlip(p=1.0), # flip the image to change the geometry even more
TT.RandomHorizontalFlip(p=1.0),
])
image = transforms(image.cpu())
image = image.permute([0,2,3,1])
image = image + ((0.25*(1-noise)+0.05) * torch.randn_like(image) ) # add further random noise
return image
def zeroed_hidden_states(clip_vision, batch_size):
image = torch.zeros([batch_size, 224, 224, 3])
comfy.model_management.load_model_gpu(clip_vision.patcher)
pixel_values = clip_preprocess(image.to(clip_vision.load_device))
if clip_vision.dtype != torch.float32:
precision_scope = torch.autocast
else:
precision_scope = lambda a, b: contextlib.nullcontext(a)
with precision_scope(comfy.model_management.get_autocast_device(clip_vision.load_device), torch.float32):
outputs = clip_vision.model(pixel_values, intermediate_output=-2)
# we only need the penultimate hidden states
outputs = outputs[1].to(comfy.model_management.intermediate_device())
return outputs
def min_(tensor_list):
# return the element-wise min of the tensor list.
x = torch.stack(tensor_list)
mn = x.min(axis=0)[0]
return torch.clamp(mn, min=0)
def max_(tensor_list):
# return the element-wise max of the tensor list.
x = torch.stack(tensor_list)
mx = x.max(axis=0)[0]
return torch.clamp(mx, max=1)
# From https://github.com/Jamy-L/Pytorch-Contrast-Adaptive-Sharpening/
def contrast_adaptive_sharpening(image, amount):
img = F.pad(image, pad=(1, 1, 1, 1)).cpu()
a = img[..., :-2, :-2]
b = img[..., :-2, 1:-1]
c = img[..., :-2, 2:]
d = img[..., 1:-1, :-2]
e = img[..., 1:-1, 1:-1]
f = img[..., 1:-1, 2:]
g = img[..., 2:, :-2]
h = img[..., 2:, 1:-1]
i = img[..., 2:, 2:]
# Computing contrast
cross = (b, d, e, f, h)
mn = min_(cross)
mx = max_(cross)
diag = (a, c, g, i)
mn2 = min_(diag)
mx2 = max_(diag)
mx = mx + mx2
mn = mn + mn2
# Computing local weight
inv_mx = torch.reciprocal(mx)
amp = inv_mx * torch.minimum(mn, (2 - mx))
# scaling
amp = torch.sqrt(amp)
w = - amp * (amount * (1/5 - 1/8) + 1/8)
div = torch.reciprocal(1 + 4*w)
output = ((b + d + f + h)*w + e) * div
output = output.clamp(0, 1)
output = torch.nan_to_num(output)
return (output)
class IPAdapterImport(nn.Module):
def __init__(self, ipadapter_model, cross_attention_dim=1024, output_cross_attention_dim=1024, clip_embeddings_dim=1024, clip_extra_context_tokens=4, is_sdxl=False, is_plus=False, is_full=False):
super().__init__()
self.clip_embeddings_dim = clip_embeddings_dim
self.cross_attention_dim = cross_attention_dim
self.output_cross_attention_dim = output_cross_attention_dim
self.clip_extra_context_tokens = clip_extra_context_tokens
self.is_sdxl = is_sdxl
self.is_full = is_full
self.image_proj_model = self.init_proj() if not is_plus else self.init_proj_plus()
self.image_proj_model.load_state_dict(ipadapter_model["image_proj"])
self.ip_layers = To_KVImport(ipadapter_model["ip_adapter"])
def init_proj(self):
image_proj_model = ImageProjModelImport(
cross_attention_dim=self.cross_attention_dim,
clip_embeddings_dim=self.clip_embeddings_dim,
clip_extra_context_tokens=self.clip_extra_context_tokens
)
return image_proj_model
def init_proj_plus(self):
if self.is_full:
image_proj_model = MLPProjModelImport(
cross_attention_dim=self.cross_attention_dim,
clip_embeddings_dim=self.clip_embeddings_dim
)
else:
image_proj_model = ResamplerImport(
dim=self.cross_attention_dim,
depth=4,
dim_head=64,
heads=20 if self.is_sdxl else 12,
num_queries=self.clip_extra_context_tokens,
embedding_dim=self.clip_embeddings_dim,
output_dim=self.output_cross_attention_dim,
ff_mult=4
)
return image_proj_model
@torch.inference_mode()
def get_image_embeds(self, clip_embed, clip_embed_zeroed):
image_prompt_embeds = self.image_proj_model(clip_embed)
uncond_image_prompt_embeds = self.image_proj_model(clip_embed_zeroed)
return image_prompt_embeds, uncond_image_prompt_embeds
class CrossAttentionPatchImport:
# forward for patching
def __init__(self, weight, ipadapter, device, dtype, number, cond, uncond, weight_type, mask=None, sigma_start=0.0, sigma_end=1.0, unfold_batch=False):
self.weights = [weight]
self.ipadapters = [ipadapter]
self.conds = [cond]
self.unconds = [uncond]
self.device = 'cuda' if 'cuda' in device.type else 'cpu'
self.dtype = dtype if 'cuda' in self.device else torch.bfloat16
self.number = number
self.weight_type = [weight_type]
self.masks = [mask]
self.sigma_start = [sigma_start]
self.sigma_end = [sigma_end]
self.unfold_batch = [unfold_batch]
self.k_key = str(self.number*2+1) + "_to_k_ip"
self.v_key = str(self.number*2+1) + "_to_v_ip"
def set_new_condition(self, weight, ipadapter, device, dtype, number, cond, uncond, weight_type, mask=None, sigma_start=0.0, sigma_end=1.0, unfold_batch=False):
self.weights.append(weight)
self.ipadapters.append(ipadapter)
self.conds.append(cond)
self.unconds.append(uncond)
self.masks.append(mask)
self.device = 'cuda' if 'cuda' in device.type else 'cpu'
self.dtype = dtype if 'cuda' in self.device else torch.bfloat16
self.weight_type.append(weight_type)
self.sigma_start.append(sigma_start)
self.sigma_end.append(sigma_end)
self.unfold_batch.append(unfold_batch)
def __call__(self, n, context_attn2, value_attn2, extra_options):
org_dtype = n.dtype
cond_or_uncond = extra_options["cond_or_uncond"]
sigma = extra_options["sigmas"][0].item() if 'sigmas' in extra_options else 999999999.9
# extra options for AnimateDiff
ad_params = extra_options['ad_params'] if "ad_params" in extra_options else None
with torch.autocast(device_type=self.device, dtype=self.dtype):
q = n
k = context_attn2
v = value_attn2
b = q.shape[0]
qs = q.shape[1]
batch_prompt = b // len(cond_or_uncond)
out = optimized_attention(q, k, v, extra_options["n_heads"])
_, _, lh, lw = extra_options["original_shape"]
for weight, cond, uncond, ipadapter, mask, weight_type, sigma_start, sigma_end, unfold_batch in zip(self.weights, self.conds, self.unconds, self.ipadapters, self.masks, self.weight_type, self.sigma_start, self.sigma_end, self.unfold_batch):
if sigma > sigma_start or sigma < sigma_end:
continue
if unfold_batch and cond.shape[0] > 1:
# Check AnimateDiff context window
if ad_params is not None and ad_params["sub_idxs"] is not None:
# if images length matches or exceeds full_length get sub_idx images
if cond.shape[0] >= ad_params["full_length"]:
cond = torch.Tensor(cond[ad_params["sub_idxs"]])
uncond = torch.Tensor(uncond[ad_params["sub_idxs"]])
# otherwise, need to do more to get proper sub_idxs masks
else:
# check if images length matches full_length - if not, make it match
if cond.shape[0] < ad_params["full_length"]:
cond = torch.cat((cond, cond[-1:].repeat((ad_params["full_length"]-cond.shape[0], 1, 1))), dim=0)
uncond = torch.cat((uncond, uncond[-1:].repeat((ad_params["full_length"]-uncond.shape[0], 1, 1))), dim=0)
# if we have too many remove the excess (should not happen, but just in case)
if cond.shape[0] > ad_params["full_length"]:
cond = cond[:ad_params["full_length"]]
uncond = uncond[:ad_params["full_length"]]
cond = cond[ad_params["sub_idxs"]]
uncond = uncond[ad_params["sub_idxs"]]
# if we don't have enough reference images repeat the last one until we reach the right size
if cond.shape[0] < batch_prompt:
cond = torch.cat((cond, cond[-1:].repeat((batch_prompt-cond.shape[0], 1, 1))), dim=0)
uncond = torch.cat((uncond, uncond[-1:].repeat((batch_prompt-uncond.shape[0], 1, 1))), dim=0)
# if we have too many remove the exceeding
elif cond.shape[0] > batch_prompt:
cond = cond[:batch_prompt]
uncond = uncond[:batch_prompt]
k_cond = ipadapter.ip_layers.to_kvs[self.k_key](cond)
k_uncond = ipadapter.ip_layers.to_kvs[self.k_key](uncond)
v_cond = ipadapter.ip_layers.to_kvs[self.v_key](cond)
v_uncond = ipadapter.ip_layers.to_kvs[self.v_key](uncond)
else:
k_cond = ipadapter.ip_layers.to_kvs[self.k_key](cond).repeat(batch_prompt, 1, 1)
k_uncond = ipadapter.ip_layers.to_kvs[self.k_key](uncond).repeat(batch_prompt, 1, 1)
v_cond = ipadapter.ip_layers.to_kvs[self.v_key](cond).repeat(batch_prompt, 1, 1)
v_uncond = ipadapter.ip_layers.to_kvs[self.v_key](uncond).repeat(batch_prompt, 1, 1)
if weight_type.startswith("linear"):
ip_k = torch.cat([(k_cond, k_uncond)[i] for i in cond_or_uncond], dim=0) * weight
ip_v = torch.cat([(v_cond, v_uncond)[i] for i in cond_or_uncond], dim=0) * weight
else:
ip_k = torch.cat([(k_cond, k_uncond)[i] for i in cond_or_uncond], dim=0)
ip_v = torch.cat([(v_cond, v_uncond)[i] for i in cond_or_uncond], dim=0)
if weight_type.startswith("channel"):
# code by Lvmin Zhang at Stanford University as also seen on Fooocus IPAdapter implementation
# please read licensing notes https://github.com/lllyasviel/Fooocus/blob/main/fooocus_extras/ip_adapter.py#L225
ip_v_mean = torch.mean(ip_v, dim=1, keepdim=True)
ip_v_offset = ip_v - ip_v_mean
_, _, C = ip_k.shape
channel_penalty = float(C) / 1280.0
W = weight * channel_penalty
ip_k = ip_k * W
ip_v = ip_v_offset + ip_v_mean * W
out_ip = optimized_attention(q, ip_k, ip_v, extra_options["n_heads"])
if weight_type.startswith("original"):
out_ip = out_ip * weight
if mask is not None:
# TODO: needs checking
mask_h = max(1, round(lh / math.sqrt(lh * lw / qs)))
mask_w = qs // mask_h
# check if using AnimateDiff and sliding context window
if (mask.shape[0] > 1 and ad_params is not None and ad_params["sub_idxs"] is not None):
# if mask length matches or exceeds full_length, just get sub_idx masks, resize, and continue
if mask.shape[0] >= ad_params["full_length"]:
mask_downsample = torch.Tensor(mask[ad_params["sub_idxs"]])
mask_downsample = F.interpolate(mask_downsample.unsqueeze(1), size=(mask_h, mask_w), mode="bicubic").squeeze(1)
# otherwise, need to do more to get proper sub_idxs masks
else:
# resize to needed attention size (to save on memory)
mask_downsample = F.interpolate(mask.unsqueeze(1), size=(mask_h, mask_w), mode="bicubic").squeeze(1)
# check if mask length matches full_length - if not, make it match
if mask_downsample.shape[0] < ad_params["full_length"]:
mask_downsample = torch.cat((mask_downsample, mask_downsample[-1:].repeat((ad_params["full_length"]-mask_downsample.shape[0], 1, 1))), dim=0)
# if we have too many remove the excess (should not happen, but just in case)
if mask_downsample.shape[0] > ad_params["full_length"]:
mask_downsample = mask_downsample[:ad_params["full_length"]]
# now, select sub_idxs masks
mask_downsample = mask_downsample[ad_params["sub_idxs"]]
# otherwise, perform usual mask interpolation
else:
mask_downsample = F.interpolate(mask.unsqueeze(1), size=(mask_h, mask_w), mode="bicubic").squeeze(1)
# if we don't have enough masks repeat the last one until we reach the right size
if mask_downsample.shape[0] < batch_prompt:
mask_downsample = torch.cat((mask_downsample, mask_downsample[-1:, :, :].repeat((batch_prompt-mask_downsample.shape[0], 1, 1))), dim=0)
# if we have too many remove the exceeding
elif mask_downsample.shape[0] > batch_prompt:
mask_downsample = mask_downsample[:batch_prompt, :, :]
# repeat the masks
mask_downsample = mask_downsample.repeat(len(cond_or_uncond), 1, 1)
mask_downsample = mask_downsample.view(mask_downsample.shape[0], -1, 1).repeat(1, 1, out.shape[2])
out_ip = out_ip * mask_downsample
out = out + out_ip
return out.to(dtype=org_dtype)
class IPAdapterApplyImport:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"ipadapter": ("IPADAPTER", ),
"clip_vision": ("CLIP_VISION",),
"image": ("IMAGE",),
"model": ("MODEL", ),
"weight": ("FLOAT", { "default": 1.0, "min": -1, "max": 3, "step": 0.05 }),
"noise": ("FLOAT", { "default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01 }),
"weight_type": (["original", "linear", "channel penalty"], ),
"start_at": ("FLOAT", { "default": 0.0, "min": 0.0, "max": 1.0, "step": 0.001 }),
"end_at": ("FLOAT", { "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.001 }),
"unfold_batch": ("BOOLEAN", { "default": False }),
},
"optional": {
"attn_mask": ("MASK",),
}
}
RETURN_TYPES = ("MODEL",)
FUNCTION = "apply_ipadapter"
CATEGORY = "ipadapter"
def apply_ipadapter(self, ipadapter, model, weight, clip_vision=None, image=None, weight_type="original", noise=None, embeds=None, attn_mask=None, start_at=0.0, end_at=1.0, unfold_batch=False):
self.dtype = model.model.diffusion_model.dtype
self.device = comfy.model_management.get_torch_device()
self.weight = weight
self.is_full = "proj.0.weight" in ipadapter["image_proj"]
self.is_plus = self.is_full or "latents" in ipadapter["image_proj"]
output_cross_attention_dim = ipadapter["ip_adapter"]["1.to_k_ip.weight"].shape[1]
self.is_sdxl = output_cross_attention_dim == 2048
cross_attention_dim = 1280 if self.is_plus and self.is_sdxl else output_cross_attention_dim
clip_extra_context_tokens = 16 if self.is_plus else 4
if embeds is not None:
embeds = torch.unbind(embeds)
clip_embed = embeds[0].cpu()
clip_embed_zeroed = embeds[1].cpu()
else:
if image.shape[1] != image.shape[2]:
print("\033[33mINFO: the IPAdapter reference image is not a square, CLIPImageProcessor will resize and crop it at the center. If the main focus of the picture is not in the middle the result might not be what you are expecting.\033[0m")
clip_embed = clip_vision.encode_image(image)
neg_image = image_add_noise(image, noise) if noise > 0 else None
if self.is_plus:
clip_embed = clip_embed.penultimate_hidden_states
if noise > 0:
clip_embed_zeroed = clip_vision.encode_image(neg_image).penultimate_hidden_states
else:
clip_embed_zeroed = zeroed_hidden_states(clip_vision, image.shape[0])
else:
clip_embed = clip_embed.image_embeds
if noise > 0:
clip_embed_zeroed = clip_vision.encode_image(neg_image).image_embeds
else:
clip_embed_zeroed = torch.zeros_like(clip_embed)
clip_embeddings_dim = clip_embed.shape[-1]
self.ipadapter = IPAdapterImport(
ipadapter,
cross_attention_dim=cross_attention_dim,
output_cross_attention_dim=output_cross_attention_dim,
clip_embeddings_dim=clip_embeddings_dim,
clip_extra_context_tokens=clip_extra_context_tokens,
is_sdxl=self.is_sdxl,
is_plus=self.is_plus,
is_full=self.is_full,
)
self.ipadapter.to(self.device, dtype=self.dtype)
image_prompt_embeds, uncond_image_prompt_embeds = self.ipadapter.get_image_embeds(clip_embed.to(self.device, self.dtype), clip_embed_zeroed.to(self.device, self.dtype))
image_prompt_embeds = image_prompt_embeds.to(self.device, dtype=self.dtype)
uncond_image_prompt_embeds = uncond_image_prompt_embeds.to(self.device, dtype=self.dtype)
work_model = model.clone()
if attn_mask is not None:
attn_mask = attn_mask.to(self.device)
sigma_start = model.model.model_sampling.percent_to_sigma(start_at)
sigma_end = model.model.model_sampling.percent_to_sigma(end_at)
patch_kwargs = {
"number": 0,
"weight": self.weight,
"ipadapter": self.ipadapter,
"device": self.device,
"dtype": self.dtype,
"cond": image_prompt_embeds,
"uncond": uncond_image_prompt_embeds,
"weight_type": weight_type,
"mask": attn_mask,
"sigma_start": sigma_start,
"sigma_end": sigma_end,
"unfold_batch": unfold_batch,
}
if not self.is_sdxl:
for id in [1,2,4,5,7,8]: # id of input_blocks that have cross attention
set_model_patch_replace(work_model, patch_kwargs, ("input", id))
patch_kwargs["number"] += 1
for id in [3,4,5,6,7,8,9,10,11]: # id of output_blocks that have cross attention
set_model_patch_replace(work_model, patch_kwargs, ("output", id))
patch_kwargs["number"] += 1
set_model_patch_replace(work_model, patch_kwargs, ("middle", 0))
else:
for id in [4,5,7,8]: # id of input_blocks that have cross attention
block_indices = range(2) if id in [4, 5] else range(10) # transformer_depth
for index in block_indices:
set_model_patch_replace(work_model, patch_kwargs, ("input", id, index))
patch_kwargs["number"] += 1
for id in range(6): # id of output_blocks that have cross attention
block_indices = range(2) if id in [3, 4, 5] else range(10) # transformer_depth
for index in block_indices:
set_model_patch_replace(work_model, patch_kwargs, ("output", id, index))
patch_kwargs["number"] += 1
for index in range(10):
set_model_patch_replace(work_model, patch_kwargs, ("middle", 0, index))
patch_kwargs["number"] += 1
return (work_model, )
def prep_image(image, interpolation="LANCZOS", crop_position="center", sharpening=0.0):
_, oh, ow, _ = image.shape
output = image.permute([0,3,1,2])
if "pad" in crop_position:
target_length = max(oh, ow)
pad_l = (target_length - ow) // 2
pad_r = (target_length - ow) - pad_l
pad_t = (target_length - oh) // 2
pad_b = (target_length - oh) - pad_t
output = F.pad(output, (pad_l, pad_r, pad_t, pad_b), value=0, mode="constant")
else:
crop_size = min(oh, ow)
x = (ow-crop_size) // 2
y = (oh-crop_size) // 2
if "top" in crop_position:
y = 0
elif "bottom" in crop_position:
y = oh-crop_size
elif "left" in crop_position:
x = 0
elif "right" in crop_position:
x = ow-crop_size
x2 = x+crop_size
y2 = y+crop_size
# crop
output = output[:, :, y:y2, x:x2]
# resize (apparently PIL resize is better than tourchvision interpolate)
imgs = []
for i in range(output.shape[0]):
img = TT.ToPILImage()(output[i])
img = img.resize((224,224), resample=Image.Resampling[interpolation])
imgs.append(TT.ToTensor()(img))
output = torch.stack(imgs, dim=0)
if sharpening > 0:
output = contrast_adaptive_sharpening(output, sharpening)
output = output.permute([0,2,3,1])
return (output,)
class ResamplerImport(nn.Module):
def __init__(
self,
dim=1024,
depth=8,
dim_head=64,
heads=16,
num_queries=8,
embedding_dim=768,
output_dim=1024,
ff_mult=4,
):
super().__init__()
self.latents = nn.Parameter(torch.randn(1, num_queries, dim) / dim**0.5)
self.proj_in = nn.Linear(embedding_dim, dim)
self.proj_out = nn.Linear(dim, output_dim)
self.norm_out = nn.LayerNorm(output_dim)
self.layers = nn.ModuleList([])
for _ in range(depth):
self.layers.append(
nn.ModuleList(
[
PerceiverAttention(dim=dim, dim_head=dim_head, heads=heads),
FeedForward(dim=dim, mult=ff_mult),
]
)
)
def forward(self, x):
latents = self.latents.repeat(x.size(0), 1, 1)
x = self.proj_in(x)
for attn, ff in self.layers:
latents = attn(x, latents) + latents
latents = ff(latents) + latents
latents = self.proj_out(latents)
return self.norm_out(latents)
class IPAdapterEncoderImport:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"clip_vision": ("CLIP_VISION",),
"image_1": ("IMAGE",),
"ipadapter_plus": ("BOOLEAN", { "default": False }),
"noise": ("FLOAT", { "default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01 }),
"weight_1": ("FLOAT", { "default": 1.0, "min": 0, "max": 1.0, "step": 0.01 }),
},
"optional": {
"image_2": ("IMAGE",),
"image_3": ("IMAGE",),
"image_4": ("IMAGE",),
"weight_2": ("FLOAT", { "default": 1.0, "min": 0, "max": 1.0, "step": 0.01 }),
"weight_3": ("FLOAT", { "default": 1.0, "min": 0, "max": 1.0, "step": 0.01 }),
"weight_4": ("FLOAT", { "default": 1.0, "min": 0, "max": 1.0, "step": 0.01 }),
}
}
RETURN_TYPES = ("EMBEDS",)
FUNCTION = "preprocess"
CATEGORY = "ipadapter"
def preprocess(self, clip_vision, image_1, ipadapter_plus, noise, weight_1, image_2=None, image_3=None, image_4=None, weight_2=1.0, weight_3=1.0, weight_4=1.0):
weight_1 *= (0.1 + (weight_1 - 0.1))
weight_1 = 1.19e-05 if weight_1 <= 1.19e-05 else weight_1
weight_2 *= (0.1 + (weight_2 - 0.1))
weight_2 = 1.19e-05 if weight_2 <= 1.19e-05 else weight_2
weight_3 *= (0.1 + (weight_3 - 0.1))
weight_3 = 1.19e-05 if weight_3 <= 1.19e-05 else weight_3
weight_4 *= (0.1 + (weight_4 - 0.1))
weight_5 = 1.19e-05 if weight_4 <= 1.19e-05 else weight_4
image = image_1
weight = [weight_1]*image_1.shape[0]
if image_2 is not None:
if image_1.shape[1:] != image_2.shape[1:]:
image_2 = comfy.utils.common_upscale(image_2.movedim(-1,1), image.shape[2], image.shape[1], "bilinear", "center").movedim(1,-1)
image = torch.cat((image, image_2), dim=0)
weight += [weight_2]*image_2.shape[0]
if image_3 is not None:
if image.shape[1:] != image_3.shape[1:]:
image_3 = comfy.utils.common_upscale(image_3.movedim(-1,1), image.shape[2], image.shape[1], "bilinear", "center").movedim(1,-1)
image = torch.cat((image, image_3), dim=0)
weight += [weight_3]*image_3.shape[0]
if image_4 is not None:
if image.shape[1:] != image_4.shape[1:]:
image_4 = comfy.utils.common_upscale(image_4.movedim(-1,1), image.shape[2], image.shape[1], "bilinear", "center").movedim(1,-1)
image = torch.cat((image, image_4), dim=0)
weight += [weight_4]*image_4.shape[0]
clip_embed = clip_vision.encode_image(image)
neg_image = image_add_noise(image, noise) if noise > 0 else None
if ipadapter_plus:
clip_embed = clip_embed.penultimate_hidden_states
if noise > 0:
clip_embed_zeroed = clip_vision.encode_image(neg_image).penultimate_hidden_states
else:
clip_embed_zeroed = zeroed_hidden_states(clip_vision, image.shape[0])
else:
clip_embed = clip_embed.image_embeds
if noise > 0:
clip_embed_zeroed = clip_vision.encode_image(neg_image).image_embeds
else:
clip_embed_zeroed = torch.zeros_like(clip_embed)
if any(e != 1.0 for e in weight):
weight = torch.tensor(weight).unsqueeze(-1) if not ipadapter_plus else torch.tensor(weight).unsqueeze(-1).unsqueeze(-1)
clip_embed = clip_embed * weight
output = torch.stack((clip_embed, clip_embed_zeroed))
return( output, )
class IPAdapterBatchEmbedsImport:
@classmethod
def INPUT_TYPES(s):
return {"required": {
"embed1": ("EMBEDS",),
"embed2": ("EMBEDS",),
}}
RETURN_TYPES = ("EMBEDS",)
FUNCTION = "batch"
CATEGORY = "ipadapter"
def batch(self, embed1, embed2):
output = torch.cat((embed1, embed2), dim=1)
return (output, )
+1
View File
@@ -0,0 +1 @@
matplotlib