429 Commits
Author SHA1 Message Date
kijai 698d958085 Update setgetnodes.js 2025-09-05 01:35:42 +03:00
kijai 0adab07d1a Update lora_nodes.py 2025-09-01 19:02:21 +03:00
kijai ee5589c9ea Update lora_nodes.py 2025-09-01 18:31:29 +03:00
kijai d8b8c637fb Add LoraReduceRank
for testing
based on:
https://github.com/kohya-ss/sd-scripts/blob/main/networks/resize_lora.py
2025-09-01 17:11:35 +03:00
kijai ba9153cb06 Allow disabling clamping on lora extract 2025-08-23 16:21:30 +03:00
kijai e2ce0843d1 bump version 2025-08-20 21:07:03 +03:00
kijai b37122a9fd Add LazySwitchKJ 2025-08-20 21:06:39 +03:00
kijai 6dfd2c2420 I have no idea... 2025-08-20 14:35:18 +03:00
kijai e435e999e4 Add GetLatentSizeAndCount 2025-08-20 12:36:50 +03:00
kijai 876a6dd292 Add LoadVideosFromFolder
This extends and requires https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite
2025-08-18 11:55:26 +03:00
kijai 2f7300dc54 Add bbox_format option for bbox visualize node 2025-08-15 12:46:02 +03:00
kijai 87d0cf42db fix dit lora block loader on loras with diff keys 2025-08-06 23:15:12 +03:00
kijai dafbcae4e6 Add option to load extra state dict with a diffusion model
Also add node DiffusionModelSelector to easily select the path. This can be used to add standalone VACE module to any WanModel
2025-08-06 20:09:14 +03:00
kijai 331260d908 Update model_optimization_nodes.py 2025-08-06 09:45:54 +03:00
kijai 984c7bb276 remove print 2025-08-06 00:24:37 +03:00
kijai 504044f181 Add TorchCompileModelQwenImage
Mostly to limit compile to transformer blocks only for less recompiles
2025-08-06 00:09:15 +03:00
kijai d382efd7e7 Support Qwen image with sageattn patch 2025-08-05 23:52:54 +03:00
kijai fbdb08f9d6 NAG for batches 2025-08-02 21:02:48 +03:00
kijai a6b867b63a Fix JoinStringMulti 2025-07-28 00:00:04 +03:00
kijai b7f337fe3a Update lora_nodes.py 2025-07-27 12:26:14 +03:00
kijai 37a0973b47 Allow RBG strings 2025-07-21 14:34:56 +03:00
Jukka Seppänen 0d909572e2 Merge pull request #340 from netjerikh/multithread_ColorMatch
Added multithread option to ColorMatch node
2025-07-18 23:03:46 +03:00
netjerikh 0d2334de6d Added multithread option to ColorMatch node 2025-07-18 11:54:19 +03:00
kijai ad37ce656c Make multi-input node inputs optional 2025-07-16 16:54:06 +03:00
kijai ab8cac5396 Add LoraExtractKJ
Improved Lora extraction node
- build in diff substraction
- lowrank algo for quick extraction
- dtype selection
2025-07-16 14:02:31 +03:00
kijai 730edb08c1 Change JoinStrings inputs to optional 2025-07-16 11:12:21 +03:00
kijai fca4aa2a15 ImageResizeKJv2: output padded area as mask if no input mask to resize 2025-07-15 14:57:57 +03:00
kijai d9425173e7 Add ImageBatchJoinWithTransition 2025-07-14 16:40:23 +03:00
kijai f20cb4c108 Update image_nodes.py 2025-07-14 15:27:53 +03:00
kijai 4549812bcc Update CrossFadeImages to allow joining different batch sizes 2025-07-14 15:24:05 +03:00
kijai 9ea455afd6 bump version 2025-07-14 09:29:27 +03:00
kijai 22e722bafe Update image_nodes.py 2025-07-10 18:59:17 +03:00
kijai cabfcb2245 ImageResizeKJv2: Allow changing pad direction, add output info display 2025-07-10 18:56:25 +03:00
kijai 479cde218b Update nodes.py 2025-07-08 13:56:58 +03:00
kijai 5c19bd835a typo 2025-07-06 23:57:29 +03:00
kijai 6a0c755c56 these should not be output nodes... 2025-07-06 23:06:20 +03:00
kijai ee0eb6696b Use node_typing for ANY -inputs 2025-07-04 13:04:18 +03:00
kijai 40d9d84b68 Update mask_nodes.py 2025-07-03 20:45:10 +03:00
kijai 1f653a8d96 ReplaceImagesInBatch: Allow replacing only masks without images 2025-07-03 17:51:40 +03:00
kijai ff49e1b01f Add sageattn++ as selectable mode for easier testing 2025-07-01 12:26:27 +03:00
kijai 6363376a17 Add optional mask resiszing to ImageResizeKJv2, allow lanczos for mask resizing 2025-07-01 12:22:37 +03:00
kijai fbc779a7bd Allow negative values in INTConstant 2025-06-30 12:03:50 +03:00
kijai f7eb33abc8 I remembered
Should keep the id or node title methods viable, though not recommended
2025-06-18 11:03:51 +03:00
kijai 24f10ca90c Don't run WidgetToString on each queue...
I can't remember the reason for this, if it ever was necessary, it doesn't seem to be anymore...
2025-06-18 10:42:57 +03:00
kijai f9118e0bbb Bump version 2025-06-16 17:31:42 +03:00
kijai ff98412479 Check if folder contents change on LoadImagesFromFolderKJ 2025-06-16 17:31:35 +03:00
kijai b5e654103a Update spline_editor.js 2025-06-16 17:17:31 +03:00
kijai b560966027 Force using shift modifier when selecting another spline 2025-06-16 01:48:56 +03:00
kijai 7afb0f906a Update model_optimization_nodes.py 2025-06-16 00:35:53 +03:00
kijai d584c711a3 Experimental NAG node 2025-06-15 22:05:24 +03:00
kijai aeab1a7de5 sync multiple splines in speed mode 2025-06-09 18:31:38 +03:00
kijai ed8294d7fb Add new "speed" sampling mode to SplineEditor 2025-06-09 18:16:58 +03:00
kijai 1fbf4805f1 SplineEditor updates 2025-06-09 17:59:31 +03:00
kijai 3f62cf2849 Use builtin widget hiding 2025-06-09 12:03:56 +03:00
kijai dfe7a909a4 Update model_optimization_nodes.py 2025-06-09 04:30:00 +03:00
kijai e96a028254 Support VACE with TeaCache 2025-06-09 02:58:41 +03:00
kijai 5736669288 Support multiple splines on CreateShapeImageOnPath 2025-06-08 20:40:14 +03:00
kijai 87b8f85103 Fix Wan enhance a video disabling sageattention patch 2025-06-06 15:46:33 +03:00
kijai f35bb4d336 Don't return coordinates as list if only one spline 2025-06-04 21:57:21 +03:00
kijai 9682804efb remove print 2025-06-04 10:15:19 +03:00
kijai cebd0fc1c5 Bump version 2025-06-03 20:46:07 +03:00
kijai 76d67a963e Remove redundant SD3 API Node 2025-06-03 20:45:28 +03:00
kijai ea5482a6ee Initial mostly working multiple splines for SplineEditor
Probably buggy and clumsy to use, but workable and way better than having to use multiple SplineEditors...
2025-06-03 20:45:08 +03:00
Jukka Seppänen 42c7641776 Merge pull request #291 from rkfg/rel
Make web paths relative
2025-06-03 17:27:30 +03:00
Jukka Seppänen daadc345b8 Merge pull request #301 from wildminder/main
fix import
2025-06-03 17:26:01 +03:00
WildAi a879d15208 fix import 2025-06-02 20:26:05 +03:00
kijai 08a22951ed Update nodes.py 2025-05-30 20:35:54 +03:00
kijai 0b78aedaee Fix spline/point editor bug on frontend versikon 1.21.2 2025-05-30 14:57:17 +03:00
kijai 9a52b7dfd2 Update image_nodes.py 2025-05-29 20:35:18 +03:00
kijai b95b79ee82 Fix lora block loading 2025-05-28 14:31:22 +03:00
kijai 37eb7bddcb Change Flux block lora loaderr into generic DiT lora block loader and support Wan with it 2025-05-28 12:57:17 +03:00
kijai 07b804cb3f Add TorchCompileModelWanVideoV2 2025-05-21 17:38:22 +03:00
kijai 16f60e53e5 Add TorchCompileModelFluxAdvancedV2
Utilizing new ComfyUI core functions that also support LoRAs without additional patches
2025-05-21 17:17:41 +03:00
kijai 44565e9bff Add choice of device for imageresize 2025-05-20 16:50:55 +03:00
kijai aca7916352 Add modes to not do interpolation with CreateFadeMaskAdvanced 2025-05-20 16:50:42 +03:00
rkfg 12a64667ae Make web paths relative 2025-05-16 15:22:54 +03:00
kijai 5dcda71011 bugfix on deleting node connected to Set node 2025-05-13 17:10:22 +03:00
kijai bec42252c6 smarter sage patch 2025-05-07 19:08:25 +03:00
kijai ca07b9dadc Add ModelPatchTorchSettings 2025-05-07 19:00:45 +03:00
kijai cc043fcac7 bump version 2025-05-07 18:29:55 +03:00
kijai 6a35eefdd5 Update model_optimization_nodes.py 2025-05-07 17:52:47 +03:00
kijai bfb6d973fe update CheckpointLoaderKJ 2025-05-07 17:30:38 +03:00
kijai b7e5b6f1e2 Default to center crop on the deprecated resize node 2025-05-05 21:54:34 +03:00
kijai c3dc82108a Fix ConditioningMultiCombine 2025-04-25 11:19:49 +03:00
kijai c3f0a55adf Deprecate ImageResizeKJ and add ImageResizeKJ v2
Due to ComfyUI frontend changes the old node doesn't make that much sense anymore, and has issues between workflows saved with different versions of the front end.
2025-04-24 14:11:17 +03:00
kijai 086ac449a2 Update model_optimization_nodes.py 2025-04-23 20:09:09 +03:00
kijai 8dac94d9d9 Update model_optimization_nodes.py 2025-04-23 20:08:10 +03:00
kijai 4d9c73ed46 bump version 2025-04-23 18:10:32 +03:00
kijai 3e3a1a8aac Update image_nodes.py 2025-04-22 09:17:29 +03:00
kijai 366b866f8e Update image_nodes.py 2025-04-20 22:29:42 +03:00
kijai 9903cf078e Further fix for input selection nodes 2025-04-20 17:49:51 +03:00
kijai d399bc559d Add ImageBatchFilter
Node that removes "empty" frames from a batch, empty being a single color with threshold
2025-04-20 14:17:44 +03:00
kijai d41ad755ef Account for frontend counting all widgets as inputs now 2025-04-20 13:33:32 +03:00
kijai 58668a44cc Update model_optimization_nodes.py 2025-04-19 16:55:05 +03:00
kijai 9026379046 Use pre_run callback for applying sageattention patch
It's still global patch, but now applied before using the model thus better allowing disabling it for unsupported models
2025-04-19 16:26:59 +03:00
kijai 8ecf5cd05e ReplaceImagesInBatch: allow replacing masks as well 2025-04-13 16:52:23 +03:00
kijai d57154c3a8 Allow repeating masks as well with ImageBatchRepeatInterleaving 2025-04-07 17:45:45 +03:00
kijai c8f5fc0760 Update image_nodes.py 2025-04-06 11:39:21 +03:00
kijai 6c997afd5f Update image_nodes.py 2025-04-05 20:32:34 +03:00
kijai 5f16aeec88 Pad node update 2025-04-05 19:35:16 +03:00
kijai 26aea1889d Update curve_nodes.py 2025-04-05 01:54:55 +03:00
kijai 9215c919b1 Update curve_nodes.py 2025-04-05 01:37:01 +03:00
kijai 402124d5c4 Add border option to CreateShapeImageOnPath
useful for VACE bounding box control
2025-04-04 01:28:48 +03:00
kijai 91bdcfdf60 Update image_nodes.py 2025-04-03 01:12:30 +03:00
kijai 843805b1fb Update image_nodes.py 2025-04-03 01:06:51 +03:00
kijai 57195496fa Update image_nodes.py 2025-04-03 00:50:04 +03:00
kijai 2aa4da0f58 Round the Float constant to avoid javascript floating precision point issues 2025-04-02 10:43:53 +03:00
kijai 18a5565b5c Update image_nodes.py 2025-04-02 02:47:03 +03:00
kijai 59bd92ff49 Add PadImageBatchInterleaved 2025-04-02 02:07:47 +03:00
kijai 0addfc6101 Maybe fix shape mismatch with SLG and I2V when doing batched cfg 2025-03-29 12:51:49 +02:00
kijai 916461c432 fix zero_init 2025-03-29 11:28:08 +02:00
kijai fc59fff1b5 Merge branch 'main' of https://github.com/kijai/ComfyUI-KJNodes 2025-03-28 17:24:45 +02:00
kijai 58a06698f0 small tweaks 2025-03-28 17:24:44 +02:00
Jukka Seppänen b3269479f2 Merge pull request #250 from ComfyNodePRs/update-publish-yaml
Update Github Action for Publishing to Comfy Registry
2025-03-27 14:34:17 +02:00
kijai 52c2e31a90 rename CFGZeroStar to avoid conflict 2025-03-26 12:29:56 +02:00
kijai f77c0dc5d8 Update model_optimization_nodes.py 2025-03-26 01:14:16 +02:00
kijai b1ec996ba3 Add CFGZeroStar
https://github.com/WeichenFan/CFG-Zero-star/
2025-03-26 01:05:20 +02:00
kijai a5bd3c86c8 Make TeaCache node error out instead of silently fail if ComfyUI isn't new enough to pass transformer_options 2025-03-19 10:13:08 +02:00
kijai 62ee13ef76 order the separated masks 2025-03-19 00:31:18 +02:00
kijai 89fb17ae84 Bump version 2025-03-18 21:36:28 +02:00
kijai 751b0bae62 Merge branch 'main' of https://github.com/kijai/ComfyUI-KJNodes 2025-03-18 20:28:49 +02:00
kijai e19285e5d1 step image/mask resize with 1 2025-03-18 20:28:47 +02:00
Jukka Seppänen e3b4bcf21e Merge pull request #235 from drozbay/main_pr_2
Fix for substep scheduling of patches with multiples conds
2025-03-18 08:36:09 +02:00
ozbayb 71351c0984 Fix for substep scheduling of patches with multiples conds 2025-03-17 13:51:06 -06:00
kijai 393ec896f7 Better compile compatibility with various patches
Shouldn't drop compile when changing slg or enhance-a-video settings anymore
2025-03-17 14:11:30 +02:00
kijai bb154eb71f Fix TeaCache start/end 2025-03-17 09:48:06 +02:00
kijai 7c488a16ef Add SkipLayerGuidanceWanVideo 2025-03-16 17:24:23 +02:00
kijai 6b7eeebe44 Update model_optimization_nodes.py 2025-03-16 13:15:47 +02:00
kijai c0f9894dc5 Update contextmenu.js 2025-03-15 11:33:56 +02:00
kijai 02c76c88b4 Update contextmenu.js 2025-03-15 11:33:08 +02:00
kijai 51e792ccdf Allow setting Set/Get node position in node suggestion menu 2025-03-15 11:27:21 +02:00
kijai c19ad34916 update to match comfyui latest update 2025-03-14 18:42:31 +02:00
kijai 1016861aec Merge branch 'main' of https://github.com/kijai/ComfyUI-KJNodes 2025-03-13 11:10:06 +02:00
kijai a5b39a8f1a Update model_optimization_nodes.py 2025-03-13 11:10:05 +02:00
Jukka Seppänen 46f9ec2c24 Merge pull request #222 from AustinMroz/main
Support comma separated types with get/set nodes
2025-03-12 22:04:34 +02:00
Austin Mroz 29a77e764a Support comma separated types with get/set nodes
Resolves #221
2025-03-12 13:16:52 -05:00
kijai 7ecb190ef9 Add StringToFloatList 2025-03-12 18:58:22 +02:00
kijai a4b9fd36da Possible workaround for model patch order patch memory issue 2025-03-12 00:05:42 +02:00
kijai 51b9efe0a1 Update model_optimization_nodes.py 2025-03-11 19:46:10 +02:00
kijai 63966e3483 Add HunyuanVideoEncodeKeyframesToCond
to use native hunyuan dashtoon keyframe LoRA:

https://huggingface.co/Kijai/HunyuanVideo_comfy/blob/main/HunyuanVideo_dashtoon_keyframe_lora_converted_comfy_bf16.safetensors
2025-03-11 08:48:58 +02:00
kijai 263961539e Add WanVideoEnhanceAVideoKJ 2025-03-09 16:53:51 +02:00
kijai 665f59fae3 Include WanVideoWrapper for help popups 2025-03-08 12:15:32 +02:00
kijai 28d1fbda34 update TeaCache docs 2025-03-08 12:09:02 +02:00
kijai 79d9aab5e7 Update model_optimization_nodes.py 2025-03-08 10:49:27 +02:00
kijai 773fab5a07 Update model_optimization_nodes.py 2025-03-08 10:49:17 +02:00
kijai 68db110554 Exclude TeaCache from compile to avoid possible compile errors, make compiling whole model default for WanVideo 2025-03-07 16:35:30 +02:00
kijai d126b62ceb Update pyproject.toml 2025-03-07 01:38:56 +02:00
kijai 8ba503c0a5 Update model_optimization_nodes.py 2025-03-07 01:38:40 +02:00
kijai d835ac9604 Fix GetImageSizeAndCount 2025-03-06 18:09:27 +02:00
kijai fa6d20eeb3 add WanVideo TeaCache coefficients 2025-03-05 14:03:05 +02:00
kijai 4d8cd3daa4 Update RifleX default freq 2025-03-04 11:27:35 +02:00
kijai 11a3744c3d Update model_optimization_nodes.py 2025-03-03 19:54:33 +02:00
kijai 0a99e6799a Update __init__.py 2025-03-03 17:09:05 +02:00
kijai 60abdef03e rename WanVideoTeaCache -> WanVideoTeaCacheKJ to avoid clash with wrapper 2025-03-03 16:55:56 +02:00
kijai 3b0d20532f Update model_optimization_nodes.py 2025-03-03 15:40:17 +02:00
kijai d00082f648 Add WanVideoTeaCache 2025-03-03 15:23:53 +02:00
kijai 9a15e22f5e Update model_optimization_nodes.py 2025-03-01 12:41:50 +02:00
Jukka Seppänen 6a16be7f21 Merge pull request #202 from DocShotgun/main
Fix nightly torch check for fp16 accumulation
2025-02-28 22:20:19 +02:00
kijai 46b76b8d7c Update model_optimization_nodes.py 2025-02-28 20:59:57 +02:00
DocShotgun 9dc737e560 Fix nightly torch check for fp16 accumulation 2025-02-28 10:59:29 -08:00
kijai ced72635c7 Add new options to the model loader 2025-02-28 19:27:35 +02:00
Jukka Seppänen 82272ef448 Merge pull request #201 from blepping/fix_riflex
Only apply RifleX to the temporal dimension
2025-02-28 12:58:25 +02:00
blepping dc482957d8 Only apply RifleX to the temporal dimension 2025-02-28 03:44:03 -07:00
kijai 97d20e27e5 ApplyRifleXRoPE_WanVideo
experimental
2025-02-27 12:55:53 +02:00
kijai 1a4259f052 TorchCompileModelWanVideo 2025-02-26 11:10:17 +02:00
kijai 24bb774432 Update model_optimization_nodes.py 2025-02-26 09:37:50 +02:00
kijai 4b3009e4bf Update nodes.py 2025-02-24 19:26:34 +02:00
kijai 69ec71d2bd Update nodes.py 2025-02-24 18:58:04 +02:00
kijai 9273d23916 Experimental ApplyRifleXRoPE_HunuyanVideo 2025-02-24 18:46:20 +02:00
kijai 8f3cc622a8 typo 2025-02-23 20:35:34 +02:00
kijai 302fd1bbe6 Update some descriptions 2025-02-23 19:55:13 +02:00
kijai 14430c118d bump version 2025-02-23 17:51:09 +02:00
kijai f3d931a630 Add ScheduledCFGGuidance 2025-02-22 15:43:58 +02:00
kijai 8950c5fe67 Add ImagePadKJ, VAELoaderKJ
simple pad node and VAE loader that let's you choose device and dtype
2025-02-21 20:35:23 +02:00
kijai f653a8e45e typo 2025-02-20 12:34:07 +02:00
kijai 095c8d4b52 Update image_nodes.py 2025-02-16 19:12:42 +02:00
kijai 56979210c7 Add ImagePrepForICLora, ImageCropByMask 2025-02-16 18:23:30 +02:00
Jukka Seppänen 2129789f93 Merge pull request #186 from Sarania/main
Add TorchCompileModelHyVideo node
2025-02-15 11:54:59 +02:00
kijai fb03b43414 Update image_nodes.py 2025-02-13 23:55:42 +02:00
kijai e98b612b2b Update image_nodes.py 2025-02-13 23:55:29 +02:00
Blyss Sarania cb0f055a12 Add TorchCompileModelHyVideo node based on HunyuanVideoWrapper torch.compile settings node and TorchCompileModelFluxAdvanced 2025-02-12 15:30:02 -05:00
kijai 80977db1ea Merge branch 'main' of https://github.com/kijai/ComfyUI-KJNodes 2025-02-11 21:15:22 +02:00
kijai a0eebe8c9d Add InsertLatentToIndexed, SaveStringKJ 2025-02-11 21:15:20 +02:00
Jukka Seppänen 86b5453a5c Merge pull request #182 from drozbay/dev
Enable handling of sub-steps in LeapfusionHunyuanI2V patch method
2025-02-08 21:03:09 +02:00
ozbayb c5d23a7c38 Enable handling of sub-steps in LeapfusionHunyuanI2V patch method 2025-02-08 11:48:10 -07:00
kijai bfe72cc964 Add ImageCropByMaskBatch, SeparateMasks 2025-02-08 16:37:08 +02:00
kijai a22b269242 Fix CustomControlNetWeightsFluxFromList 2025-02-07 16:46:01 +02:00
kijai 9a3434ffa3 Update leapfusion_hunyuuanvideo_i2v_native_testing.json 2025-02-04 01:12:06 +02:00
kijai 854da433f4 Create leapfusion_hunyuuanvideo_i2v_native_testing.json 2025-02-04 01:07:54 +02:00
kijai a8aeef670b Update nodes.py 2025-02-02 00:32:18 +02:00
kijai e9a3170c72 Update nodes.py 2025-02-02 00:16:57 +02:00
kijai e11695a305 Update nodes.py 2025-02-01 23:58:46 +02:00
kijai 23103dffde Add ImageNoiseAugmentation 2025-02-01 23:43:22 +02:00
kijai 81b7d71454 Update nodes.py 2025-02-01 22:15:13 +02:00
kijai 6c4aa273c8 Add LeapfusionHunyuanI2VPatcher 2025-02-01 21:38:51 +02:00
kijai 188793f2ba Merge branch 'main' of https://github.com/kijai/ComfyUI-KJNodes 2025-01-31 12:59:01 +02:00
kijai 3a62577717 LoadImagesFromFolderKJ: support subfolders, change resize logic and add aspcet ratio preserving options 2025-01-31 12:58:55 +02:00
Jukka Seppänen fa6964b96c Merge pull request #132 from eltociear/patch-1
docs: update README.md
2025-01-31 12:25:23 +02:00
Jukka Seppänen df7edf7893 Merge pull request #155 from yondonfu/taesd-compile
Support taesd in TorchCompileVAE
2025-01-31 12:25:09 +02:00
kijai 75a990b40d LoadImagesFromFolderKJ: resize batch to largest image in the folder 2025-01-31 12:24:40 +02:00
kijai 2abf557e3d Update curve_nodes.py 2025-01-27 12:14:45 +02:00
snomiao bd2546b137 chore(publish): update GitHub Actions workflow for node publishing
- Add permissions for issue writing
- Update action version to v1 for publish-node-action
- Add condition to run job only for 'kijai' repository owner
2025-01-21 08:44:30 +00:00
kijai 3f141b8f1c Update nodes.py 2025-01-19 00:52:28 +02:00
kijai 7b36368a74 Update pyproject.toml 2025-01-18 19:41:14 +02:00
kijai ae5a6a92d6 Allow limiting decimals with WidgetToString 2025-01-18 19:38:30 +02:00
kijai 690268ee27 Add CutAndDragOnPath 2025-01-18 18:25:38 +02:00
kijai c9c8dcd5e7 Update model_optimization_nodes.py 2025-01-16 21:13:37 +02:00
kijai 5b8d419c6f Update image_nodes.py 2025-01-14 21:21:35 +02:00
kijai 3d6b72f28d Update pyproject.toml 2025-01-14 15:17:07 +02:00
kijai 28f0470a9a Repeat last frame when concatenating different length videos 2025-01-14 15:08:34 +02:00
kijai 3adcc529f2 Update pyproject.toml 2025-01-12 19:42:37 +02:00
kijai 7364ce7d2d remove prints 2025-01-12 19:42:19 +02:00
kijai 5a15eea036 js menu fixes 2025-01-12 19:26:14 +02:00
kijai 4d0e5cf240 torch.compile for cosmos 2025-01-12 18:46:35 +02:00
kijai 833cda9fa2 Support sageattn for Cosmos 2025-01-12 17:49:55 +02:00
Jukka Seppänen 31cb7c1d14 Update pyproject.toml 2025-01-08 22:48:57 +02:00
kijai 4c5c26a2c9 Fix hangup with Show connections on Set/Get nodes 2024-12-25 13:17:03 +02:00
Jukka Seppänen 7a190ce8c1 Merge pull request #160 from melMass/fix/set-alerts
fix: 🐛 simplify set/get alerts
2024-12-23 19:37:30 +02:00
Mel Massadian f38b89ba7f fix: 🐛 message 2024-12-23 18:29:20 +01:00
Mel Massadian 3c6bb87083 fix: 🐛 simplify set/get alerts
Now use the builtin toast api
2024-12-23 18:24:54 +01:00
kijai cdbd38213f Add PathchSageAttentionKJ 2024-12-22 22:31:20 +02:00
kijai f5ce7d017b Fix up sageattention loader for hunyuan etc. 2024-12-22 14:59:38 +02:00
Jukka Seppänen 973ceb6ca8 Add AudioConcat 2024-12-15 21:41:54 +02:00
Yondon Fu d089395bb5 Support taesd in TorchCompileVAE 2024-12-11 17:46:13 -05:00
kijai 8c590fd5a0 maybe fix lora compile 2024-12-10 17:30:47 +02:00
kijai 6eb9211091 Make SaveImageKJ work with prefix replacement 2024-12-10 12:52:12 +02:00
kijai 1a39ccde72 restructure 2024-12-10 12:07:24 +02:00
kijai 28381262d1 Increate fade mask max frames 2024-11-26 14:49:38 +02:00
kijai f4a0f7fc6e Allow setting -1 as height for AddLabel for automatically setting the height to fit all of the text 2024-11-26 14:40:18 +02:00
kijai 22231d401a Update nodes.py 2024-11-26 14:23:46 +02:00
Jukka Seppänen ebd5915d8e Rename custom_dimensions.json to custom_dimensions_example.json 2024-11-26 14:19:00 +02:00
Jukka Seppänen 7341ae0096 Merge pull request #127 from tonyatpeking/main
fix LoadImagesFromFolderKJ returns null image_path
2024-11-26 14:17:23 +02:00
kijai 1bc5c99f5a Add DiffusionModelLoaderKJ 2024-11-25 16:47:21 +02:00
kijai 5920419f44 Fix FluxBlockLoraLoader 2024-11-25 16:28:29 +02:00
kijai 8f057eb563 Update nodes.py 2024-11-22 17:15:50 +02:00
kijai 44620cb566 Update nodes.py 2024-11-22 17:12:06 +02:00
kijai fb5aa296ae Add TorchCompileLTXModel 2024-11-22 17:00:49 +02:00
kijai e2e8e7ebe5 Update nodes.py 2024-11-22 14:00:43 +02:00
kijai f4f43d12e7 Add StyleModelApplyAdvanced 2024-11-22 10:23:50 +02:00
kijai 5d7aaf93ce Update nodes.py 2024-11-19 11:56:06 +02:00
kijai 67c54b31e8 revert this 2024-11-15 17:24:27 +02:00
kijai 31639b4c79 Update image_nodes.py 2024-11-14 10:34:41 +02:00
Jukka Seppänen 1dbb38d63d Merge pull request #140 from bananasss00/main
Fix suggestion removal for each slot type in LiteGraph
2024-11-12 16:39:22 +02:00
rdp 2cabc3eaa3 Fix suggestion removal for each slot type in LiteGraph 2024-11-12 17:19:19 +03:00
kijai 5f032e3b8f Update nodes.py 2024-11-07 10:58:38 +02:00
kijai 75e647560f small fixes 2024-11-06 17:25:39 +02:00
kijai a982a31956 fixup ImageCropByMaskAndResize for batches more 2024-11-06 14:21:57 +02:00
kijai 3f903091b3 Update image_nodes.py 2024-11-06 13:01:27 +02:00
kijai 3b065864fa Update image_nodes.py 2024-11-06 12:59:11 +02:00
kijai ba33d02198 Update nodes.py 2024-11-06 01:52:06 +02:00
kijai cdf8ca8298 save and retrieve compile settings when re-compiling 2024-11-05 16:10:27 +02:00
Ikko Eltociear Ashimine 1614e1d932 docs: update README.md
lenghth -> length
2024-11-05 16:48:16 +09:00
kijai 365e0699b1 Update nodes.py 2024-11-04 19:09:26 +02:00
kijai 8cfbbaf29e torch compile with LoRAs 2024-11-04 18:40:51 +02:00
kijai 3e11fff5f5 Add SigmasToFloat 2024-11-03 20:00:49 +02:00
Tony Yu 1eaa2a1928 fix LoadImagesFromFolderKJ returns null image_path 2024-10-29 13:30:40 -05:00
kijai d9191b4c1d fix sticky context menu when multiple spline editors 2024-10-29 10:30:27 +02:00
kijai f6ba944afc Update nodes.py 2024-10-28 04:03:31 +02:00
kijai fe5fbb03ff TorchCompileVAE and controlnet 2024-10-28 03:57:58 +02:00
kijai 530c5d7eaf fix up TorchCompileModelFluxAdvanced 2024-10-28 02:32:56 +02:00
kijai f59e410568 Update image_nodes.py 2024-10-26 22:31:28 +03:00
kijai 19ec49ae2b TorchCompileModelFluxAdvanced 2024-10-25 20:52:16 +03:00
kijai 1ec5810868 Update spline_editor.js 2024-10-22 19:22:33 +03:00
kijai 0227f7b77f fixes 2024-10-22 18:15:42 +03:00
kijai b3e5108ad4 Add AppendStringsToList 2024-10-22 16:33:22 +03:00
kijai eb8ce5a574 Update spline_editor.js 2024-10-22 01:58:43 +03:00
kijai 971bf375e5 SplineEditor rework 2024-10-22 00:33:55 +03:00
kijai bbb28a0fc9 Update curve_nodes.py 2024-10-20 14:27:32 +03:00
kijai 6842699f9e bugfixes 2024-10-20 14:09:17 +03:00
kijai 7c1228a5a3 Swap out PatchCublasLinear in favor of checkpoint loader that patches it
Because the patch needs to happen before model loading
2024-10-18 14:29:43 +03:00
kijai 2263b8cb41 add ShuffleImageBatch 2024-10-18 02:13:31 +03:00
kijai 14f6d5fb51 add TransitionImagesInBatch -node 2024-10-17 22:10:03 +03:00
kijai 3df9f978f7 new nodes 2024-10-15 22:57:52 +03:00
kijai 74c335b6ef Update image_nodes.py 2024-10-15 13:44:19 +03:00
kijai f123b36d28 Update image_nodes.py 2024-10-15 12:26:01 +03:00
kijai 579f0b4050 Add TransitionImagesMulti 2024-10-15 11:58:45 +03:00
kijai d1848eb402 Add CrossFadeImagesMulti 2024-10-14 18:16:32 +03:00
kijai fb2f09651b conditioning combine options 2024-10-14 11:26:00 +03:00
kijai 2fbed0575d add FastPreview 2024-10-12 21:03:33 +03:00
kijai b5419c853c Allow images with alpha to be concatenated, improve ImageGridtoBatch 2024-09-29 17:17:18 +03:00
kijai c31fa9f438 Add ModelSaveKJ
node to save a model with the prefix you want
2024-09-24 23:27:54 +03:00
kijai 6d119fda33 Update image_nodes.py 2024-09-24 10:41:25 +03:00
kijai 71904a4eca Add node to save image along with .txt file for captions (SaveImageKJ) 2024-09-24 10:25:58 +03:00
kijai 13242fc3bf Add SetShakkerLabsUnionControlNetType -node 2024-09-22 00:35:51 +03:00
kijai 912cbd2ab6 Add CustomControlNetWeightsFluxFromList 2024-09-19 14:24:41 +03:00
Jukka Seppänen ca0e1ad28d Merge pull request #108 from tombearx/main
Fix ImagePadForOutpaintTargetSize
2024-09-17 17:58:02 +03:00
kijai 999be76780 update custom presets 2024-09-17 16:57:53 +03:00
kijai 3dacf166df Add EmptyLatentImageCustomPresets 2024-09-17 16:08:59 +03:00
kijai 7cb775ce40 Merge branch 'main' of https://github.com/kijai/ComfyUI-KJNodes 2024-09-17 15:25:36 +03:00
kijai 8c456d5307 update latent image presets and other small stuff 2024-09-17 15:25:34 +03:00
Jukka Seppänen 2230682bfb Create FUNDING.yml 2024-09-17 15:18:11 +03:00
kijai 7aa591b3a6 Update nodes.py 2024-09-01 20:40:00 +03:00
kijai 181f153120 Update nodes.py 2024-09-01 20:37:18 +03:00
kijai a219601b2a allow loading the lora from absolute path 2024-09-01 20:06:19 +03:00
kijai ab526783d2 Update jsnodes.js 2024-08-31 21:20:39 +03:00
kijai d85a3c92a8 FluxBlockLoraSelect update 2024-08-31 16:01:44 +03:00
kijai 0defb731ac improve Flux lora block select 2024-08-31 15:10:18 +03:00
kijai bdb65e5635 Update nodes.py 2024-08-31 12:12:24 +03:00
kijai e362c926d3 Update nodes.py 2024-08-31 12:06:39 +03:00
kijai 8fc4794f88 Update nodes.py 2024-08-31 03:19:33 +03:00
kijai a175adfe73 Add FluxBlockLoraLoader experimental node 2024-08-31 03:19:09 +03:00
kijai 326d5945b7 Update nodes.py 2024-08-29 19:15:38 +03:00
kijai d02a3bf46f Add DifferentialDiffusionAdvanced
Seems useful for Flux inpainting to be able to adjust the effect
2024-08-29 19:07:16 +03:00
tombearx 8a2ea3fa79 Fix ImagePadForOutpaintTargetSize
There is an error in the code: 
If scale factor is equal 1, then padding should calculated as difference between target and current sizes. Now padding is just target size.
2024-08-29 07:44:11 +03:00
kijai 47a6da5f62 Add ImageGridtoBatch 2024-08-24 01:49:06 +03:00
kijai bbf3db8760 invert mask 2024-08-22 17:13:14 +03:00
kijai 1bfdb8a1a2 Don't fill the alpha by default 2024-08-22 00:47:31 +03:00
Kijai d7f91f2c65 improve LoadAndResize image 2024-08-21 15:20:53 +03:00
kijai 87084633be Add ImageConcatFromBatch 2024-08-21 01:58:31 +03:00
kijai 11c2155138 fix help popup offset with the new interface 2024-08-18 15:11:26 +03:00
kijai 7e7a73191c LoadImagesFromFolderKJ 2024-08-16 01:39:16 +03:00
kijai a23d734fa0 Update image_nodes.py 2024-08-16 00:54:07 +03:00
kijai bff39a4e9c Update image_nodes.py 2024-08-14 02:48:48 +03:00
kijai 7e989daae3 add crop to ResizeImage 2024-08-14 01:48:19 +03:00
Jukka Seppänen eaed0d3593 Merge pull request #103 from webfiltered/add-litegraph-es6-support
Adds support for LiteGraph LiteGraph ES6 classes
2024-08-13 01:49:44 +03:00
filtered 7235a3dccf Adds support for LiteGraph LiteGraph ES6 classes
This should continue to work unchanged on old versions of ComfyUI.

The changes just allow the set/get nodes to also work with the latest version of litegraph - conversion to proper classes is slowly going on.
2024-08-13 01:16:21 +10:00
kijai b751e34383 bye hippo 2024-08-11 00:10:31 +03:00
kijai 48cf28c7d0 fix JoinStringMulti 2024-08-10 17:06:18 +03:00
kijai 58f234d544 Update point_editor.js 2024-08-10 14:21:21 +03:00
kijai a6f358ac29 Merge branch 'main' of https://github.com/kijai/ComfyUI-KJNodes 2024-08-10 12:57:00 +03:00
kijai e549bd91bc Update point_editor.js 2024-08-10 12:56:58 +03:00
Kijai 2c3d2b9832 WidgetToString bugfix 2024-08-09 16:14:29 +03:00
kijai 855aef479f passthrough fix 2024-08-08 21:04:00 +03:00
kijai 0bc25cfe55 PointEditor fixes 2024-08-05 21:50:57 +03:00
kijai 20aab31d83 category fix 2024-08-05 20:05:24 +03:00
Jukka Seppänen 9bb1e47ba7 Merge pull request #99 from sn0w12/main
Update WidgetToString to use 'any_input' link if no id or title provided.
2024-08-04 19:15:05 +02:00
kijai 98c1fb17c5 support tensors 2024-08-04 20:13:56 +03:00
kijai c3f6dcd850 PointsEditor updates 2024-08-04 20:08:13 +03:00
Lucas Sköld 9349d318ba Add comment 2024-08-04 17:32:08 +02:00
kijai 2e7129fdb9 matplotlib optional as well 2024-08-04 16:43:31 +03:00
kijai 1afb92dee2 Update pyproject.toml 2024-08-04 16:39:02 +03:00
Lucas Sköld 70301c5e54 Fix link checking. 2024-08-04 15:36:52 +02:00
kijai c08c858374 don't require cv2 for everything 2024-08-04 16:35:30 +03:00
kijai dc479dc2ef Update curve_nodes.py 2024-08-04 16:18:41 +03:00
kijai df5e0d49e4 PointsEditor: add negative points 2024-08-04 16:16:56 +03:00
Lucas Sköld d9bef9692a Move WidgetToString inputs back. 2024-08-04 14:48:39 +02:00
Lucas Sköld cc6196c25f Change WidgetToString to use the any_input link if no id or title.
If no node if or title is provided use the link to get the node id.
2024-08-04 14:44:20 +02:00
kijai 3c0bc27343 Update curve_nodes.py 2024-08-03 22:57:34 +03:00
kijai 6f82bca98d Update nodes.py 2024-08-03 22:36:22 +03:00
kijai 42741f578c Update nodes.py 2024-08-03 22:26:54 +03:00
kijai 286f8ea4a3 PointsEditor refactor, support image input, copypaste etc. 2024-08-03 16:07:31 +03:00
kijai 5229aaa2c0 Update point_editor.js 2024-08-02 21:50:28 +03:00
kijai 4a2d499a5b Update curve_nodes.py 2024-08-02 21:20:55 +03:00
kijai 17f8d8db60 Update curve_nodes.py 2024-08-02 20:43:00 +03:00
kijai dee4e8f1eb bbox to point editor 2024-08-02 18:52:05 +03:00
kijai 5ccd427bd8 Update point_editor.js 2024-08-02 17:07:56 +03:00
kijai 27cfddc379 fix category 2024-08-01 23:01:57 +03:00
kijai 0888ece9b0 prototype points editor 2024-07-31 18:43:25 +03:00
kijai ad60519856 Direct controlpoint sample method to spline editor 2024-07-31 12:18:18 +03:00
kijai 7191a1fbbb add color match str parameter 2024-07-30 16:08:59 +03:00
kijai 7ea33f68fc js fixes 2024-07-28 21:05:45 +03:00
Jukka Seppänen adb35ee233 Merge pull request #93 from Quasimondo/patch-1
Update image_nodes.py
2024-07-26 22:17:06 +02:00
Mario Klingemann 5568cff68a Update image_nodes.py
Trying to set capture size for cameras that support multiple resolutions. Might be better to use two separate values for that or query the camera's native resolutions, but for now that's what I needed.
2024-07-26 22:15:53 +02:00
kijai 6e3af6e7b9 fix webcam index selection 2024-07-24 21:59:02 +03:00
kijai bc4890f798 simplify webcam node 2024-07-24 16:58:36 +03:00
kijai 8df4ea78cb webcam node 2024-07-24 16:36:42 +03:00
kijai 44f7e559b9 Update image_nodes.py 2024-07-24 13:17:47 +03:00
kijai 86dc22a028 Add faster screencap node 2024-07-23 19:00:04 +03:00
kijai 0905db846d Update mask_nodes.py 2024-07-20 12:02:59 +03:00
kijai 8560348572 Update requirements.txt 2024-07-20 02:13:29 +03:00
kijai 6e084c0f9b fix clipseg default model choice 2024-07-20 02:12:28 +03:00
kijai ffafc9c2c6 Update curve_nodes.py 2024-07-07 19:28:16 +03:00
kijai 296a1beb66 Better image concat size matching 2024-07-07 15:10:02 +03:00
kijai 3d7577f316 Update GetImageRangeFromBatch to work with only masks too 2024-07-06 14:35:17 +03:00
kijai 2ead4fae1d Merge branch 'main' of https://github.com/kijai/ComfyUI-KJNodes 2024-06-26 00:16:39 +03:00
kijai 83095a51ba fix 2024-06-26 00:16:36 +03:00
Jukka Seppänen 2c9acabaa5 Merge pull request #87 from kevinramirez723/ImageAndMaskPreviewColorPatch
ImageAndMaskPreview node mask_color widget fix and hexadecimal color support
2024-06-23 13:54:07 +03:00
Jukka Seppänen 86a1125ae1 Merge pull request #88 from telamonian/fix-instristic-spelling
Fix spelling of `intristic_loras` directory
2024-06-23 13:53:03 +03:00
telamonian fa10533f8c fix all mentions of intristic_loras -> intrinsic_loras 2024-06-21 04:21:54 -04:00
telamonian d6b69f56bb fix spelling intristic_loras -> intrinsic_loras 2024-06-21 04:21:29 -04:00
Kevin Ramirez 05b4c3c4b7 Add support for hex format and clip rgb format 2024-06-21 06:10:54 +00:00
Kevin Ramirez 1f03079338 Fix mask_color widget in ImageAndMaskPreview node 2024-06-21 06:10:34 +00:00
kijai 2fb0ee4934 registry 2024-06-19 12:05:15 +03:00
Jukka Seppänen 97cefb427a Merge pull request #77 from haohaocreates/publish
Add Github Action for Publishing to Comfy Registry
2024-06-19 12:00:55 +03:00
Jukka Seppänen 6f37e62dd6 Merge pull request #76 from haohaocreates/pyproject
Add pyproject.toml for Custom Node Registry
2024-06-19 12:00:43 +03:00
kijai c98c94125e Update nodes.py 2024-06-15 01:29:41 +03:00
kijai 36a8633aff Update nodes.py 2024-06-15 01:19:00 +03:00
kijai 3ba3ddf0b1 experimental 2024-06-15 01:08:26 +03:00
kijai 07288d1b6f Add ImageConcatMulti 2024-06-13 16:51:58 +03:00
kijai 36e3b6f66a autodownload superprompt model 2024-06-10 17:46:25 +03:00
kijai cdd507de43 Update nodes.py 2024-06-08 12:49:16 +03:00
kijai 5430564e7f Update image_nodes.py 2024-06-01 16:07:44 +03:00
Kijai b6193451b3 Update image_nodes.py 2024-05-31 14:09:36 +03:00
kijai 2c7e8613e0 Update image_nodes.py 2024-05-29 22:37:34 +03:00
Kijai a15d79d5dc Merge branch 'main' of https://github.com/kijai/ComfyUI-KJNodes 2024-05-29 15:04:10 +03:00
Kijai 0690bff42c Fix outdated scipy erf import 2024-05-29 15:04:08 +03:00
haohaocreates 63e6b81aee Update pyproject.toml description 2024-05-21 11:13:03 -04:00
haohaocreates bba419b07b chore(pyproject): Add pyproject.toml for Custom Node Registry 2024-05-21 15:01:47 +00:00
haohaocreates a8f6110828 chore(publish): Add Github Action for Publishing to Comfy Registry 2024-05-21 15:01:46 +00:00
kijai a7141dc5ca Add optional mask batch slicing to GetImageRangeFromBatch 2024-05-17 12:21:59 +03:00
Kijai d8cf496704 Update image_nodes.py 2024-05-17 12:09:17 +03:00
Kijai 2f843db54e More Set/Get autocolors 2024-05-16 15:04:33 +03:00
Kijai fca2e82853 Update __init__.py 2024-05-16 14:23:32 +03:00
Kijai cb6864c7c8 Update nodes.py 2024-05-16 12:38:48 +03:00
Jukka Seppänen 2f6e38220c Merge pull request #72 from coolzilj/patch-1
Fix "not enough values to unpack" error for ResizeMask
2024-05-16 10:59:34 +03:00
Jin Liu c4c63fce73 Fix "not enough values to unpack" error for ResizeMask 2024-05-16 15:06:02 +08:00
Kijai 7e6bd8d14a Add LoadAndResizeImage 2024-05-14 15:20:03 +03:00
Kijai f741ef0252 Add ImageResizeKJ 2024-05-14 13:07:42 +03:00
Kijai 33ef974370 Update mask_nodes.py 2024-05-14 12:12:09 +03:00
kijai 6ca2bb2708 Update mask_nodes.py 2024-05-13 22:58:17 +03:00
kijai 68471f65b3 Merge branch 'main' of https://github.com/kijai/ComfyUI-KJNodes 2024-05-13 22:46:49 +03:00
kijai 17a6b358af clipseg improvements 2024-05-13 22:46:47 +03:00
Kijai b2d5ab3fcd Update mask_nodes.py 2024-05-13 15:06:38 +03:00
kijai 4812eff6e5 Batch clip seg improvements 2024-05-12 21:52:56 +03:00
kijai 0ef2b86b28 Merge branch 'pr/67' 2024-05-12 21:21:04 +03:00
kijai 22f166008f Update spline_editor.js 2024-05-12 02:55:36 +03:00
kijai d24d1c64d2 ImageAddMulti 2024-05-12 02:30:08 +03:00
kijai 5631cd0146 Update curve_nodes.py 2024-05-12 02:08:53 +03:00
kijai 9991155130 Add CreateShapeImageOnPath -node 2024-05-12 01:52:29 +03:00
kijai 668496d85f Add normalized_str output to spline editor
thanks @joviex !
2024-05-12 01:34:15 +03:00
kijai 218e95c8c9 Add ImagePadForOutpaintTargetSize -node 2024-05-11 18:41:27 +03:00
kijai d2a44569b2 Don't allow SplineEditor width to be too small 2024-05-11 13:15:54 +03:00
kijai bee351960d Update curve_nodes.py 2024-05-11 02:46:45 +03:00
kijai d165ae2542 New nodes 2024-05-11 02:44:57 +03:00
kijai 26ca2925c6 Fixes 2024-05-10 11:46:46 +03:00
Mokan Alexander 030b6a20b5 Update mask_nodes.py 2024-05-10 03:39:06 +03:00
Mokan Alexander 3260038935 Merge branch 'kijai:main' into main 2024-05-10 03:38:45 +03:00
kijai cba1cb92c1 Update setgetnodes.js 2024-05-09 21:25:15 +03:00
Mokan Alexander 3652e8eee2 Update mask_nodes.py
Reduced the total time for creating masks in a batch by using a handler built into the processor:
processor(images=images)
2024-05-09 19:45:13 +03:00
kijai 148c805a15 Add ModelPassThrough -node 2024-05-09 19:14:05 +03:00
kijai 916241840a Fix add overlay label 2024-05-09 15:41:22 +03:00
kijai 26fbf1f03d Update nodes.py 2024-05-09 03:02:45 +03:00
kijai 46e91987be Update nodes.py 2024-05-09 02:56:55 +03:00
kijai 928e5cc778 Update nodes.py 2024-05-09 01:48:53 +03:00
Kijai 4f9d3f4142 experimental 2024-05-08 18:44:32 +03:00
Kijai 147c4505d1 Add PreviewAnimation -node 2024-05-07 14:09:19 +03:00
Kijai 855f9993b9 Add CreateTextOnPath -node 2024-05-07 11:56:29 +03:00
Kijai d12a011943 Fix the coordinate interpolation to properly work in 0.0-1.0 range 2024-05-07 10:11:18 +03:00
kijai 24f27c5b82 clamp colormatch output 2024-05-06 22:08:56 +03:00
31 changed files with 11254 additions and 1123 deletions
+2
View File
@@ -0,0 +1,2 @@
github: [kijai]
custom: ["https://www.paypal.me/kijaidesign"]
+25
View File
@@ -0,0 +1,25 @@
name: Publish to Comfy registry
on:
workflow_dispatch:
push:
branches:
- main
paths:
- "pyproject.toml"
permissions:
issues: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'kijai' }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@v1
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+4 -1
View File
@@ -1,8 +1,11 @@
__pycache__
/venv
*.code-workspace
.history
.vscode
*.ckpt
*.pth
types
models
jsconfig.json
jsconfig.json
custom_dimensions.json
+1 -1
View File
@@ -17,7 +17,7 @@ This is still work in progress, like everything else.
## Javascript
### browserstatus.js
Sets the favicon to green circle when not processing anything, sets it to red when processing and shows progress percentage and the lenghth of your queue.
Sets the favicon to green circle when not processing anything, sets it to red when processing and shows progress percentage and the length of your queue.
Default off, needs to be enabled from options, overrides Custom-Scripts favicon when enabled.
## Nodes:
+108 -10
View File
@@ -5,8 +5,11 @@ from .nodes.audioscheduler_nodes import *
from .nodes.image_nodes import *
from .nodes.intrinsic_lora_nodes import *
from .nodes.mask_nodes import *
from .nodes.model_optimization_nodes import *
from .nodes.lora_nodes import *
NODE_CONFIG = {
#constants
"BOOLConstant": {"class": BOOLConstant, "name": "BOOL Constant"},
"INTConstant": {"class": INTConstant, "name": "INT Constant"},
"FloatConstant": {"class": FloatConstant, "name": "Float Constant"},
"StringConstant": {"class": StringConstant, "name": "String Constant"},
@@ -19,6 +22,7 @@ NODE_CONFIG = {
"ConditioningSetMaskAndCombine5": {"class": ConditioningSetMaskAndCombine5, "name": "ConditioningSetMaskAndCombine5"},
"CondPassThrough": {"class": CondPassThrough},
#masking
"DownloadAndLoadCLIPSeg": {"class": DownloadAndLoadCLIPSeg, "name": "(Down)load CLIPSeg"},
"BatchCLIPSeg": {"class": BatchCLIPSeg, "name": "Batch CLIPSeg"},
"ColorToMask": {"class": ColorToMask, "name": "Color To Mask"},
"CreateGradientMask": {"class": CreateGradientMask, "name": "Create Gradient Mask"},
@@ -37,31 +41,64 @@ NODE_CONFIG = {
"RemapMaskRange": {"class": RemapMaskRange, "name": "Remap Mask Range"},
"ResizeMask": {"class": ResizeMask, "name": "Resize Mask"},
"RoundMask": {"class": RoundMask, "name": "Round Mask"},
"SeparateMasks": {"class": SeparateMasks, "name": "Separate Masks"},
#images
"AddLabel": {"class": AddLabel, "name": "Add Label"},
"ColorMatch": {"class": ColorMatch, "name": "Color Match"},
"ImageTensorList": {"class": ImageTensorList, "name": "Image Tensor List"},
"CrossFadeImages": {"class": CrossFadeImages, "name": "Cross Fade Images"},
"GetImageRangeFromBatch": {"class": GetImageRangeFromBatch, "name": "Get Image Range From Batch"},
"CrossFadeImagesMulti": {"class": CrossFadeImagesMulti, "name": "Cross Fade Images Multi"},
"GetImagesFromBatchIndexed": {"class": GetImagesFromBatchIndexed, "name": "Get Images From Batch Indexed"},
"GetImageRangeFromBatch": {"class": GetImageRangeFromBatch, "name": "Get Image or Mask Range From Batch"},
"GetLatentRangeFromBatch": {"class": GetLatentRangeFromBatch, "name": "Get Latent Range From Batch"},
"GetLatentSizeAndCount": {"class": GetLatentSizeAndCount, "name": "Get Latent Size & Count"},
"GetImageSizeAndCount": {"class": GetImageSizeAndCount, "name": "Get Image Size & Count"},
"FastPreview": {"class": FastPreview, "name": "Fast Preview"},
"ImageBatchFilter": {"class": ImageBatchFilter, "name": "Image Batch Filter"},
"ImageAndMaskPreview": {"class": ImageAndMaskPreview},
"ImageAddMulti": {"class": ImageAddMulti, "name": "Image Add Multi"},
"ImageBatchJoinWithTransition": {"class": ImageBatchJoinWithTransition, "name": "Image Batch Join With Transition"},
"ImageBatchMulti": {"class": ImageBatchMulti, "name": "Image Batch Multi"},
"ImageBatchRepeatInterleaving": {"class": ImageBatchRepeatInterleaving},
"ImageBatchTestPattern": {"class": ImageBatchTestPattern, "name": "Image Batch Test Pattern"},
"ImageConcanate": {"class": ImageConcanate, "name": "Image Concatenate"},
"ImageConcatFromBatch": {"class": ImageConcatFromBatch, "name": "Image Concatenate From Batch"},
"ImageConcatMulti": {"class": ImageConcatMulti, "name": "Image Concatenate Multi"},
"ImageCropByMask": {"class": ImageCropByMask, "name": "Image Crop By Mask"},
"ImageCropByMaskAndResize": {"class": ImageCropByMaskAndResize, "name": "Image Crop By Mask And Resize"},
"ImageCropByMaskBatch": {"class": ImageCropByMaskBatch, "name": "Image Crop By Mask Batch"},
"ImageUncropByMask": {"class": ImageUncropByMask, "name": "Image Uncrop By Mask"},
"ImageGrabPIL": {"class": ImageGrabPIL, "name": "Image Grab PIL"},
"ImageGridComposite2x2": {"class": ImageGridComposite2x2, "name": "Image Grid Composite 2x2"},
"ImageGridComposite3x3": {"class": ImageGridComposite3x3, "name": "Image Grid Composite 3x3"},
"ImageGridtoBatch": {"class": ImageGridtoBatch, "name": "Image Grid To Batch"},
"ImageNoiseAugmentation": {"class": ImageNoiseAugmentation, "name": "Image Noise Augmentation"},
"ImageNormalize_Neg1_To_1": {"class": ImageNormalize_Neg1_To_1, "name": "Image Normalize -1 to 1"},
"ImagePass": {"class": ImagePass},
"ImagePadKJ": {"class": ImagePadKJ, "name": "ImagePad KJ"},
"ImagePadForOutpaintMasked": {"class": ImagePadForOutpaintMasked, "name": "Image Pad For Outpaint Masked"},
"ImagePadForOutpaintTargetSize": {"class": ImagePadForOutpaintTargetSize, "name": "Image Pad For Outpaint Target Size"},
"ImagePrepForICLora": {"class": ImagePrepForICLora, "name": "Image Prep For ICLora"},
"ImageResizeKJ": {"class": ImageResizeKJ, "name": "Resize Image (deprecated)"},
"ImageResizeKJv2": {"class": ImageResizeKJv2, "name": "Resize Image v2"},
"ImageUpscaleWithModelBatched": {"class": ImageUpscaleWithModelBatched, "name": "Image Upscale With Model Batched"},
"InsertImagesToBatchIndexed": {"class": InsertImagesToBatchIndexed, "name": "Insert Images To Batch Indexed"},
"InsertLatentToIndexed": {"class": InsertLatentToIndex, "name": "Insert Latent To Index"},
"LoadAndResizeImage": {"class": LoadAndResizeImage, "name": "Load & Resize Image"},
"LoadImagesFromFolderKJ": {"class": LoadImagesFromFolderKJ, "name": "Load Images From Folder (KJ)"},
"LoadVideosFromFolder": {"class": LoadVideosFromFolder, "name": "Load Videos From Folder"},
"MergeImageChannels": {"class": MergeImageChannels, "name": "Merge Image Channels"},
"PadImageBatchInterleaved": {"class": PadImageBatchInterleaved, "name": "Pad Image Batch Interleaved"},
"PreviewAnimation": {"class": PreviewAnimation, "name": "Preview Animation"},
"RemapImageRange": {"class": RemapImageRange, "name": "Remap Image Range"},
"ReverseImageBatch": {"class": ReverseImageBatch, "name": "Reverse Image Batch"},
"ReplaceImagesInBatch": {"class": ReplaceImagesInBatch, "name": "Replace Images In Batch"},
"SaveImageWithAlpha": {"class": SaveImageWithAlpha, "name": "Save Image With Alpha"},
"SplitImageChannels": {"class": SplitImageChannels, "name": "Split Image Channels"},
"SaveImageKJ": {"class": SaveImageKJ, "name": "Save Image KJ"},
"ShuffleImageBatch": {"class": ShuffleImageBatch, "name": "Shuffle Image Batch"},
"SplitImageChannels": {"class": SplitImageChannels, "name": "Split Image Channels"},
"TransitionImagesMulti": {"class": TransitionImagesMulti, "name": "Transition Images Multi"},
"TransitionImagesInBatch": {"class": TransitionImagesInBatch, "name": "Transition Images In Batch"},
#batch cropping
"BatchCropFromMask": {"class": BatchCropFromMask, "name": "Batch Crop From Mask"},
"BatchCropFromMaskAdvanced": {"class": BatchCropFromMaskAdvanced, "name": "Batch Crop From Mask Advanced"},
@@ -78,34 +115,52 @@ NODE_CONFIG = {
"InjectNoiseToLatent": {"class": InjectNoiseToLatent, "name": "Inject Noise To Latent"},
"CustomSigmas": {"class": CustomSigmas, "name": "Custom Sigmas"},
#utility
"StringToFloatList": {"class": StringToFloatList, "name": "String to Float List"},
"WidgetToString": {"class": WidgetToString, "name": "Widget To String"},
"DummyLatentOut": {"class": DummyLatentOut, "name": "Dummy Latent Out"},
"SaveStringKJ": {"class": SaveStringKJ, "name": "Save String KJ"},
"DummyOut": {"class": DummyOut, "name": "Dummy Out"},
"GetLatentsFromBatchIndexed": {"class": GetLatentsFromBatchIndexed, "name": "Get Latents From Batch Indexed"},
"ScaleBatchPromptSchedule": {"class": ScaleBatchPromptSchedule, "name": "Scale Batch Prompt Schedule"},
"CameraPoseVisualizer": {"class": CameraPoseVisualizer, "name": "Camera Pose Visualizer"},
"AppendStringsToList": {"class": AppendStringsToList, "name": "Append Strings To List"},
"JoinStrings": {"class": JoinStrings, "name": "Join Strings"},
"JoinStringMulti": {"class": JoinStringMulti, "name": "Join String Multi"},
"SomethingToString": {"class": SomethingToString, "name": "Something To String"},
"Sleep": {"class": Sleep, "name": "Sleep"},
"VRAM_Debug": {"class": VRAM_Debug, "name": "VRAM Debug"},
"SomethingToString": {"class": SomethingToString, "name": "Something To String"},
"EmptyLatentImagePresets": {"class": EmptyLatentImagePresets, "name": "Empty Latent Image Presets"},
"EmptyLatentImageCustomPresets": {"class": EmptyLatentImageCustomPresets, "name": "Empty Latent Image Custom Presets"},
"ModelPassThrough": {"class": ModelPassThrough, "name": "ModelPass"},
"ModelSaveKJ": {"class": ModelSaveKJ, "name": "Model Save KJ"},
"SetShakkerLabsUnionControlNetType": {"class": SetShakkerLabsUnionControlNetType, "name": "Set Shakker Labs Union ControlNet Type"},
"StyleModelApplyAdvanced": {"class": StyleModelApplyAdvanced, "name": "Style Model Apply Advanced"},
"DiffusionModelSelector": {"class": DiffusionModelSelector, "name": "Diffusion Model Selector"},
"LazySwitchKJ": {"class": LazySwitchKJ, "name": "Lazy Switch KJ"},
#audioscheduler stuff
"NormalizedAmplitudeToMask": {"class": NormalizedAmplitudeToMask},
"NormalizedAmplitudeToFloatList": {"class": NormalizedAmplitudeToFloatList},
"OffsetMaskByNormalizedAmplitude": {"class": OffsetMaskByNormalizedAmplitude},
"ImageTransformByNormalizedAmplitude": {"class": ImageTransformByNormalizedAmplitude},
"AudioConcatenate": {"class": AudioConcatenate},
#curve nodes
"SplineEditor": {"class": SplineEditor, "name": "Spline Editor"},
"CreateShapeImageOnPath": {"class": CreateShapeImageOnPath, "name": "Create Shape Image On Path"},
"CreateShapeMaskOnPath": {"class": CreateShapeMaskOnPath, "name": "Create Shape Mask On Path"},
"CreateTextOnPath": {"class": CreateTextOnPath, "name": "Create Text On Path"},
"CreateGradientFromCoords": {"class": CreateGradientFromCoords, "name": "Create Gradient From Coords"},
"CutAndDragOnPath": {"class": CutAndDragOnPath, "name": "Cut And Drag On Path"},
"GradientToFloat": {"class": GradientToFloat, "name": "Gradient To Float"},
"WeightScheduleExtend": {"class": WeightScheduleExtend, "name": "Weight Schedule Extend"},
"MaskOrImageToWeight": {"class": MaskOrImageToWeight, "name": "Mask Or Image To Weight"},
"WeightScheduleConvert": {"class": WeightScheduleConvert, "name": "Weight Schedule Convert"},
"FloatToMask": {"class": FloatToMask, "name": "Float To Mask"},
"FloatToSigmas": {"class": FloatToSigmas, "name": "Float To Sigmas"},
"SigmasToFloat": {"class": SigmasToFloat, "name": "Sigmas To Float"},
"PlotCoordinates": {"class": PlotCoordinates, "name": "Plot Coordinates"},
"InterpolateCoords": {"class": InterpolateCoords, "name": "Interpolate Coords"},
"PointsEditor": {"class": PointsEditor, "name": "Points Editor"},
#experimental
"StabilityAPI_SD3": {"class": StabilityAPI_SD3, "name": "Stability API SD3"},
"SoundReactive": {"class": SoundReactive, "name": "Sound Reactive"},
"StableZero123_BatchSchedule": {"class": StableZero123_BatchSchedule, "name": "Stable Zero123 Batch Schedule"},
"SV3D_BatchSchedule": {"class": SV3D_BatchSchedule, "name": "SV3D Batch Schedule"},
@@ -113,10 +168,51 @@ NODE_CONFIG = {
"Superprompt": {"class": Superprompt, "name": "Superprompt"},
"GLIGENTextBoxApplyBatchCoords": {"class": GLIGENTextBoxApplyBatchCoords},
"Intrinsic_lora_sampling": {"class": Intrinsic_lora_sampling, "name": "Intrinsic Lora Sampling"},
"CheckpointPerturbWeights": {"class": CheckpointPerturbWeights, "name": "CheckpointPerturbWeights"},
"Screencap_mss": {"class": Screencap_mss, "name": "Screencap mss"},
"WebcamCaptureCV2": {"class": WebcamCaptureCV2, "name": "Webcam Capture CV2"},
"DifferentialDiffusionAdvanced": {"class": DifferentialDiffusionAdvanced, "name": "Differential Diffusion Advanced"},
"DiTBlockLoraLoader": {"class": DiTBlockLoraLoader, "name": "DiT Block Lora Loader"},
"FluxBlockLoraSelect": {"class": FluxBlockLoraSelect, "name": "Flux Block Lora Select"},
"HunyuanVideoBlockLoraSelect": {"class": HunyuanVideoBlockLoraSelect, "name": "Hunyuan Video Block Lora Select"},
"Wan21BlockLoraSelect": {"class": Wan21BlockLoraSelect, "name": "Wan21 Block Lora Select"},
"CustomControlNetWeightsFluxFromList": {"class": CustomControlNetWeightsFluxFromList, "name": "Custom ControlNet Weights Flux From List"},
"CheckpointLoaderKJ": {"class": CheckpointLoaderKJ, "name": "CheckpointLoaderKJ"},
"DiffusionModelLoaderKJ": {"class": DiffusionModelLoaderKJ, "name": "Diffusion Model Loader KJ"},
"TorchCompileModelFluxAdvanced": {"class": TorchCompileModelFluxAdvanced, "name": "TorchCompileModelFluxAdvanced"},
"TorchCompileModelFluxAdvancedV2": {"class": TorchCompileModelFluxAdvancedV2, "name": "TorchCompileModelFluxAdvancedV2"},
"TorchCompileModelHyVideo": {"class": TorchCompileModelHyVideo, "name": "TorchCompileModelHyVideo"},
"TorchCompileVAE": {"class": TorchCompileVAE, "name": "TorchCompileVAE"},
"TorchCompileControlNet": {"class": TorchCompileControlNet, "name": "TorchCompileControlNet"},
"PatchModelPatcherOrder": {"class": PatchModelPatcherOrder, "name": "Patch Model Patcher Order"},
"TorchCompileLTXModel": {"class": TorchCompileLTXModel, "name": "TorchCompileLTXModel"},
"TorchCompileCosmosModel": {"class": TorchCompileCosmosModel, "name": "TorchCompileCosmosModel"},
"TorchCompileModelQwenImage": {"class": TorchCompileModelQwenImage, "name": "TorchCompileModelQwenImage"},
"TorchCompileModelWanVideo": {"class": TorchCompileModelWanVideo, "name": "TorchCompileModelWanVideo"},
"TorchCompileModelWanVideoV2": {"class": TorchCompileModelWanVideoV2, "name": "TorchCompileModelWanVideoV2"},
"PathchSageAttentionKJ": {"class": PathchSageAttentionKJ, "name": "Patch Sage Attention KJ"},
"LeapfusionHunyuanI2VPatcher": {"class": LeapfusionHunyuanI2V, "name": "Leapfusion Hunyuan I2V Patcher"},
"VAELoaderKJ": {"class": VAELoaderKJ, "name": "VAELoader KJ"},
"ScheduledCFGGuidance": {"class": ScheduledCFGGuidance, "name": "Scheduled CFG Guidance"},
"ApplyRifleXRoPE_HunuyanVideo": {"class": ApplyRifleXRoPE_HunuyanVideo, "name": "Apply RifleXRoPE HunuyanVideo"},
"ApplyRifleXRoPE_WanVideo": {"class": ApplyRifleXRoPE_WanVideo, "name": "Apply RifleXRoPE WanVideo"},
"WanVideoTeaCacheKJ": {"class": WanVideoTeaCacheKJ, "name": "WanVideo Tea Cache (native)"},
"WanVideoEnhanceAVideoKJ": {"class": WanVideoEnhanceAVideoKJ, "name": "WanVideo Enhance A Video (native)"},
"SkipLayerGuidanceWanVideo": {"class": SkipLayerGuidanceWanVideo, "name": "Skip Layer Guidance WanVideo"},
"TimerNodeKJ": {"class": TimerNodeKJ, "name": "Timer Node KJ"},
"HunyuanVideoEncodeKeyframesToCond": {"class": HunyuanVideoEncodeKeyframesToCond, "name": "HunyuanVideo Encode Keyframes To Cond"},
"CFGZeroStarAndInit": {"class": CFGZeroStarAndInit, "name": "CFG Zero Star/Init"},
"ModelPatchTorchSettings": {"class": ModelPatchTorchSettings, "name": "Model Patch Torch Settings"},
"WanVideoNAG": {"class": WanVideoNAG, "name": "WanVideoNAG"},
#instance diffusion
"CreateInstanceDiffusionTracking": {"class": CreateInstanceDiffusionTracking},
"AppendInstanceDiffusionTracking": {"class": AppendInstanceDiffusionTracking},
"DrawInstanceDiffusionTracking": {"class": DrawInstanceDiffusionTracking},
#lora
"LoraExtractKJ": {"class": LoraExtractKJ, "name": "LoraExtractKJ"},
"LoraReduceRankKJ": {"class": LoraReduceRank, "name": "LoraReduceRank"}
}
def generate_node_mappings(node_config):
@@ -140,9 +236,11 @@ from server import PromptServer
from pathlib import Path
if hasattr(PromptServer, "instance"):
# NOTE: we add an extra static path to avoid comfy mechanism
# that loads every script in web.
PromptServer.instance.app.add_routes(
[web.static("/kjweb_async", (Path(__file__).parent.absolute() / "kjweb_async").as_posix())]
)
try:
# NOTE: we add an extra static path to avoid comfy mechanism
# that loads every script in web.
PromptServer.instance.app.add_routes(
[web.static("/kjweb_async", (Path(__file__).parent.absolute() / "kjweb_async").as_posix())]
)
except:
pass
-3
View File
@@ -1,3 +0,0 @@
{
"sai_api_key": "your_api_key_here"
}
+22
View File
@@ -0,0 +1,22 @@
[
{
"label": "SD",
"value": "512x512"
},
{
"label": "HD",
"value": "768x768"
},
{
"label": "Full HD",
"value": "1024x1024"
},
{
"label": "4k",
"value": "2048x2048"
},
{
"label": "SVD",
"value": "1024x576"
}
]
File diff suppressed because it is too large Load Diff
+38 -12
View File
@@ -694,6 +694,7 @@ class BboxVisualize:
"images": ("IMAGE",),
"bboxes": ("BBOX",),
"line_width": ("INT", {"default": 1,"min": 1, "max": 10, "step": 1}),
"bbox_format": (["xywh", "xyxy"], {"default": "xywh"}),
},
}
@@ -706,31 +707,56 @@ Visualizes the specified bbox on the image.
CATEGORY = "KJNodes/masking"
def visualizebbox(self, bboxes, images, line_width):
def visualizebbox(self, bboxes, images, line_width, bbox_format):
image_list = []
for image, bbox in zip(images, bboxes):
x_min, y_min, width, height = bbox
if bbox_format == "xywh":
x_min, y_min, width, height = bbox
elif bbox_format == "xyxy":
x_min, y_min, x_max, y_max = bbox
width = x_max - x_min
height = y_max - y_min
else:
raise ValueError(f"Unknown bbox_format: {bbox_format}")
# Ensure bbox coordinates are integers
x_min = int(x_min)
y_min = int(y_min)
width = int(width)
height = int(height)
# Permute the image dimensions
image = image.permute(2, 0, 1)
# Clone the image to draw bounding boxes
img_with_bbox = image.clone()
# Define the color for the bbox, e.g., red
color = torch.tensor([1, 0, 0], dtype=torch.float32)
# Ensure color tensor matches the image channels
if color.shape[0] != img_with_bbox.shape[0]:
color = color.unsqueeze(1).expand(-1, line_width)
# Draw lines for each side of the bbox with the specified line width
for lw in range(line_width):
# Top horizontal line
img_with_bbox[:, y_min + lw, x_min:x_min + width] = color[:, None]
if y_min + lw < img_with_bbox.shape[1]:
img_with_bbox[:, y_min + lw, x_min:x_min + width] = color[:, None]
# Bottom horizontal line
img_with_bbox[:, y_min + height - lw, x_min:x_min + width] = color[:, None]
if y_min + height - lw < img_with_bbox.shape[1]:
img_with_bbox[:, y_min + height - lw, x_min:x_min + width] = color[:, None]
# Left vertical line
img_with_bbox[:, y_min:y_min + height, x_min + lw] = color[:, None]
if x_min + lw < img_with_bbox.shape[2]:
img_with_bbox[:, y_min:y_min + height, x_min + lw] = color[:, None]
# Right vertical line
img_with_bbox[:, y_min:y_min + height, x_min + width - lw] = color[:, None]
if x_min + width - lw < img_with_bbox.shape[2]:
img_with_bbox[:, y_min:y_min + height, x_min + width - lw] = color[:, None]
# Permute the image dimensions back
img_with_bbox = img_with_bbox.permute(1, 2, 0).unsqueeze(0)
image_list.append(img_with_bbox)
+726 -42
View File
@@ -1,10 +1,49 @@
import torch
from torchvision import transforms
import json
from PIL import Image, ImageDraw, ImageFont
from PIL import Image, ImageDraw, ImageFont, ImageColor, ImageFilter, ImageChops
import numpy as np
from ..utility.utility import pil2tensor
from ..utility.utility import pil2tensor, tensor2pil
import folder_paths
import io
import base64
from comfy.utils import common_upscale
def parse_color(color):
if isinstance(color, str) and ',' in color:
return tuple(int(c.strip()) for c in color.split(','))
return color
def parse_json_tracks(tracks):
tracks_data = []
try:
# If tracks is a string, try to parse it as JSON
if isinstance(tracks, str):
parsed = json.loads(tracks.replace("'", '"'))
tracks_data.extend(parsed)
else:
# If tracks is a list of strings, parse each one
for track_str in tracks:
parsed = json.loads(track_str.replace("'", '"'))
tracks_data.append(parsed)
# Check if we have a single track (dict with x,y) or a list of tracks
if tracks_data and isinstance(tracks_data[0], dict) and 'x' in tracks_data[0]:
# Single track detected, wrap it in a list
tracks_data = [tracks_data]
elif tracks_data and isinstance(tracks_data[0], list) and tracks_data[0] and isinstance(tracks_data[0][0], dict) and 'x' in tracks_data[0][0]:
# Already a list of tracks, nothing to do
pass
else:
# Unexpected format
print(f"Warning: Unexpected track format: {type(tracks_data[0])}")
except json.JSONDecodeError as e:
print(f"Error parsing tracks JSON: {e}")
tracks_data = []
return tracks_data
def plot_coordinates_to_tensor(coordinates, height, width, bbox_height, bbox_width, size_multiplier, prompt):
import matplotlib
@@ -90,8 +129,10 @@ Plots coordinates to sequence of images using Matplotlib.
def append(self, coordinates, text, width, height, bbox_width, bbox_height, size_multiplier=[1.0]):
coordinates = json.loads(coordinates.replace("'", '"'))
coordinates = [(coord['x'], coord['y']) for coord in coordinates]
batch_size = len(coordinates)
if len(size_multiplier) != batch_size:
batch_size = len(coordinates)
if not size_multiplier or len(size_multiplier) != batch_size:
size_multiplier = [0] * batch_size
else:
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
plot_image_tensor = plot_coordinates_to_tensor(coordinates, height, width, bbox_height, bbox_width, size_multiplier, text)
@@ -113,6 +154,8 @@ class SplineEditor:
[
'path',
'time',
'controlpoints',
'speed'
],
{
"default": 'time'
@@ -146,11 +189,12 @@ class SplineEditor:
"optional": {
"min_value": ("FLOAT", {"default": 0.0, "min": -10000.0, "max": 10000.0, "step": 0.01}),
"max_value": ("FLOAT", {"default": 1.0, "min": -10000.0, "max": 10000.0, "step": 0.01}),
"bg_image": ("IMAGE", ),
}
}
RETURN_TYPES = ("MASK", "STRING", "FLOAT", "INT")
RETURN_NAMES = ("mask", "coord_str", "float", "count")
RETURN_TYPES = ("MASK", "STRING", "FLOAT", "INT", "STRING",)
RETURN_NAMES = ("mask", "coord_str", "float", "count", "normalized_str",)
FUNCTION = "splinedata"
CATEGORY = "KJNodes/weights"
DESCRIPTION = """
@@ -168,6 +212,16 @@ guaranteed!!
Note that you can't delete from start/end.
Right click on canvas for context menu:
NEW!:
- Add new spline
- Creates a new spline on same canvas, currently these paths are only outputed
as coordinates.
- Add single point
- Creates a single point that only returns it's current position coords
- Delete spline
- Deletes the currently selected spline, you can select a spline by clicking on
it's path, or cycle through them with the 'Next spline' -option.
These are purely visual options, doesn't affect the output:
- Toggle handles visibility
- Display sample points: display the points to be returned.
@@ -178,6 +232,7 @@ actual control points, so the interpolation type matters.
sampling_method:
- time: samples along the time axis, used for schedules
- path: samples along the path itself, useful for coordinates
- controlpoints: samples only the control points themselves
output types:
- mask batch
@@ -192,36 +247,77 @@ output types:
"""
def splinedata(self, mask_width, mask_height, coordinates, float_output_type, interpolation,
points_to_sample, sampling_method, points_store, tension, repeat_output, min_value=0.0, max_value=1.0):
points_to_sample, sampling_method, points_store, tension, repeat_output,
min_value=0.0, max_value=1.0, bg_image=None):
coordinates = json.loads(coordinates)
for coord in coordinates:
coord['x'] = int(round(coord['x']))
coord['y'] = int(round(coord['y']))
# Handle nested list structure if present
all_normalized = []
all_normalized_y_values = []
# Check if we have a nested list structure
if isinstance(coordinates, list) and len(coordinates) > 0 and isinstance(coordinates[0], list):
# Process each list of coordinates in the nested structure
coordinate_sets = coordinates
else:
# If not nested, treat as a single list of coordinates
coordinate_sets = [coordinates]
# Process each set of coordinates
for coord_set in coordinate_sets:
normalized = []
normalized_y_values = []
normalized_y_values = [
(1.0 - (point['y'] / mask_height) - 0.0) * (max_value - min_value) + min_value
for point in coordinates
]
for coord in coord_set:
coord['x'] = int(round(coord['x']))
coord['y'] = int(round(coord['y']))
norm_x = (1.0 - (coord['x'] / mask_height) - 0.0) * (max_value - min_value) + min_value
norm_y = (1.0 - (coord['y'] / mask_height) - 0.0) * (max_value - min_value) + min_value
normalized_y_values.append(norm_y)
normalized.append({'x':norm_x, 'y':norm_y})
all_normalized.extend(normalized)
all_normalized_y_values.extend(normalized_y_values)
# Use the combined normalized values for output
if float_output_type == 'list':
out_floats = normalized_y_values * repeat_output
out_floats = all_normalized_y_values * repeat_output
elif float_output_type == 'pandas series':
try:
import pandas as pd
except:
raise Exception("MaskOrImageToWeight: pandas is not installed. Please install pandas to use this output_type")
out_floats = pd.Series(normalized_y_values * repeat_output),
out_floats = pd.Series(all_normalized_y_values * repeat_output),
elif float_output_type == 'tensor':
out_floats = torch.tensor(normalized_y_values * repeat_output, dtype=torch.float32)
out_floats = torch.tensor(all_normalized_y_values * repeat_output, dtype=torch.float32)
# Create a color map for grayscale intensities
color_map = lambda y: torch.full((mask_height, mask_width, 3), y, dtype=torch.float32)
# Create image tensors for each normalized y value
mask_tensors = [color_map(y) for y in normalized_y_values]
mask_tensors = [color_map(y) for y in all_normalized_y_values]
masks_out = torch.stack(mask_tensors)
masks_out = masks_out.repeat(repeat_output, 1, 1, 1)
masks_out = masks_out.mean(dim=-1)
return (masks_out, str(coordinates), out_floats, len(out_floats))
if bg_image is None:
return (masks_out, json.dumps(coordinates if len(coordinates) > 1 else coordinates[0]), out_floats, len(out_floats), json.dumps(all_normalized))
else:
transform = transforms.ToPILImage()
image = transform(bg_image[0].permute(2, 0, 1))
buffered = io.BytesIO()
image.save(buffered, format="JPEG", quality=75)
# Encode the image bytes to a Base64 string
img_bytes = buffered.getvalue()
img_base64 = base64.b64encode(img_bytes).decode('utf-8')
return {
"ui": {"bg_image": [img_base64]},
"result": (masks_out, json.dumps(coordinates if len(coordinates) > 1 else coordinates[0]), out_floats, len(out_floats), json.dumps(all_normalized))
}
class CreateShapeMaskOnPath:
@@ -232,8 +328,8 @@ class CreateShapeMaskOnPath:
DESCRIPTION = """
Creates a mask or batch of masks with the specified shape.
Locations are center locations.
Grow value is the amount to grow the shape on each frame, creating animated masks.
"""
DEPRECATED = True
@classmethod
def INPUT_TYPES(s):
@@ -266,7 +362,9 @@ Grow value is the amount to grow the shape on each frame, creating animated mask
batch_size = len(coordinates)
out = []
color = "white"
if len(size_multiplier) != batch_size:
if not size_multiplier or len(size_multiplier) != batch_size:
size_multiplier = [0] * batch_size
else:
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
for i, coord in enumerate(coordinates):
image = Image.new("RGB", (frame_width, frame_height), "black")
@@ -302,6 +400,320 @@ Grow value is the amount to grow the shape on each frame, creating animated mask
out.append(mask)
outstack = torch.cat(out, dim=0)
return (outstack, 1.0 - outstack,)
class CreateShapeImageOnPath:
RETURN_TYPES = ("IMAGE", "MASK",)
RETURN_NAMES = ("image","mask", )
FUNCTION = "createshapemask"
CATEGORY = "KJNodes/image"
DESCRIPTION = """
Creates an image or batch of images with the specified shape.
Locations are center locations.
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"shape": (
[ 'circle',
'square',
'triangle',
],
{
"default": 'circle'
}),
"coordinates": ("STRING", {"forceInput": True}),
"frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
"frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
"shape_width": ("INT", {"default": 128,"min": 2, "max": 4096, "step": 1}),
"shape_height": ("INT", {"default": 128,"min": 2, "max": 4096, "step": 1}),
"shape_color": ("STRING", {"default": 'white'}),
"bg_color": ("STRING", {"default": 'black'}),
"blur_radius": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100, "step": 0.1}),
"intensity": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0, "step": 0.01}),
},
"optional": {
"size_multiplier": ("FLOAT", {"default": [1.0], "forceInput": True}),
"trailing": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}),
"border_width": ("INT", {"default": 0, "min": 0, "max": 100, "step": 1}),
"border_color": ("STRING", {"default": 'black'}),
}
}
def createshapemask(self, coordinates, frame_width, frame_height, shape_width, shape_height, shape_color,
bg_color, blur_radius, shape, intensity, size_multiplier=[1.0], trailing=1.0, border_width=0, border_color='black'):
shape_color = parse_color(shape_color)
border_color = parse_color(border_color)
bg_color = parse_color(bg_color)
coords_list = parse_json_tracks(coordinates)
batch_size = len(coords_list[0])
images_list = []
masks_list = []
if not size_multiplier or len(size_multiplier) != batch_size:
size_multiplier = [1] * batch_size
else:
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
previous_output = None
for i in range(batch_size):
image = Image.new("RGB", (frame_width, frame_height), bg_color)
draw = ImageDraw.Draw(image)
# Calculate the size for this frame and ensure it's not less than 0
current_width = shape_width * size_multiplier[i]
current_height = shape_height * size_multiplier[i]
for coords in coords_list:
location_x = coords[i]['x']
location_y = coords[i]['y']
if shape == 'circle' or shape == 'square':
# Define the bounding box for the shape
left_up_point = (location_x - current_width // 2, location_y - current_height // 2)
right_down_point = (location_x + current_width // 2, location_y + current_height // 2)
two_points = [left_up_point, right_down_point]
if shape == 'circle':
if border_width > 0:
draw.ellipse(two_points, fill=shape_color, outline=border_color, width=border_width)
else:
draw.ellipse(two_points, fill=shape_color)
elif shape == 'square':
if border_width > 0:
draw.rectangle(two_points, fill=shape_color, outline=border_color, width=border_width)
else:
draw.rectangle(two_points, fill=shape_color)
elif shape == 'triangle':
# Define the points for the triangle
left_up_point = (location_x - current_width // 2, location_y + current_height // 2) # bottom left
right_down_point = (location_x + current_width // 2, location_y + current_height // 2) # bottom right
top_point = (location_x, location_y - current_height // 2) # top point
if border_width > 0:
draw.polygon([top_point, left_up_point, right_down_point], fill=shape_color, outline=border_color, width=border_width)
else:
draw.polygon([top_point, left_up_point, right_down_point], fill=shape_color)
if blur_radius != 0:
image = image.filter(ImageFilter.GaussianBlur(blur_radius))
# Blend the current image with the accumulated image
image = pil2tensor(image)
if trailing != 1.0 and previous_output is not None:
# Add the decayed previous output to the current frame
image += trailing * previous_output
image = image / image.max()
previous_output = image
image = image * intensity
mask = image[:, :, :, 0]
masks_list.append(mask)
images_list.append(image)
out_images = torch.cat(images_list, dim=0).cpu().float()
out_masks = torch.cat(masks_list, dim=0)
return (out_images, out_masks)
class CreateTextOnPath:
RETURN_TYPES = ("IMAGE", "MASK", "MASK",)
RETURN_NAMES = ("image", "mask", "mask_inverted",)
FUNCTION = "createtextmask"
CATEGORY = "KJNodes/masking/generate"
DESCRIPTION = """
Creates a mask or batch of masks with the specified text.
Locations are center locations.
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"coordinates": ("STRING", {"forceInput": True}),
"text": ("STRING", {"default": 'text', "multiline": True}),
"frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
"frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
"font": (folder_paths.get_filename_list("kjnodes_fonts"), ),
"font_size": ("INT", {"default": 42}),
"alignment": (
[ 'left',
'center',
'right'
],
{"default": 'center'}
),
"text_color": ("STRING", {"default": 'white'}),
},
"optional": {
"size_multiplier": ("FLOAT", {"default": [1.0], "forceInput": True}),
}
}
def createtextmask(self, coordinates, frame_width, frame_height, font, font_size, text, text_color, alignment, size_multiplier=[1.0]):
coordinates = coordinates.replace("'", '"')
coordinates = json.loads(coordinates)
batch_size = len(coordinates)
mask_list = []
image_list = []
color = parse_color(text_color)
font_path = folder_paths.get_full_path("kjnodes_fonts", font)
if len(size_multiplier) != batch_size:
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
for i, coord in enumerate(coordinates):
image = Image.new("RGB", (frame_width, frame_height), "black")
draw = ImageDraw.Draw(image)
lines = text.split('\n') # Split the text into lines
# Apply the size multiplier to the font size for this iteration
current_font_size = int(font_size * size_multiplier[i])
current_font = ImageFont.truetype(font_path, current_font_size)
line_heights = [current_font.getbbox(line)[3] for line in lines] # List of line heights
total_text_height = sum(line_heights) # Total height of text block
# Calculate the starting Y position to center the block of text
start_y = coord['y'] - total_text_height // 2
for j, line in enumerate(lines):
text_width, text_height = current_font.getbbox(line)[2], line_heights[j]
if alignment == 'left':
location_x = coord['x']
elif alignment == 'center':
location_x = int(coord['x'] - text_width // 2)
elif alignment == 'right':
location_x = int(coord['x'] - text_width)
location_y = int(start_y + sum(line_heights[:j]))
text_position = (location_x, location_y)
# Draw the text
try:
draw.text(text_position, line, fill=color, font=current_font, features=['-liga'])
except:
draw.text(text_position, line, fill=color, font=current_font)
image = pil2tensor(image)
non_black_pixels = (image > 0).any(dim=-1)
mask = non_black_pixels.to(image.dtype)
mask_list.append(mask)
image_list.append(image)
out_images = torch.cat(image_list, dim=0).cpu().float()
out_masks = torch.cat(mask_list, dim=0)
return (out_images, out_masks, 1.0 - out_masks,)
class CreateGradientFromCoords:
RETURN_TYPES = ("IMAGE", )
RETURN_NAMES = ("image", )
FUNCTION = "generate"
CATEGORY = "KJNodes/image"
DESCRIPTION = """
Creates a gradient image from coordinates.
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"coordinates": ("STRING", {"forceInput": True}),
"frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
"frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
"start_color": ("STRING", {"default": 'white'}),
"end_color": ("STRING", {"default": 'black'}),
"multiplier": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 100.0, "step": 0.01}),
},
}
def generate(self, coordinates, frame_width, frame_height, start_color, end_color, multiplier):
# Parse the coordinates
coordinates = json.loads(coordinates.replace("'", '"'))
# Create an image
image = Image.new("RGB", (frame_width, frame_height))
draw = ImageDraw.Draw(image)
# Extract start and end points for the gradient
start_coord = coordinates[0]
end_coord = coordinates[1]
start_color = parse_color(start_color)
end_color = parse_color(end_color)
# Calculate the gradient direction (vector)
gradient_direction = (end_coord['x'] - start_coord['x'], end_coord['y'] - start_coord['y'])
gradient_length = (gradient_direction[0] ** 2 + gradient_direction[1] ** 2) ** 0.5
# Iterate over each pixel in the image
for y in range(frame_height):
for x in range(frame_width):
# Calculate the projection of the point on the gradient line
point_vector = (x - start_coord['x'], y - start_coord['y'])
projection = (point_vector[0] * gradient_direction[0] + point_vector[1] * gradient_direction[1]) / gradient_length
projection = max(min(projection, gradient_length), 0) # Clamp the projection value
# Calculate the blend factor for the current pixel
blend = projection * multiplier / gradient_length
# Determine the color of the current pixel
color = (
int(start_color[0] + (end_color[0] - start_color[0]) * blend),
int(start_color[1] + (end_color[1] - start_color[1]) * blend),
int(start_color[2] + (end_color[2] - start_color[2]) * blend)
)
# Set the pixel color
draw.point((x, y), fill=color)
# Convert the PIL image to a tensor (assuming such a function exists in your context)
image_tensor = pil2tensor(image)
return (image_tensor,)
class GradientToFloat:
RETURN_TYPES = ("FLOAT", "FLOAT",)
RETURN_NAMES = ("float_x", "float_y", )
FUNCTION = "sample"
CATEGORY = "KJNodes/image"
DESCRIPTION = """
Calculates list of floats from image.
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE", ),
"steps": ("INT", {"default": 10, "min": 2, "max": 10000, "step": 1}),
},
}
def sample(self, image, steps):
# Assuming image is a tensor with shape [B, H, W, C]
B, H, W, C = image.shape
# Sample along the width axis (W)
w_intervals = torch.linspace(0, W - 1, steps=steps, dtype=torch.int64)
# Assuming we're sampling from the first batch and the first channel
w_sampled = image[0, :, w_intervals, 0]
# Sample along the height axis (H)
h_intervals = torch.linspace(0, H - 1, steps=steps, dtype=torch.int64)
# Assuming we're sampling from the first batch and the first channel
h_sampled = image[0, h_intervals, :, 0]
# Taking the mean across the height for width sampling, and across the width for height sampling
w_values = w_sampled.mean(dim=0).tolist()
h_values = h_sampled.mean(dim=1).tolist()
return (w_values, h_values)
class MaskOrImageToWeight:
@@ -347,7 +759,7 @@ and returns that as the selected output type.
# Convert mean_values to the specified output_type
if output_type == 'list':
out = mean_values,
out = mean_values
elif output_type == 'pandas series':
try:
import pandas as pd
@@ -604,6 +1016,25 @@ Creates a sigmas tensor from list of float values.
def customsigmas(self, float_list):
return torch.tensor(float_list, dtype=torch.float32),
class SigmasToFloat:
@classmethod
def INPUT_TYPES(s):
return {"required":
{
"sigmas": ("SIGMAS",),
}
}
RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("float",)
CATEGORY = "KJNodes/noise"
FUNCTION = "customsigmas"
DESCRIPTION = """
Creates a float list from sigmas tensors.
"""
def customsigmas(self, sigmas):
return sigmas.tolist(),
class GLIGENTextBoxApplyBatchCoords:
@classmethod
def INPUT_TYPES(s):
@@ -732,8 +1163,10 @@ for example:
batch_size = len(coordinates)
# Initialize a list to hold the coordinates for the current ID
id_coordinates = []
if len(size_multiplier) != batch_size:
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
if not size_multiplier or len(size_multiplier) != batch_size:
size_multiplier = [0] * batch_size
else:
size_multiplier = size_multiplier * (batch_size // len(size_multiplier)) + size_multiplier[:batch_size % len(size_multiplier)]
for i, coord in enumerate(coordinates):
x = coord['x']
y = coord['y']
@@ -751,6 +1184,13 @@ for example:
top_left_y = max(0, top_left_y)
bottom_right_x = min(width, bottom_right_x)
bottom_right_y = min(height, bottom_right_y)
# Ensure width and height are positive
adjusted_bbox_width = max(1, bottom_right_x - top_left_x)
adjusted_bbox_height = max(1, bottom_right_y - top_left_y)
# Update the coordinates with the new width and height
bottom_right_x = top_left_x + adjusted_bbox_width
bottom_right_y = top_left_y + adjusted_bbox_height
# Append the top left and bottom right coordinates to the list for the current ID
id_coordinates.append([top_left_x, top_left_y, bottom_right_x, bottom_right_y, width, height])
@@ -830,48 +1270,51 @@ Interpolates coordinates based on a curve.
}
def interpolate(self, coordinates, interpolation_curve):
# Parse the JSON string to get the list of coordinates
# Parse the JSON string to get the list of coordinates
coordinates = json.loads(coordinates.replace("'", '"'))
# Convert the list of dictionaries to a list of (x, y) tuples for easier processing
coordinates = [(coord['x'], coord['y']) for coord in coordinates]
# Calculate the total length of the original path
path_length = sum(np.linalg.norm(np.array(coordinates[i]) - np.array(coordinates[i-1])) for i in range(1, len(coordinates)))
# Normalize the interpolation curve
normalized_curve = [x / path_length for x in interpolation_curve]
path_length = sum(np.linalg.norm(np.array(coordinates[i]) - np.array(coordinates[i-1]))
for i in range(1, len(coordinates)))
# Initialize variables for interpolation
interpolated_coords = []
current_length = 0
current_index = 1
current_index = 0
# Iterate over the normalized curve
for target_length in normalized_curve:
target_length *= path_length # Convert back to the original scale
while current_length < target_length and current_index < len(coordinates):
segment_length = np.linalg.norm(np.array(coordinates[current_index]) - np.array(coordinates[current_index-1]))
for normalized_length in interpolation_curve:
target_length = normalized_length * path_length # Convert to the original scale
while current_index < len(coordinates) - 1:
segment_start, segment_end = np.array(coordinates[current_index]), np.array(coordinates[current_index + 1])
segment_length = np.linalg.norm(segment_end - segment_start)
if current_length + segment_length >= target_length:
break
current_length += segment_length
current_index += 1
# Interpolate between the last two points
if current_index == 1:
interpolated_coords.append(coordinates[0])
else:
p1, p2 = np.array(coordinates[current_index-2]), np.array(coordinates[current_index-1])
if current_index < len(coordinates) - 1:
p1, p2 = np.array(coordinates[current_index]), np.array(coordinates[current_index + 1])
segment_length = np.linalg.norm(p2 - p1)
if segment_length > 0:
t = (target_length - (current_length - segment_length)) / segment_length
t = (target_length - current_length) / segment_length
interpolated_point = p1 + t * (p2 - p1)
interpolated_coords.append(interpolated_point.tolist())
else:
interpolated_coords.append(p1.tolist())
else:
# If the target_length is at or beyond the end of the path, add the last coordinate
interpolated_coords.append(coordinates[-1])
# Convert back to string format if necessary
interpolated_coords_str = "[" + ", ".join([f"{{'x': {round(coord[0])}, 'y': {round(coord[1])}}}" for coord in interpolated_coords]) + "]"
print(interpolated_coords_str)
return (interpolated_coords_str, )
return (interpolated_coords_str,)
class DrawInstanceDiffusionTracking:
@@ -949,4 +1392,245 @@ CreateInstanceDiffusionTracking -node.
# Stack the modified images back into a batch
image_tensor_batch = torch.stack(modified_images).cpu().float()
return image_tensor_batch,
return image_tensor_batch,
class PointsEditor:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"points_store": ("STRING", {"multiline": False}),
"coordinates": ("STRING", {"multiline": False}),
"neg_coordinates": ("STRING", {"multiline": False}),
"bbox_store": ("STRING", {"multiline": False}),
"bboxes": ("STRING", {"multiline": False}),
"bbox_format": (
[
'xyxy',
'xywh',
],
),
"width": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}),
"height": ("INT", {"default": 512, "min": 8, "max": 4096, "step": 8}),
"normalize": ("BOOLEAN", {"default": False}),
},
"optional": {
"bg_image": ("IMAGE", ),
},
}
RETURN_TYPES = ("STRING", "STRING", "BBOX", "MASK", "IMAGE")
RETURN_NAMES = ("positive_coords", "negative_coords", "bbox", "bbox_mask", "cropped_image")
FUNCTION = "pointdata"
CATEGORY = "KJNodes/experimental"
DESCRIPTION = """
# WORK IN PROGRESS
Do not count on this as part of your workflow yet,
probably contains lots of bugs and stability is not
guaranteed!!
## Graphical editor to create coordinates
**Shift + click** to add a positive (green) point.
**Shift + right click** to add a negative (red) point.
**Ctrl + click** to draw a box.
**Right click on a point** to delete it.
Note that you can't delete from start/end of the points array.
To add an image select the node and copy/paste or drag in the image.
Or from the bg_image input on queue (first frame of the batch).
**THE IMAGE IS SAVED TO THE NODE AND WORKFLOW METADATA**
you can clear the image from the context menu by right clicking on the canvas
"""
def pointdata(self, points_store, bbox_store, width, height, coordinates, neg_coordinates, normalize, bboxes, bbox_format="xyxy", bg_image=None):
coordinates = json.loads(coordinates)
pos_coordinates = []
for coord in coordinates:
coord['x'] = int(round(coord['x']))
coord['y'] = int(round(coord['y']))
if normalize:
norm_x = coord['x'] / width
norm_y = coord['y'] / height
pos_coordinates.append({'x': norm_x, 'y': norm_y})
else:
pos_coordinates.append({'x': coord['x'], 'y': coord['y']})
if neg_coordinates:
coordinates = json.loads(neg_coordinates)
neg_coordinates = []
for coord in coordinates:
coord['x'] = int(round(coord['x']))
coord['y'] = int(round(coord['y']))
if normalize:
norm_x = coord['x'] / width
norm_y = coord['y'] / height
neg_coordinates.append({'x': norm_x, 'y': norm_y})
else:
neg_coordinates.append({'x': coord['x'], 'y': coord['y']})
# Create a blank mask
mask = np.zeros((height, width), dtype=np.uint8)
bboxes = json.loads(bboxes)
print(bboxes)
valid_bboxes = []
for bbox in bboxes:
if (bbox.get("startX") is None or
bbox.get("startY") is None or
bbox.get("endX") is None or
bbox.get("endY") is None):
continue # Skip this bounding box if any value is None
else:
# Ensure that endX and endY are greater than startX and startY
x_min = min(int(bbox["startX"]), int(bbox["endX"]))
y_min = min(int(bbox["startY"]), int(bbox["endY"]))
x_max = max(int(bbox["startX"]), int(bbox["endX"]))
y_max = max(int(bbox["startY"]), int(bbox["endY"]))
valid_bboxes.append((x_min, y_min, x_max, y_max))
bboxes_xyxy = []
for bbox in valid_bboxes:
x_min, y_min, x_max, y_max = bbox
bboxes_xyxy.append((x_min, y_min, x_max, y_max))
mask[y_min:y_max, x_min:x_max] = 1 # Fill the bounding box area with 1s
if bbox_format == "xywh":
bboxes_xywh = []
for bbox in valid_bboxes:
x_min, y_min, x_max, y_max = bbox
width = x_max - x_min
height = y_max - y_min
bboxes_xywh.append((x_min, y_min, width, height))
bboxes = bboxes_xywh
else:
bboxes = bboxes_xyxy
mask_tensor = torch.from_numpy(mask)
mask_tensor = mask_tensor.unsqueeze(0).float().cpu()
if bg_image is not None and len(valid_bboxes) > 0:
x_min, y_min, x_max, y_max = bboxes[0]
cropped_image = bg_image[:, y_min:y_max, x_min:x_max, :]
elif bg_image is not None:
cropped_image = bg_image
if bg_image is None:
return (json.dumps(pos_coordinates), json.dumps(neg_coordinates), bboxes, mask_tensor)
else:
transform = transforms.ToPILImage()
image = transform(bg_image[0].permute(2, 0, 1))
buffered = io.BytesIO()
image.save(buffered, format="JPEG", quality=75)
# Step 3: Encode the image bytes to a Base64 string
img_bytes = buffered.getvalue()
img_base64 = base64.b64encode(img_bytes).decode('utf-8')
return {
"ui": {"bg_image": [img_base64]},
"result": (json.dumps(pos_coordinates), json.dumps(neg_coordinates), bboxes, mask_tensor, cropped_image)
}
class CutAndDragOnPath:
RETURN_TYPES = ("IMAGE", "MASK",)
RETURN_NAMES = ("image","mask", )
FUNCTION = "cutanddrag"
CATEGORY = "KJNodes/image"
DESCRIPTION = """
Cuts the masked area from the image, and drags it along the path. If inpaint is enabled, and no bg_image is provided, the cut area is filled using cv2 TELEA algorithm.
"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"coordinates": ("STRING", {"forceInput": True}),
"mask": ("MASK",),
"frame_width": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
"frame_height": ("INT", {"default": 512,"min": 16, "max": 4096, "step": 1}),
"inpaint": ("BOOLEAN", {"default": True}),
},
"optional": {
"bg_image": ("IMAGE",),
}
}
def cutanddrag(self, image, coordinates, mask, frame_width, frame_height, inpaint, bg_image=None):
# Parse coordinates
coords_list = parse_json_tracks(coordinates)
batch_size = len(coords_list[0])
images_list = []
masks_list = []
# Convert input image and mask to PIL
input_image = tensor2pil(image)[0]
input_mask = tensor2pil(mask)[0]
# Find masked region bounds
mask_array = np.array(input_mask)
y_indices, x_indices = np.where(mask_array > 0)
if len(x_indices) == 0 or len(y_indices) == 0:
return (image, mask)
x_min, x_max = x_indices.min(), x_indices.max()
y_min, y_max = y_indices.min(), y_indices.max()
# Cut out the masked region
cut_width = x_max - x_min
cut_height = y_max - y_min
cut_image = input_image.crop((x_min, y_min, x_max, y_max))
cut_mask = input_mask.crop((x_min, y_min, x_max, y_max))
# Create inpainted background
if bg_image is None:
background = input_image.copy()
# Inpaint the cut area
if inpaint:
import cv2
border = 5 # Create small border around cut area for better inpainting
fill_mask = Image.new("L", background.size, 0)
draw = ImageDraw.Draw(fill_mask)
draw.rectangle([x_min-border, y_min-border, x_max+border, y_max+border], fill=255)
background = cv2.inpaint(
np.array(background),
np.array(fill_mask),
inpaintRadius=3,
flags=cv2.INPAINT_TELEA
)
background = Image.fromarray(background)
else:
background = tensor2pil(bg_image)[0]
# Create batch of images with cut region at different positions
for i in range(batch_size):
# Create new image
new_image = background.copy()
new_mask = Image.new("L", (frame_width, frame_height), 0)
# Get target position from coordinates
for coords in coords_list:
target_x = int(coords[i]['x'] - cut_width/2)
target_y = int(coords[i]['y'] - cut_height/2)
# Paste cut region at new position
new_image.paste(cut_image, (target_x, target_y), cut_mask)
new_mask.paste(cut_mask, (target_x, target_y))
# Convert to tensor and append
image_tensor = pil2tensor(new_image)
mask_tensor = pil2tensor(new_mask)
images_list.append(image_tensor)
masks_list.append(mask_tensor)
# Stack tensors into batches
out_images = torch.cat(images_list, dim=0).cpu().float()
out_masks = torch.cat(masks_list, dim=0)
return (out_images, out_masks)
+2891 -170
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -7,7 +7,7 @@ import comfy.sample
from nodes import CLIPTextEncode
script_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
folder_paths.add_model_folder_path("intristic_loras", os.path.join(script_directory, "intristic_loras"))
folder_paths.add_model_folder_path("intrinsic_loras", os.path.join(script_directory, "intrinsic_loras"))
class Intrinsic_lora_sampling:
def __init__(self):
@@ -16,7 +16,7 @@ class Intrinsic_lora_sampling:
@classmethod
def INPUT_TYPES(s):
return {"required": { "model": ("MODEL",),
"lora_name": (folder_paths.get_filename_list("intristic_loras"), ),
"lora_name": (folder_paths.get_filename_list("intrinsic_loras"), ),
"task": (
[
'depth map',
@@ -81,7 +81,7 @@ with this node pack.
#load lora
model_clone = model.clone()
lora_path = folder_paths.get_full_path("intristic_loras", lora_name)
lora_path = folder_paths.get_full_path("intrinsic_loras", lora_name)
lora = load_torch_file(lora_path, safe_load=True)
self.loaded_lora = (lora_path, lora)
+552
View File
@@ -0,0 +1,552 @@
import torch
import comfy.model_management
import comfy.utils
import folder_paths
import os
import logging
from tqdm import tqdm
import numpy as np
device = comfy.model_management.get_torch_device()
CLAMP_QUANTILE = 0.99
def extract_lora(diff, key, rank, algorithm, lora_type, lowrank_iters=7, adaptive_param=1.0, clamp_quantile=True):
"""
Extracts LoRA weights from a weight difference tensor using SVD.
"""
conv2d = (len(diff.shape) == 4)
kernel_size = None if not conv2d else diff.size()[2:4]
conv2d_3x3 = conv2d and kernel_size != (1, 1)
out_dim, in_dim = diff.size()[0:2]
if conv2d:
if conv2d_3x3:
diff = diff.flatten(start_dim=1)
else:
diff = diff.squeeze()
diff_float = diff.float()
if algorithm == "svd_lowrank":
U, S, V = torch.svd_lowrank(diff_float, q=min(rank, in_dim, out_dim), niter=lowrank_iters)
U = U @ torch.diag(S)
Vh = V.t()
else:
#torch.linalg.svdvals()
U, S, Vh = torch.linalg.svd(diff_float)
# Flexible rank selection logic like locon: https://github.com/KohakuBlueleaf/LyCORIS/blob/main/tools/extract_locon.py
if "adaptive" in lora_type:
if lora_type == "adaptive_ratio":
min_s = torch.max(S) * adaptive_param
lora_rank = torch.sum(S > min_s).item()
elif lora_type == "adaptive_energy":
energy = torch.cumsum(S**2, dim=0)
total_energy = torch.sum(S**2)
threshold = adaptive_param * total_energy # e.g., adaptive_param=0.95 for 95%
lora_rank = torch.sum(energy < threshold).item() + 1
elif lora_type == "adaptive_quantile":
s_cum = torch.cumsum(S, dim=0)
min_cum_sum = adaptive_param * torch.sum(S)
lora_rank = torch.sum(s_cum < min_cum_sum).item()
print(f"{key} Extracted LoRA rank: {lora_rank}")
else:
lora_rank = rank
lora_rank = max(1, lora_rank)
lora_rank = min(out_dim, in_dim, lora_rank)
U = U[:, :lora_rank]
S = S[:lora_rank]
U = U @ torch.diag(S)
Vh = Vh[:lora_rank, :]
if clamp_quantile:
dist = torch.cat([U.flatten(), Vh.flatten()])
if dist.numel() > 100_000:
# Sample 100,000 elements for quantile estimation
idx = torch.randperm(dist.numel(), device=dist.device)[:100_000]
dist_sample = dist[idx]
hi_val = torch.quantile(dist_sample, CLAMP_QUANTILE)
else:
hi_val = torch.quantile(dist, CLAMP_QUANTILE)
low_val = -hi_val
U = U.clamp(low_val, hi_val)
Vh = Vh.clamp(low_val, hi_val)
if conv2d:
U = U.reshape(out_dim, lora_rank, 1, 1)
Vh = Vh.reshape(lora_rank, in_dim, kernel_size[0], kernel_size[1])
return (U, Vh)
def calc_lora_model(model_diff, rank, prefix_model, prefix_lora, output_sd, lora_type, algorithm, lowrank_iters, out_dtype, bias_diff=False, adaptive_param=1.0, clamp_quantile=True):
comfy.model_management.load_models_gpu([model_diff], force_patch_weights=True)
model_diff.model.diffusion_model.cpu()
sd = model_diff.model_state_dict(filter_prefix=prefix_model)
del model_diff
comfy.model_management.soft_empty_cache()
for k, v in sd.items():
if isinstance(v, torch.Tensor):
sd[k] = v.cpu()
# Get total number of keys to process for progress bar
total_keys = len([k for k in sd if k.endswith(".weight") or (bias_diff and k.endswith(".bias"))])
# Create progress bar
progress_bar = tqdm(total=total_keys, desc=f"Extracting LoRA ({prefix_lora.strip('.')})")
comfy_pbar = comfy.utils.ProgressBar(total_keys)
for k in sd:
if k.endswith(".weight"):
weight_diff = sd[k]
if weight_diff.ndim == 5:
logging.info(f"Skipping 5D tensor for key {k}") #skip patch embed
progress_bar.update(1)
comfy_pbar.update(1)
continue
if lora_type != "full":
if weight_diff.ndim < 2:
if bias_diff:
output_sd["{}{}.diff".format(prefix_lora, k[len(prefix_model):-7])] = weight_diff.contiguous().to(out_dtype).cpu()
progress_bar.update(1)
comfy_pbar.update(1)
continue
try:
out = extract_lora(weight_diff.to(device), k, rank, algorithm, lora_type, lowrank_iters=lowrank_iters, adaptive_param=adaptive_param, clamp_quantile=clamp_quantile)
output_sd["{}{}.lora_up.weight".format(prefix_lora, k[len(prefix_model):-7])] = out[0].contiguous().to(out_dtype).cpu()
output_sd["{}{}.lora_down.weight".format(prefix_lora, k[len(prefix_model):-7])] = out[1].contiguous().to(out_dtype).cpu()
except Exception as e:
logging.warning(f"Could not generate lora weights for key {k}, error {e}")
else:
output_sd["{}{}.diff".format(prefix_lora, k[len(prefix_model):-7])] = weight_diff.contiguous().to(out_dtype).cpu()
progress_bar.update(1)
comfy_pbar.update(1)
elif bias_diff and k.endswith(".bias"):
output_sd["{}{}.diff_b".format(prefix_lora, k[len(prefix_model):-5])] = sd[k].contiguous().to(out_dtype).cpu()
progress_bar.update(1)
comfy_pbar.update(1)
progress_bar.close()
return output_sd
class LoraExtractKJ:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
@classmethod
def INPUT_TYPES(s):
return {"required":
{
"finetuned_model": ("MODEL",),
"original_model": ("MODEL",),
"filename_prefix": ("STRING", {"default": "loras/ComfyUI_extracted_lora"}),
"rank": ("INT", {"default": 8, "min": 1, "max": 4096, "step": 1}),
"lora_type": (["standard", "full", "adaptive_ratio", "adaptive_quantile", "adaptive_energy"],),
"algorithm": (["svd_linalg", "svd_lowrank"], {"default": "svd_linalg", "tooltip": "SVD algorithm to use, svd_lowrank is faster but less accurate."}),
"lowrank_iters": ("INT", {"default": 7, "min": 1, "max": 100, "step": 1, "tooltip": "The number of subspace iterations for lowrank SVD algorithm."}),
"output_dtype": (["fp16", "bf16", "fp32"], {"default": "fp16"}),
"bias_diff": ("BOOLEAN", {"default": True}),
"adaptive_param": ("FLOAT", {"default": 0.15, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "For ratio mode, this is the ratio of the maximum singular value. For quantile mode, this is the quantile of the singular values."}),
"clamp_quantile": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ()
FUNCTION = "save"
OUTPUT_NODE = True
CATEGORY = "KJNodes/lora"
def save(self, finetuned_model, original_model, filename_prefix, rank, lora_type, algorithm, lowrank_iters, output_dtype, bias_diff, adaptive_param, clamp_quantile):
if algorithm == "svd_lowrank" and lora_type != "standard":
raise ValueError("svd_lowrank algorithm is only supported for standard LoRA extraction.")
dtype = {"fp8_e4m3fn": torch.float8_e4m3fn, "bf16": torch.bfloat16, "fp16": torch.float16, "fp16_fast": torch.float16, "fp32": torch.float32}[output_dtype]
m = finetuned_model.clone()
kp = original_model.get_key_patches("diffusion_model.")
for k in kp:
m.add_patches({k: kp[k]}, - 1.0, 1.0)
model_diff = m
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir)
output_sd = {}
if model_diff is not None:
output_sd = calc_lora_model(model_diff, rank, "diffusion_model.", "diffusion_model.", output_sd, lora_type, algorithm, lowrank_iters, dtype, bias_diff=bias_diff, adaptive_param=adaptive_param, clamp_quantile=clamp_quantile)
if "adaptive" in lora_type:
rank_str = f"{lora_type}_{adaptive_param:.2f}"
else:
rank_str = rank
output_checkpoint = f"{filename}_rank_{rank_str}_{output_dtype}_{counter:05}_.safetensors"
output_checkpoint = os.path.join(full_output_folder, output_checkpoint)
comfy.utils.save_torch_file(output_sd, output_checkpoint, metadata=None)
return {}
NODE_CLASS_MAPPINGS = {
"LoraExtractKJ": LoraExtractKJ
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LoraExtractKJ": "LoraExtractKJ"
}
class LoraReduceRank:
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
@classmethod
def INPUT_TYPES(s):
return {"required":
{
"lora_name": (folder_paths.get_filename_list("loras"), {"tooltip": "The name of the LoRA."}),
"new_rank": ("INT", {"default": 8, "min": 1, "max": 4096, "step": 1, "tooltip": "The new rank to resize the LoRA. Acts as max rank when using dynamic_method."}),
"dynamic_method": (["disabled", "sv_ratio", "sv_cumulative", "sv_fro"], {"default": "disabled", "tooltip": "Method to use for dynamically determining new alphas and dims"}),
"dynamic_param": ("FLOAT", {"default": 0.2, "min": 0.0, "max": 1.0, "step": 0.01, "tooltip": "Method to use for dynamically determining new alphas and dims"}),
"output_dtype": (["match_original", "fp16", "bf16", "fp32"], {"default": "match_original", "tooltip": "Data type to save the LoRA as."}),
"verbose": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ()
FUNCTION = "save"
OUTPUT_NODE = True
EXPERIMENTAL = True
DESCRIPTION = "Resize a LoRA model by reducing it's rank. Based on kohya's sd-scripts: https://github.com/kohya-ss/sd-scripts/blob/main/networks/resize_lora.py"
CATEGORY = "KJNodes/lora"
def save(self, lora_name, new_rank, output_dtype, dynamic_method, dynamic_param, verbose):
lora_path = folder_paths.get_full_path("loras", lora_name)
lora_sd, metadata = comfy.utils.load_torch_file(lora_path, return_metadata=True)
if output_dtype == "fp16":
save_dtype = torch.float16
elif output_dtype == "bf16":
save_dtype = torch.bfloat16
elif output_dtype == "fp32":
save_dtype = torch.float32
elif output_dtype == "match_original":
first_weight_key = next(k for k in lora_sd if k.endswith(".weight") and isinstance(lora_sd[k], torch.Tensor))
save_dtype = lora_sd[first_weight_key].dtype
new_lora_sd = {}
for k, v in lora_sd.items():
new_lora_sd[k.replace(".default", "")] = v
del lora_sd
print("Resizing Lora...")
output_sd, old_dim, new_alpha, rank_list = resize_lora_model(new_lora_sd, new_rank, save_dtype, device, dynamic_method, dynamic_param, verbose)
# update metadata
if metadata is None:
metadata = {}
comment = metadata.get("ss_training_comment", "")
if dynamic_method == "disabled":
metadata["ss_training_comment"] = f"dimension is resized from {old_dim} to {new_rank}; {comment}"
metadata["ss_network_dim"] = str(new_rank)
metadata["ss_network_alpha"] = str(new_alpha)
else:
metadata["ss_training_comment"] = f"Dynamic resize with {dynamic_method}: {dynamic_param} from {old_dim}; {comment}"
metadata["ss_network_dim"] = "Dynamic"
metadata["ss_network_alpha"] = "Dynamic"
# cast to save_dtype before calculating hashes
for key in list(output_sd.keys()):
value = output_sd[key]
if type(value) == torch.Tensor and value.dtype.is_floating_point and value.dtype != save_dtype:
output_sd[key] = value.to(save_dtype)
output_filename_prefix = "loras/" + lora_name
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(output_filename_prefix, self.output_dir)
output_dtype_str = f"_{output_dtype}" if output_dtype != "match_original" else ""
average_rank = str(int(np.mean(rank_list)))
rank_str = new_rank if dynamic_method == "disabled" else f"dynamic_{average_rank}"
output_checkpoint = f"{filename.replace('.safetensors', '')}_resized_from_{old_dim}_to_{rank_str}{output_dtype_str}_{counter:05}_.safetensors"
output_checkpoint = os.path.join(full_output_folder, output_checkpoint)
print(f"Saving resized LoRA to {output_checkpoint}")
comfy.utils.save_torch_file(output_sd, output_checkpoint, metadata=metadata)
return {}
NODE_CLASS_MAPPINGS = {
"LoraExtractKJ": LoraExtractKJ
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LoraExtractKJ": "LoraExtractKJ"
}
# Convert LoRA to different rank approximation (should only be used to go to lower rank)
# This code is based off the extract_lora_from_models.py file which is based on https://github.com/cloneofsimo/lora/blob/develop/lora_diffusion/cli_svd.py
# Thanks to cloneofsimo
# This version is based on
# https://github.com/kohya-ss/sd-scripts/blob/main/networks/resize_lora.py
MIN_SV = 1e-6
LORA_DOWN_UP_FORMATS = [
("lora_down", "lora_up"), # sd-scripts LoRA
("lora_A", "lora_B"), # PEFT LoRA
("down", "up"), # ControlLoRA
]
# Indexing functions
def index_sv_cumulative(S, target):
original_sum = float(torch.sum(S))
cumulative_sums = torch.cumsum(S, dim=0) / original_sum
index = int(torch.searchsorted(cumulative_sums, target)) + 1
index = max(1, min(index, len(S) - 1))
return index
def index_sv_fro(S, target):
S_squared = S.pow(2)
S_fro_sq = float(torch.sum(S_squared))
sum_S_squared = torch.cumsum(S_squared, dim=0) / S_fro_sq
index = int(torch.searchsorted(sum_S_squared, target**2)) + 1
index = max(1, min(index, len(S) - 1))
return index
def index_sv_ratio(S, target):
max_sv = S[0]
min_sv = max_sv / target
index = int(torch.sum(S > min_sv).item())
index = max(1, min(index, len(S) - 1))
return index
# Modified from Kohaku-blueleaf's extract/merge functions
def extract_conv(weight, lora_rank, dynamic_method, dynamic_param, device, scale=1):
out_size, in_size, kernel_size, _ = weight.size()
if weight.dtype != torch.float32:
weight = weight.to(torch.float32)
U, S, Vh = torch.linalg.svd(weight.reshape(out_size, -1).to(device))
param_dict = rank_resize(S, lora_rank, dynamic_method, dynamic_param, scale)
lora_rank = param_dict["new_rank"]
U = U[:, :lora_rank]
S = S[:lora_rank]
U = U @ torch.diag(S)
Vh = Vh[:lora_rank, :]
param_dict["lora_down"] = Vh.reshape(lora_rank, in_size, kernel_size, kernel_size).cpu()
param_dict["lora_up"] = U.reshape(out_size, lora_rank, 1, 1).cpu()
del U, S, Vh, weight
return param_dict
def extract_linear(weight, lora_rank, dynamic_method, dynamic_param, device, scale=1):
out_size, in_size = weight.size()
if weight.dtype != torch.float32:
weight = weight.to(torch.float32)
U, S, Vh = torch.linalg.svd(weight.to(device))
param_dict = rank_resize(S, lora_rank, dynamic_method, dynamic_param, scale)
lora_rank = param_dict["new_rank"]
U = U[:, :lora_rank]
S = S[:lora_rank]
U = U @ torch.diag(S)
Vh = Vh[:lora_rank, :]
param_dict["lora_down"] = Vh.reshape(lora_rank, in_size).cpu()
param_dict["lora_up"] = U.reshape(out_size, lora_rank).cpu()
del U, S, Vh, weight
return param_dict
def merge_conv(lora_down, lora_up, device):
in_rank, in_size, kernel_size, k_ = lora_down.shape
out_size, out_rank, _, _ = lora_up.shape
assert in_rank == out_rank and kernel_size == k_, f"rank {in_rank} {out_rank} or kernel {kernel_size} {k_} mismatch"
lora_down = lora_down.to(device)
lora_up = lora_up.to(device)
merged = lora_up.reshape(out_size, -1) @ lora_down.reshape(in_rank, -1)
weight = merged.reshape(out_size, in_size, kernel_size, kernel_size)
del lora_up, lora_down
return weight
def merge_linear(lora_down, lora_up, device):
in_rank, in_size = lora_down.shape
out_size, out_rank = lora_up.shape
assert in_rank == out_rank, f"rank {in_rank} {out_rank} mismatch"
lora_down = lora_down.to(device)
lora_up = lora_up.to(device)
weight = lora_up @ lora_down
del lora_up, lora_down
return weight
# Calculate new rank
def rank_resize(S, rank, dynamic_method, dynamic_param, scale=1):
param_dict = {}
if dynamic_method == "sv_ratio":
# Calculate new dim and alpha based off ratio
new_rank = index_sv_ratio(S, dynamic_param) + 1
new_alpha = float(scale * new_rank)
elif dynamic_method == "sv_cumulative":
# Calculate new dim and alpha based off cumulative sum
new_rank = index_sv_cumulative(S, dynamic_param) + 1
new_alpha = float(scale * new_rank)
elif dynamic_method == "sv_fro":
# Calculate new dim and alpha based off sqrt sum of squares
new_rank = index_sv_fro(S, dynamic_param) + 1
new_alpha = float(scale * new_rank)
else:
new_rank = rank
new_alpha = float(scale * new_rank)
if S[0] <= MIN_SV: # Zero matrix, set dim to 1
new_rank = 1
new_alpha = float(scale * new_rank)
elif new_rank > rank: # cap max rank at rank
new_rank = rank
new_alpha = float(scale * new_rank)
# Calculate resize info
s_sum = torch.sum(torch.abs(S))
s_rank = torch.sum(torch.abs(S[:new_rank]))
S_squared = S.pow(2)
s_fro = torch.sqrt(torch.sum(S_squared))
s_red_fro = torch.sqrt(torch.sum(S_squared[:new_rank]))
fro_percent = float(s_red_fro / s_fro)
param_dict["new_rank"] = new_rank
param_dict["new_alpha"] = new_alpha
param_dict["sum_retained"] = (s_rank) / s_sum
param_dict["fro_retained"] = fro_percent
param_dict["max_ratio"] = S[0] / S[new_rank - 1]
return param_dict
def resize_lora_model(lora_sd, new_rank, save_dtype, device, dynamic_method, dynamic_param, verbose):
max_old_rank = None
new_alpha = None
verbose_str = "\n"
fro_list = []
rank_list = []
if dynamic_method:
print(f"Dynamically determining new alphas and dims based off {dynamic_method}: {dynamic_param}, max rank is {new_rank}")
lora_down_weight = None
lora_up_weight = None
o_lora_sd = lora_sd.copy()
block_down_name = None
block_up_name = None
total_keys = len([k for k in lora_sd if k.endswith(".weight")])
pbar = comfy.utils.ProgressBar(total_keys)
for key, value in tqdm(lora_sd.items()):
key_parts = key.split(".")
block_down_name = None
for _format in LORA_DOWN_UP_FORMATS:
# Currently we only match lora_down_name in the last two parts of key
# because ("down", "up") are general words and may appear in block_down_name
if len(key_parts) >= 2 and _format[0] == key_parts[-2]:
block_down_name = ".".join(key_parts[:-2])
lora_down_name = "." + _format[0]
lora_up_name = "." + _format[1]
weight_name = "." + key_parts[-1]
break
if len(key_parts) >= 1 and _format[0] == key_parts[-1]:
block_down_name = ".".join(key_parts[:-1])
lora_down_name = "." + _format[0]
lora_up_name = "." + _format[1]
weight_name = ""
break
if block_down_name is None:
# This parameter is not lora_down
continue
# Now weight_name can be ".weight" or ""
# Find corresponding lora_up and alpha
block_up_name = block_down_name
lora_down_weight = value
lora_up_weight = lora_sd.get(block_up_name + lora_up_name + weight_name, None)
lora_alpha = lora_sd.get(block_down_name + ".alpha", None)
weights_loaded = lora_down_weight is not None and lora_up_weight is not None
if weights_loaded:
conv2d = len(lora_down_weight.size()) == 4
old_rank = lora_down_weight.size()[0]
max_old_rank = max(max_old_rank or 0, old_rank)
if lora_alpha is None:
scale = 1.0
else:
scale = lora_alpha / old_rank
if conv2d:
full_weight_matrix = merge_conv(lora_down_weight, lora_up_weight, device)
param_dict = extract_conv(full_weight_matrix, new_rank, dynamic_method, dynamic_param, device, scale)
else:
full_weight_matrix = merge_linear(lora_down_weight, lora_up_weight, device)
param_dict = extract_linear(full_weight_matrix, new_rank, dynamic_method, dynamic_param, device, scale)
if verbose:
max_ratio = param_dict["max_ratio"]
sum_retained = param_dict["sum_retained"]
fro_retained = param_dict["fro_retained"]
if not np.isnan(fro_retained):
fro_list.append(float(fro_retained))
verbose_str += f"{block_down_name:75} | "
verbose_str += f"sum(S) retained: {sum_retained:.1%}, fro retained: {fro_retained:.1%}, max(S) ratio: {max_ratio:0.1f}"
print(verbose_str)
if verbose and dynamic_method:
verbose_str += f", dynamic | dim: {param_dict['new_rank']}, alpha: {param_dict['new_alpha']}\n"
else:
verbose_str += "\n"
new_alpha = param_dict["new_alpha"]
o_lora_sd[block_down_name + lora_down_name + weight_name] = param_dict["lora_down"].to(save_dtype).contiguous()
o_lora_sd[block_up_name + lora_up_name + weight_name] = param_dict["lora_up"].to(save_dtype).contiguous()
o_lora_sd[block_down_name + ".alpha"] = torch.tensor(param_dict["new_alpha"]).to(save_dtype)
block_down_name = None
block_up_name = None
lora_down_weight = None
lora_up_weight = None
weights_loaded = False
rank_list.append(param_dict["new_rank"])
del param_dict
pbar.update(1)
if verbose:
print(verbose_str)
print(f"Average Frobenius norm retention: {np.mean(fro_list):.2%} | std: {np.std(fro_list):0.3f}")
return o_lora_sd, max_old_rank, new_alpha, rank_list
+336 -75
View File
@@ -4,13 +4,12 @@ from torchvision.transforms import functional as TF
from PIL import Image, ImageDraw, ImageFilter, ImageFont
import scipy.ndimage
import numpy as np
import matplotlib.pyplot as plt
from contextlib import nullcontext
import os
import model_management
from comfy import model_management
from comfy.utils import ProgressBar
from comfy.utils import common_upscale
from nodes import MAX_RESOLUTION
import folder_paths
@@ -31,73 +30,155 @@ class BatchCLIPSeg:
{
"images": ("IMAGE",),
"text": ("STRING", {"multiline": False}),
"threshold": ("FLOAT", {"default": 0.1,"min": 0.0, "max": 10.0, "step": 0.001}),
"threshold": ("FLOAT", {"default": 0.5,"min": 0.0, "max": 10.0, "step": 0.001}),
"binary_mask": ("BOOLEAN", {"default": True}),
"combine_mask": ("BOOLEAN", {"default": False}),
"use_cuda": ("BOOLEAN", {"default": True}),
},
"optional":
{
"blur_sigma": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 100.0, "step": 0.1}),
"opt_model": ("CLIPSEGMODEL", ),
"prev_mask": ("MASK", {"default": None}),
"image_bg_level": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
"invert": ("BOOLEAN", {"default": False}),
}
}
CATEGORY = "KJNodes/masking"
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("Mask",)
RETURN_TYPES = ("MASK", "IMAGE", )
RETURN_NAMES = ("Mask", "Image", )
FUNCTION = "segment_image"
DESCRIPTION = """
Segments an image or batch of images using CLIPSeg.
"""
def segment_image(self, images, text, threshold, binary_mask, combine_mask, use_cuda):
def segment_image(self, images, text, threshold, binary_mask, combine_mask, use_cuda, blur_sigma=0.0, opt_model=None, prev_mask=None, invert= False, image_bg_level=0.5):
from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation
out = []
height, width, _ = images[0].shape
if use_cuda and torch.cuda.is_available():
device = torch.device("cuda")
else:
import torchvision.transforms as transforms
offload_device = model_management.unet_offload_device()
device = model_management.get_torch_device()
if not use_cuda:
device = torch.device("cpu")
dtype = model_management.unet_dtype()
model = CLIPSegForImageSegmentation.from_pretrained("CIDAS/clipseg-rd64-refined")
model.to(dtype)
model.to(device)
if opt_model is None:
checkpoint_path = os.path.join(folder_paths.models_dir,'clip_seg', 'clipseg-rd64-refined-fp16')
if not hasattr(self, "model"):
try:
if not os.path.exists(checkpoint_path):
from huggingface_hub import snapshot_download
snapshot_download(repo_id="Kijai/clipseg-rd64-refined-fp16", local_dir=checkpoint_path, local_dir_use_symlinks=False)
self.model = CLIPSegForImageSegmentation.from_pretrained(checkpoint_path)
except:
checkpoint_path = "CIDAS/clipseg-rd64-refined"
self.model = CLIPSegForImageSegmentation.from_pretrained(checkpoint_path)
processor = CLIPSegProcessor.from_pretrained(checkpoint_path)
else:
self.model = opt_model['model']
processor = opt_model['processor']
self.model.to(dtype).to(device)
B, H, W, C = images.shape
images = images.to(device)
processor = CLIPSegProcessor.from_pretrained("CIDAS/clipseg-rd64-refined")
pbar = ProgressBar(images.shape[0])
autocast_condition = (dtype != torch.float32) and not model_management.is_device_mps(device)
with torch.autocast(model_management.get_autocast_device(device), dtype=dtype) if autocast_condition else nullcontext():
for image in images:
image = (image* 255).type(torch.uint8)
prompt = text
input_prc = processor(text=prompt, images=image, return_tensors="pt")
# Move the processed input to the device
for key in input_prc:
input_prc[key] = input_prc[key].to(device)
outputs = model(**input_prc)
tensor = torch.sigmoid(outputs[0])
tensor_thresholded = torch.where(tensor > threshold, tensor, torch.tensor(0, dtype=torch.float))
tensor_normalized = (tensor_thresholded - tensor_thresholded.min()) / (tensor_thresholded.max() - tensor_thresholded.min())
tensor = tensor_normalized
# Resize the mask
if len(tensor.shape) == 3:
tensor = tensor.unsqueeze(0)
resized_tensor = F.interpolate(tensor, size=(height, width), mode='nearest')
PIL_images = [Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) for image in images ]
prompt = [text] * len(images)
input_prc = processor(text=prompt, images=PIL_images, return_tensors="pt")
# Remove the extra dimensions
resized_tensor = resized_tensor[0, 0, :, :]
pbar.update(1)
out.append(resized_tensor)
results = torch.stack(out).cpu().float()
for key in input_prc:
input_prc[key] = input_prc[key].to(device)
outputs = self.model(**input_prc)
mask_tensor = torch.sigmoid(outputs.logits)
mask_tensor = (mask_tensor - mask_tensor.min()) / (mask_tensor.max() - mask_tensor.min())
mask_tensor = torch.where(mask_tensor > (threshold), mask_tensor, torch.tensor(0, dtype=torch.float))
print(mask_tensor.shape)
if len(mask_tensor.shape) == 2:
mask_tensor = mask_tensor.unsqueeze(0)
mask_tensor = F.interpolate(mask_tensor.unsqueeze(1), size=(H, W), mode='nearest')
mask_tensor = mask_tensor.squeeze(1)
self.model.to(offload_device)
if combine_mask:
combined_results = torch.max(results, dim=0)[0]
results = combined_results.unsqueeze(0).repeat(len(images),1,1)
if binary_mask:
results = results.round()
return results,
mask_tensor = (mask_tensor > 0).float()
if blur_sigma > 0:
kernel_size = int(6 * int(blur_sigma) + 1)
blur = transforms.GaussianBlur(kernel_size=(kernel_size, kernel_size), sigma=(blur_sigma, blur_sigma))
mask_tensor = blur(mask_tensor)
if combine_mask:
mask_tensor = torch.max(mask_tensor, dim=0)[0]
mask_tensor = mask_tensor.unsqueeze(0).repeat(len(images),1,1)
del outputs
model_management.soft_empty_cache()
if prev_mask is not None:
if prev_mask.shape != mask_tensor.shape:
prev_mask = F.interpolate(prev_mask.unsqueeze(1), size=(H, W), mode='nearest')
mask_tensor = mask_tensor + prev_mask.to(device)
torch.clamp(mask_tensor, min=0.0, max=1.0)
if invert:
mask_tensor = 1 - mask_tensor
image_tensor = images * mask_tensor.unsqueeze(-1) + (1 - mask_tensor.unsqueeze(-1)) * image_bg_level
image_tensor = torch.clamp(image_tensor, min=0.0, max=1.0).cpu().float()
mask_tensor = mask_tensor.cpu().float()
return mask_tensor, image_tensor,
class DownloadAndLoadCLIPSeg:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {"required":
{
"model": (
[ 'Kijai/clipseg-rd64-refined-fp16',
'CIDAS/clipseg-rd64-refined',
],
),
},
}
CATEGORY = "KJNodes/masking"
RETURN_TYPES = ("CLIPSEGMODEL",)
RETURN_NAMES = ("clipseg_model",)
FUNCTION = "segment_image"
DESCRIPTION = """
Downloads and loads CLIPSeg model with huggingface_hub,
to ComfyUI/models/clip_seg
"""
def segment_image(self, model):
from transformers import CLIPSegProcessor, CLIPSegForImageSegmentation
checkpoint_path = os.path.join(folder_paths.models_dir,'clip_seg', os.path.basename(model))
if not hasattr(self, "model"):
if not os.path.exists(checkpoint_path):
from huggingface_hub import snapshot_download
snapshot_download(repo_id=model, local_dir=checkpoint_path, local_dir_use_symlinks=False)
self.model = CLIPSegForImageSegmentation.from_pretrained(checkpoint_path)
processor = CLIPSegProcessor.from_pretrained(checkpoint_path)
clipseg_model = {}
clipseg_model['model'] = self.model
clipseg_model['processor'] = processor
return clipseg_model,
class CreateTextMask:
@@ -277,7 +358,7 @@ class CreateFluidMask:
return {
"required": {
"invert": ("BOOLEAN", {"default": False}),
"frames": ("INT", {"default": 0,"min": 0, "max": 255, "step": 1}),
"frames": ("INT", {"default": 1,"min": 1, "max": 4096, "step": 1}),
"width": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
"height": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
"inflow_count": ("INT", {"default": 3,"min": 0, "max": 255, "step": 1}),
@@ -290,7 +371,10 @@ class CreateFluidMask:
#using code from https://github.com/GregTJ/stable-fluids
def createfluidmask(self, frames, width, height, invert, inflow_count, inflow_velocity, inflow_radius, inflow_padding, inflow_duration):
from ..utility.fluid import Fluid
from scipy.spatial import erf
try:
from scipy.special import erf
except:
from scipy.spatial import erf
out = []
masks = []
RESOLUTION = width, height
@@ -444,7 +528,7 @@ class CreateFadeMask:
return {
"required": {
"invert": ("BOOLEAN", {"default": False}),
"frames": ("INT", {"default": 2,"min": 2, "max": 255, "step": 1}),
"frames": ("INT", {"default": 2,"min": 2, "max": 10000, "step": 1}),
"width": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
"height": ("INT", {"default": 256,"min": 16, "max": 4096, "step": 1}),
"interpolation": (["linear", "ease_in", "ease_out", "ease_in_out"],),
@@ -531,10 +615,10 @@ and interpolating from that to fully black at the 16th frame.
"required": {
"points_string": ("STRING", {"default": "0:(0.0),\n7:(1.0),\n15:(0.0)\n", "multiline": True}),
"invert": ("BOOLEAN", {"default": False}),
"frames": ("INT", {"default": 16,"min": 2, "max": 255, "step": 1}),
"frames": ("INT", {"default": 16,"min": 2, "max": 10000, "step": 1}),
"width": ("INT", {"default": 512,"min": 1, "max": 4096, "step": 1}),
"height": ("INT", {"default": 512,"min": 1, "max": 4096, "step": 1}),
"interpolation": (["linear", "ease_in", "ease_out", "ease_in_out"],),
"interpolation": (["linear", "ease_in", "ease_out", "ease_in_out", "none", "default_to_black"],),
},
}
@@ -558,7 +642,7 @@ and interpolating from that to fully black at the 16th frame.
points.append((frame, color))
# Check if the last frame is already in the points
if len(points) == 0 or points[-1][0] != frames - 1:
if (interpolation != "default_to_black") and (len(points) == 0 or points[-1][0] != frames - 1):
# If not, add it with the color of the last specified frame
points.append((frames - 1, points[-1][1] if points else 0))
@@ -578,17 +662,39 @@ and interpolating from that to fully black at the 16th frame.
# Interpolate between the previous point and the next point
prev_point = next_point - 1
t = (i - points[prev_point][0]) / (points[next_point][0] - points[prev_point][0])
if interpolation == "ease_in":
t = ease_in(t)
elif interpolation == "ease_out":
t = ease_out(t)
elif interpolation == "ease_in_out":
t = ease_in_out(t)
elif interpolation == "linear":
pass # No need to modify `t` for linear interpolation
color = points[prev_point][1] - t * (points[prev_point][1] - points[next_point][1])
if interpolation == "none":
exact_match = False
for p in points:
if p[0] == i: # Exact frame match
color = p[1]
exact_match = True
break
if not exact_match:
color = points[prev_point][1]
elif interpolation == "default_to_black":
exact_match = False
for p in points:
if p[0] == i: # Exact frame match
color = p[1]
exact_match = True
break
if not exact_match:
color = 0
else:
t = (i - points[prev_point][0]) / (points[next_point][0] - points[prev_point][0])
if interpolation == "ease_in":
t = ease_in(t)
elif interpolation == "ease_out":
t = ease_out(t)
elif interpolation == "ease_in_out":
t = ease_in_out(t)
elif interpolation == "linear":
pass # No need to modify `t` for linear interpolation
color = points[prev_point][1] - t * (points[prev_point][1] - points[next_point][1])
color = np.clip(color, 0, 255)
image = np.full((height, width), color, dtype=np.float32)
image_batch[i] = image
@@ -624,6 +730,7 @@ class CreateMagicMask:
def createmagicmask(self, frames, transitions, depth, distortion, seed, frame_width, frame_height):
from ..utility.magictex import coordinate_grid, random_transform, magic
import matplotlib.pyplot as plt
rng = np.random.default_rng(seed)
out = []
coords = coordinate_grid((frame_width, frame_height))
@@ -903,7 +1010,7 @@ class GrowMaskWithBlur:
previous_output = None
current_expand = expand
for m in growmask:
output = m.numpy()
output = m.numpy().astype(np.float32)
for _ in range(abs(round(current_expand))):
if current_expand < 0:
output = scipy.ndimage.grey_erosion(output, footprint=kernel)
@@ -1095,14 +1202,17 @@ Rounds the mask or batch of masks to a binary mask.
return (mask,)
class ResizeMask:
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"mask": ("MASK",),
"width": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, "display": "number" }),
"height": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 8, "display": "number" }),
"width": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, "display": "number" }),
"height": ("INT", { "default": 512, "min": 0, "max": MAX_RESOLUTION, "step": 1, "display": "number" }),
"keep_proportions": ("BOOLEAN", { "default": False }),
"upscale_method": (s.upscale_methods,),
"crop": (["disabled","center"],),
}
}
@@ -1114,20 +1224,21 @@ class ResizeMask:
Resizes the mask or batch of masks to the specified width and height.
"""
def resize(self, mask, width, height, keep_proportions):
def resize(self, mask, width, height, keep_proportions, upscale_method,crop):
if keep_proportions:
_, oh, ow, _ = mask.shape
_, oh, ow = mask.shape
width = ow if width == 0 else width
height = oh if height == 0 else height
ratio = min(width / ow, height / oh)
width = round(ow*ratio)
height = round(oh*ratio)
outputs = mask.unsqueeze(0) # Add an extra dimension for batch size
outputs = F.interpolate(outputs, size=(height, width), mode="nearest")
outputs = outputs.squeeze(0) # Remove the extra dimension after interpolation
return(outputs, outputs.shape[2], outputs.shape[1],)
if upscale_method == "lanczos":
out_mask = common_upscale(mask.unsqueeze(1).repeat(1, 3, 1, 1), width, height, upscale_method, crop=crop).movedim(1,-1)[:, :, :, 0]
else:
out_mask = common_upscale(mask.unsqueeze(1), width, height, upscale_method, crop=crop).squeeze(1)
return(out_mask, out_mask.shape[2], out_mask.shape[1],)
class RemapMaskRange:
@classmethod
@@ -1163,4 +1274,154 @@ Sets new min and max values for the mask.
# Clamp the values to ensure they are within [0.0, 1.0]
scaled_mask = torch.clamp(scaled_mask, min=0.0, max=1.0)
return (scaled_mask, )
return (scaled_mask, )
def get_mask_polygon(self, mask_np):
import cv2
"""Helper function to get polygon points from mask"""
# Find contours
contours, _ = cv2.findContours(mask_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return None
# Get the largest contour
largest_contour = max(contours, key=cv2.contourArea)
# Approximate polygon
epsilon = 0.02 * cv2.arcLength(largest_contour, True)
polygon = cv2.approxPolyDP(largest_contour, epsilon, True)
return polygon.squeeze()
import cv2
class SeparateMasks:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK", ),
"size_threshold_width" : ("INT", {"default": 256, "min": 0.0, "max": 4096, "step": 1}),
"size_threshold_height" : ("INT", {"default": 256, "min": 0.0, "max": 4096, "step": 1}),
"mode": (["convex_polygons", "area", "box"],),
"max_poly_points": ("INT", {"default": 8, "min": 3, "max": 32, "step": 1}),
},
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("mask",)
FUNCTION = "separate"
CATEGORY = "KJNodes/masking"
OUTPUT_NODE = True
DESCRIPTION = "Separates a mask into multiple masks based on the size of the connected components."
def polygon_to_mask(self, polygon, shape):
mask = np.zeros((shape[0], shape[1]), dtype=np.uint8) # Fixed shape handling
if len(polygon.shape) == 2: # Check if polygon points are valid
polygon = polygon.astype(np.int32)
cv2.fillPoly(mask, [polygon], 1)
return mask
def get_mask_polygon(self, mask_np, max_points):
contours, _ = cv2.findContours(mask_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return None
largest_contour = max(contours, key=cv2.contourArea)
hull = cv2.convexHull(largest_contour)
# Initialize with smaller epsilon for more points
perimeter = cv2.arcLength(hull, True)
epsilon = perimeter * 0.01 # Start smaller
min_eps = perimeter * 0.001 # Much smaller minimum
max_eps = perimeter * 0.2 # Smaller maximum
best_approx = None
best_diff = float('inf')
max_iterations = 20
#print(f"Target points: {max_points}, Perimeter: {perimeter}")
for i in range(max_iterations):
curr_eps = (min_eps + max_eps) / 2
approx = cv2.approxPolyDP(hull, curr_eps, True)
points_diff = len(approx) - max_points
#print(f"Iteration {i}: points={len(approx)}, eps={curr_eps:.4f}")
if abs(points_diff) < best_diff:
best_approx = approx
best_diff = abs(points_diff)
if len(approx) > max_points:
min_eps = curr_eps * 1.1 # More gradual adjustment
elif len(approx) < max_points:
max_eps = curr_eps * 0.9 # More gradual adjustment
else:
return approx.squeeze()
if abs(max_eps - min_eps) < perimeter * 0.0001: # Relative tolerance
break
# If we didn't find exact match, return best approximation
return best_approx.squeeze() if best_approx is not None else hull.squeeze()
def separate(self, mask: torch.Tensor, size_threshold_width: int, size_threshold_height: int, max_poly_points: int, mode: str):
from scipy.ndimage import label, center_of_mass
import numpy as np
B, H, W = mask.shape
separated = []
mask = mask.round()
for b in range(B):
mask_np = mask[b].cpu().numpy().astype(np.uint8)
structure = np.ones((3, 3), dtype=np.int8)
labeled, ncomponents = label(mask_np, structure=structure)
pbar = ProgressBar(ncomponents)
for component in range(1, ncomponents + 1):
component_mask_np = (labeled == component).astype(np.uint8)
rows = np.any(component_mask_np, axis=1)
cols = np.any(component_mask_np, axis=0)
y_min, y_max = np.where(rows)[0][[0, -1]]
x_min, x_max = np.where(cols)[0][[0, -1]]
width = x_max - x_min + 1
height = y_max - y_min + 1
centroid_x = (x_min + x_max) / 2 # Calculate x centroid
print(f"Component {component}: width={width}, height={height}, x_pos={centroid_x}")
if width >= size_threshold_width and height >= size_threshold_height:
if mode == "convex_polygons":
polygon = self.get_mask_polygon(component_mask_np, max_poly_points)
if polygon is not None:
poly_mask = self.polygon_to_mask(polygon, (H, W))
poly_mask = torch.tensor(poly_mask, device=mask.device)
separated.append((centroid_x, poly_mask))
elif mode == "box":
# Create bounding box mask
box_mask = np.zeros((H, W), dtype=np.uint8)
box_mask[y_min:y_max+1, x_min:x_max+1] = 1
box_mask = torch.tensor(box_mask, device=mask.device)
separated.append((centroid_x, box_mask))
else:
area_mask = torch.tensor(component_mask_np, device=mask.device)
separated.append((centroid_x, area_mask))
pbar.update(1)
if len(separated) > 0:
# Sort by x position and extract only the masks
separated.sort(key=lambda x: x[0])
separated = [x[1] for x in separated]
out_masks = torch.stack(separated, dim=0)
return out_masks,
else:
return torch.empty((1, 64, 64), device=mask.device),
File diff suppressed because it is too large Load Diff
+1226 -233
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
[project]
name = "comfyui-kjnodes"
description = "Various quality of life -nodes for ComfyUI, mostly just visual stuff to improve usability."
version = "1.1.4"
license = {file = "LICENSE"}
dependencies = ["librosa", "numpy", "pillow>=10.3.0", "scipy", "color-matcher", "matplotlib", "huggingface_hub"]
[project.urls]
Repository = "https://github.com/kijai/ComfyUI-KJNodes"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
PublisherId = "kijai"
DisplayName = "ComfyUI-KJNodes"
Icon = "https://avatars.githubusercontent.com/u/40791699"
+3 -2
View File
@@ -1,6 +1,7 @@
librosa
numpy
pillow>=10.3.0
scipy
color-matcher
matplotlib
huggingface_hub
mss
opencv-python
+3 -1
View File
@@ -47,7 +47,9 @@ app.registerExtension({
)
if (pythongossFeed) {
console.warn("KJNodes - Overriding pysssss.FaviconStatus")
app.extensions = app.extensions.filter(item => item !== pythongossFeed);
pythongossFeed.setup = function() {
console.warn("Disabled by KJNodes")
};
}
},
});
+48 -53
View File
@@ -48,105 +48,100 @@ app.registerExtension({
}
},
async setup(app) {
const onChange = (value) => {
if (value) {
const valuesToAddToIn = ["GetNode"];
const valuesToAddToOut = ["SetNode"];
const updateSlots = (value) => {
const valuesToAddToIn = ["GetNode"];
const valuesToAddToOut = ["SetNode"];
// Remove entries if they exist
for (const arr of Object.values(LiteGraph.slot_types_default_in)) {
for (const valueToAdd of valuesToAddToIn) {
const idx = arr.indexOf(valueToAdd);
if (idx !== -1) {
arr.splice(idx, 1);
}
}
}
for (const arr of Object.values(LiteGraph.slot_types_default_out)) {
for (const valueToAdd of valuesToAddToOut) {
const idx = arr.indexOf(valueToAdd);
if (idx !== -1) {
arr.splice(idx, 1);
}
}
}
if (value!="disabled") {
for (const arr of Object.values(LiteGraph.slot_types_default_in)) {
for (const valueToAdd of valuesToAddToIn) {
const idx = arr.indexOf(valueToAdd);
if (idx !== 0) {
if (idx !== -1) {
arr.splice(idx, 1);
}
arr.unshift(valueToAdd);
if (value === "top") {
arr.unshift(valueToAdd);
} else {
arr.push(valueToAdd);
}
}
}
for (const arr of Object.values(LiteGraph.slot_types_default_out)) {
for (const valueToAdd of valuesToAddToOut) {
const idx = arr.indexOf(valueToAdd);
if (idx !== 0) {
if (idx !== -1) {
arr.splice(idx, 1);
}
arr.unshift(valueToAdd);
if (value === "top") {
arr.unshift(valueToAdd);
} else {
arr.push(valueToAdd);
}
}
}
}
};
app.ui.settings.addSetting({
id: "🦛 KJNodes.SetGetMenu",
name: "🦛 KJNodes: Make Set/Get -nodes defaults (turn off and reload to disable)",
defaultValue: false,
type: "boolean",
options: (value) => [
{
value: true,
text: "On",
selected: value === true,
},
{
value: false,
text: "Off",
selected: value === false,
},
],
onChange: onChange,
id: "KJNodes.SetGetMenu",
name: "KJNodes: Make Set/Get -nodes defaults",
tooltip: 'Adds Set/Get nodes to the top or bottom of the list of available node suggestions.',
options: ['disabled', 'top', 'bottom'],
defaultValue: 'disabled',
type: "combo",
onChange: updateSlots,
});
app.ui.settings.addSetting({
id: "KJNodes.DisableMiddleClickDefault",
name: "🦛 KJNodes: Middle click default node adding",
id: "KJNodes.MiddleClickDefault",
name: "KJNodes: Middle click default node adding",
defaultValue: false,
type: "boolean",
options: (value) => [
{ value: true, text: "On", selected: value === true },
{ value: false, text: "Off", selected: value === false },
],
onChange: (value) => {
LiteGraph.middle_click_slot_add_default_node = value;
},
});
app.ui.settings.addSetting({
id: "KJNodes.nodeAutoColor",
name: "🦛 KJNodes: Automatically set node colors",
defaultValue: true,
name: "KJNodes: Automatically set node colors",
type: "boolean",
options: (value) => [
{ value: true, text: "On", selected: value === true },
{ value: false, text: "Off", selected: value === false },
],
defaultValue: true,
});
app.ui.settings.addSetting({
id: "KJNodes.helpPopup",
name: "🦛 KJNodes: Help popups",
name: "KJNodes: Help popups",
defaultValue: true,
type: "boolean",
options: (value) => [
{ value: true, text: "On", selected: value === true },
{ value: false, text: "Off", selected: value === false },
],
});
app.ui.settings.addSetting({
id: "KJNodes.disablePrefix",
name: "🦛 KJNodes: Disable automatic Set_ and Get_ prefix",
defaultValue: false,
name: "KJNodes: Disable automatic Set_ and Get_ prefix",
defaultValue: true,
type: "boolean",
options: (value) => [
{ value: true, text: "On", selected: value === true },
{ value: false, text: "Off", selected: value === false },
],
});
app.ui.settings.addSetting({
id: "KJNodes.browserStatus",
name: "🦛 KJNodes: 🟢 Stoplight browser status icon 🔴",
name: "KJNodes: 🟢 Stoplight browser status icon 🔴",
defaultValue: false,
type: "boolean",
options: (value) => [
{ value: true, text: "On", selected: value === true },
{ value: false, text: "Off", selected: value === false },
],
});
}
});
+95
View File
@@ -0,0 +1,95 @@
import { app } from '../../../scripts/app.js'
//from melmass
export function makeUUID() {
let dt = new Date().getTime()
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = ((dt + Math.random() * 16) % 16) | 0
dt = Math.floor(dt / 16)
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
})
return uuid
}
function chainCallback(object, property, callback) {
if (object == undefined) {
//This should not happen.
console.error("Tried to add callback to non-existant object")
return;
}
if (property in object) {
const callback_orig = object[property]
object[property] = function () {
const r = callback_orig.apply(this, arguments);
callback.apply(this, arguments);
return r
};
} else {
object[property] = callback;
}
}
app.registerExtension({
name: 'KJNodes.FastPreview',
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name === 'FastPreview') {
chainCallback(nodeType.prototype, "onNodeCreated", function () {
var element = document.createElement("div");
this.uuid = makeUUID()
element.id = `fast-preview-${this.uuid}`
this.previewWidget = this.addDOMWidget(nodeData.name, "FastPreviewWidget", element, {
serialize: false,
hideOnZoom: false,
});
this.previewer = new Previewer(this);
this.setSize([550, 550]);
this.resizable = false;
this.previewWidget.parentEl = document.createElement("div");
this.previewWidget.parentEl.className = "fast-preview";
this.previewWidget.parentEl.id = `fast-preview-${this.uuid}`
element.appendChild(this.previewWidget.parentEl);
chainCallback(this, "onExecuted", function (message) {
let bg_image = message["bg_image"];
this.properties.imgData = {
name: "bg_image",
base64: bg_image
};
this.previewer.refreshBackgroundImage(this);
});
}); // onAfterGraphConfigured
}//node created
} //before register
})//register
class Previewer {
constructor(context) {
this.node = context;
this.previousWidth = null;
this.previousHeight = null;
}
refreshBackgroundImage = () => {
const imgData = this.node?.properties?.imgData;
if (imgData?.base64) {
const base64String = imgData.base64;
const imageUrl = `data:${imgData.type};base64,${base64String}`;
const img = new Image();
img.src = imageUrl;
img.onload = () => {
const { width, height } = img;
if (width !== this.previousWidth || height !== this.previousHeight) {
this.node.setSize([width, height]);
this.previousWidth = width;
this.previousHeight = height;
}
this.node.previewWidget.element.style.backgroundImage = `url(${imageUrl})`;
};
}
};
}
+6 -5
View File
@@ -38,14 +38,14 @@ export const loadScript = (
})
}
loadScript('/kjweb_async/marked.min.js').catch((e) => {
loadScript('kjweb_async/marked.min.js').catch((e) => {
console.log(e)
})
loadScript('/kjweb_async/purify.min.js').catch((e) => {
loadScript('kjweb_async/purify.min.js').catch((e) => {
console.log(e)
})
const categories = ["KJNodes", "SUPIR", "VoiceCraft", "Marigold"];
const categories = ["KJNodes", "SUPIR", "VoiceCraft", "Marigold", "IC-Light", "WanVideoWrapper"];
app.registerExtension({
name: "KJNodes.HelpPopup",
async beforeRegisterNodeDef(nodeType, nodeData) {
@@ -257,12 +257,13 @@ const create_documentation_stylesheet = () => {
const scale = new DOMMatrix()
.scaleSelf(transform.a, transform.d);
const bcr = app.canvas.canvas.getBoundingClientRect()
const styleObject = {
transformOrigin: '0 0',
transform: scale,
left: `${transform.a + transform.e}px`,
top: `${transform.d + transform.f}px`,
left: `${transform.a + bcr.x + transform.e}px`,
top: `${transform.d + bcr.y + transform.f}px`,
};
Object.assign(docElement.style, styleObject);
}
+206 -69
View File
@@ -1,4 +1,5 @@
import { app } from "../../../scripts/app.js";
import { applyTextReplacements } from "../../../scripts/utils.js";
app.registerExtension({
name: "KJNodes.jsnodes",
@@ -9,87 +10,158 @@ app.registerExtension({
switch (nodeData.name) {
case "ConditioningMultiCombine":
nodeType.prototype.onNodeCreated = function () {
this.cond_type = "CONDITIONING"
this._type = "CONDITIONING"
this.inputs_offset = nodeData.name.includes("selective")?1:0
this.addWidget("button", "Update inputs", null, () => {
if (!this.inputs) {
this.inputs = [];
}
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
if(target_number_of_inputs===this.inputs.length)return; // already set, do nothing
const num_inputs = this.inputs.filter(input => input.type === this._type).length
if(target_number_of_inputs===num_inputs)return; // already set, do nothing
if(target_number_of_inputs < this.inputs.length){
for(let i = this.inputs.length; i>=this.inputs_offset+target_number_of_inputs; i--)
this.removeInput(i)
}
else{
for(let i = this.inputs.length+1-this.inputs_offset; i <= target_number_of_inputs; ++i)
this.addInput(`conditioning_${i}`, this.cond_type)
}
if(target_number_of_inputs < num_inputs){
const inputs_to_remove = num_inputs - target_number_of_inputs;
for(let i = 0; i < inputs_to_remove; i++) {
this.removeInput(this.inputs.length - 1);
}
}
else{
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
this.addInput(`conditioning_${i}`, this._type)
}
});
}
break;
case "ImageBatchMulti":
case "ImageAddMulti":
case "ImageConcatMulti":
case "CrossFadeImagesMulti":
case "TransitionImagesMulti":
nodeType.prototype.onNodeCreated = function () {
this._type = "IMAGE"
this.inputs_offset = nodeData.name.includes("selective")?1:0
this.addWidget("button", "Update inputs", null, () => {
if (!this.inputs) {
this.inputs = [];
}
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
if(target_number_of_inputs===this.inputs.length)return; // already set, do nothing
const num_inputs = this.inputs.filter(input => input.type === this._type).length
if(target_number_of_inputs===num_inputs)return; // already set, do nothing
if(target_number_of_inputs < this.inputs.length){
for(let i = this.inputs.length; i>=this.inputs_offset+target_number_of_inputs; i--)
this.removeInput(i)
}
else{
for(let i = this.inputs.length+1-this.inputs_offset; i <= target_number_of_inputs; ++i)
this.addInput(`image_${i}`, this._type)
if(target_number_of_inputs < num_inputs){
const inputs_to_remove = num_inputs - target_number_of_inputs;
for(let i = 0; i < inputs_to_remove; i++) {
this.removeInput(this.inputs.length - 1);
}
}
else{
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
this.addInput(`image_${i}`, this._type, {shape: 7});
}
});
}
break;
case "MaskBatchMulti":
nodeType.prototype.onNodeCreated = function () {
this._type = "MASK"
this.inputs_offset = nodeData.name.includes("selective")?1:0
this.addWidget("button", "Update inputs", null, () => {
if (!this.inputs) {
this.inputs = [];
}
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
if(target_number_of_inputs===this.inputs.length)return; // already set, do nothing
const num_inputs = this.inputs.filter(input => input.type === this._type).length
if(target_number_of_inputs===num_inputs)return; // already set, do nothing
if(target_number_of_inputs < this.inputs.length){
for(let i = this.inputs.length; i>=this.inputs_offset+target_number_of_inputs; i--)
this.removeInput(i)
if(target_number_of_inputs < num_inputs){
const inputs_to_remove = num_inputs - target_number_of_inputs;
for(let i = 0; i < inputs_to_remove; i++) {
this.removeInput(this.inputs.length - 1);
}
else{
for(let i = this.inputs.length+1-this.inputs_offset; i <= target_number_of_inputs; ++i)
this.addInput(`mask_${i}`, this._type)
}
});
}
else{
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
this.addInput(`mask_${i}`, this._type)
}
});
}
break;
case "FluxBlockLoraSelect":
case "HunyuanVideoBlockLoraSelect":
case "Wan21BlockLoraSelect":
nodeType.prototype.onNodeCreated = function () {
this.addWidget("button", "Set all", null, () => {
const userInput = prompt("Enter the values to set for widgets (e.g., s0,1,2-7=2.0, d0,1,2-7=2.0, or 1.0):", "");
if (userInput) {
const regex = /([sd])?(\d+(?:,\d+|-?\d+)*?)?=(\d+(\.\d+)?)/;
const match = userInput.match(regex);
if (match) {
const type = match[1];
const indicesPart = match[2];
const value = parseFloat(match[3]);
let targetWidgets = [];
if (type === 's') {
targetWidgets = this.widgets.filter(widget => widget.name.includes("single"));
} else if (type === 'd') {
targetWidgets = this.widgets.filter(widget => widget.name.includes("double"));
} else {
targetWidgets = this.widgets; // No type specified, all widgets
}
if (indicesPart) {
const indices = indicesPart.split(',').flatMap(part => {
if (part.includes('-')) {
const [start, end] = part.split('-').map(Number);
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
return Number(part);
});
for (const index of indices) {
if (index < targetWidgets.length) {
targetWidgets[index].value = value;
}
}
} else {
// No indices provided, set value for all target widgets
for (const widget of targetWidgets) {
widget.value = value;
}
}
} else if (!isNaN(parseFloat(userInput))) {
// Single value provided, set it for all widgets
const value = parseFloat(userInput);
for (const widget of this.widgets) {
widget.value = value;
}
} else {
alert("Invalid input format. Please use the format s0,1,2-7=2.0, d0,1,2-7=2.0, or 1.0");
}
} else {
alert("Invalid input. Please enter a value.");
}
});
};
break;
case "GetMaskSizeAndCount":
const onGetMaskSizeConnectInput = nodeType.prototype.onConnectInput;
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
const v = onGetMaskSizeConnectInput?.(this, arguments);
targetSlot.outputs[1]["name"] = "width"
targetSlot.outputs[2]["name"] = "height"
targetSlot.outputs[3]["name"] = "count"
const v = onGetMaskSizeConnectInput? onGetMaskSizeConnectInput.apply(this, arguments): undefined
this.outputs[1]["label"] = "width"
this.outputs[2]["label"] = "height"
this.outputs[3]["label"] = "count"
return v;
}
const onGetMaskSizeExecuted = nodeType.prototype.onExecuted;
const onGetMaskSizeExecuted = nodeType.prototype.onAfterExecuteNode;
nodeType.prototype.onExecuted = function(message) {
const r = onGetMaskSizeExecuted? onGetMaskSizeExecuted.apply(this,arguments): undefined
let values = message["text"].toString().split('x').map(Number);
this.outputs[1]["name"] = values[1] + " width"
this.outputs[2]["name"] = values[2] + " height"
this.outputs[3]["name"] = values[0] + " count"
this.outputs[1]["label"] = values[1] + " width"
this.outputs[2]["label"] = values[2] + " height"
this.outputs[3]["label"] = values[0] + " count"
return r
}
break;
@@ -97,62 +169,116 @@ app.registerExtension({
case "GetImageSizeAndCount":
const onGetImageSizeConnectInput = nodeType.prototype.onConnectInput;
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
const v = onGetImageSizeConnectInput?.(this, arguments);
targetSlot.outputs[1]["name"] = "width"
targetSlot.outputs[2]["name"] = "height"
targetSlot.outputs[3]["name"] = "count"
console.log(this)
const v = onGetImageSizeConnectInput? onGetImageSizeConnectInput.apply(this, arguments): undefined
//console.log(this)
this.outputs[1]["label"] = "width"
this.outputs[2]["label"] = "height"
this.outputs[3]["label"] = "count"
return v;
}
const onGetImageSizeExecuted = nodeType.prototype.onExecuted;
//const onGetImageSizeExecuted = nodeType.prototype.onExecuted;
const onGetImageSizeExecuted = nodeType.prototype.onAfterExecuteNode;
nodeType.prototype.onExecuted = function(message) {
console.log(this)
const r = onGetImageSizeExecuted? onGetImageSizeExecuted.apply(this,arguments): undefined
let values = message["text"].toString().split('x').map(Number);
this.outputs[1]["name"] = values[1] + " width"
this.outputs[2]["name"] = values[2] + " height"
this.outputs[3]["name"] = values[0] + " count"
console.log(values)
this.outputs[1]["label"] = values[1] + " width"
this.outputs[2]["label"] = values[2] + " height"
this.outputs[3]["label"] = values[0] + " count"
return r
}
break;
case "GetLatentSizeAndCount":
const onGetLatentConnectInput = nodeType.prototype.onConnectInput;
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
console.log(this)
const v = onGetLatentConnectInput? onGetLatentConnectInput.apply(this, arguments): undefined
//console.log(this)
this.outputs[1]["label"] = "width"
this.outputs[2]["label"] = "height"
this.outputs[3]["label"] = "count"
return v;
}
//const onGetImageSizeExecuted = nodeType.prototype.onExecuted;
const onGetLatentSizeExecuted = nodeType.prototype.onAfterExecuteNode;
nodeType.prototype.onExecuted = function(message) {
console.log(this)
const r = onGetLatentSizeExecuted? onGetLatentSizeExecuted.apply(this,arguments): undefined
let values = message["text"].toString().split('x').map(Number);
console.log(values)
this.outputs[1]["label"] = values[0] + " batch"
this.outputs[2]["label"] = values[1] + " channels"
this.outputs[3]["label"] = values[2] + " frames"
this.outputs[4]["label"] = values[3] + " height"
this.outputs[5]["label"] = values[4] + " width"
return r
}
break;
case "PreviewAnimation":
const onPreviewAnimationConnectInput = nodeType.prototype.onConnectInput;
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
const v = onPreviewAnimationConnectInput? onPreviewAnimationConnectInput.apply(this, arguments): undefined
this.title = "Preview Animation"
return v;
}
const onPreviewAnimationExecuted = nodeType.prototype.onAfterExecuteNode;
nodeType.prototype.onExecuted = function(message) {
const r = onPreviewAnimationExecuted? onPreviewAnimationExecuted.apply(this,arguments): undefined
let values = message["text"].toString();
this.title = "Preview Animation " + values
return r
}
break;
case "VRAM_Debug":
const onVRAM_DebugConnectInput = nodeType.prototype.onConnectInput;
nodeType.prototype.onConnectInput = function (targetSlot, type, output, originNode, originSlot) {
const v = onVRAM_DebugConnectInput?.(this, arguments);
targetSlot.outputs[3]["name"] = "freemem_before"
targetSlot.outputs[4]["name"] = "freemem_after"
const v = onVRAM_DebugConnectInput? onVRAM_DebugConnectInput.apply(this, arguments): undefined
this.outputs[3]["label"] = "freemem_before"
this.outputs[4]["label"] = "freemem_after"
return v;
}
const onVRAM_DebugExecuted = nodeType.prototype.onExecuted;
const onVRAM_DebugExecuted = nodeType.prototype.onAfterExecuteNode;
nodeType.prototype.onExecuted = function(message) {
const r = onVRAM_DebugExecuted? onVRAM_DebugExecuted.apply(this,arguments): undefined
let values = message["text"].toString().split('x');
this.outputs[3]["name"] = values[0] + " freemem_before"
this.outputs[4]["name"] = values[1] + " freemem_after"
this.outputs[3]["label"] = values[0] + " freemem_before"
this.outputs[4]["label"] = values[1] + " freemem_after"
return r
}
break;
case "JoinStringMulti":
const originalOnNodeCreated = nodeType.prototype.onNodeCreated || function() {};
nodeType.prototype.onNodeCreated = function () {
this._type = "STRING"
this.inputs_offset = nodeData.name.includes("selective")?1:0
this.addWidget("button", "Update inputs", null, () => {
if (!this.inputs) {
this.inputs = [];
}
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
if(target_number_of_inputs===this.inputs.length)return; // already set, do nothing
if(target_number_of_inputs < this.inputs.length){
for(let i = this.inputs.length; i>=this.inputs_offset+target_number_of_inputs; i--)
this.removeInput(i)
originalOnNodeCreated.apply(this, arguments);
this._type = "STRING";
this.addWidget("button", "Update inputs", null, () => {
if (!this.inputs) {
this.inputs = [];
}
const target_number_of_inputs = this.widgets.find(w => w.name === "inputcount")["value"];
const num_inputs = this.inputs.filter(input => input.name && input.name.toLowerCase().includes("string_")).length
if (target_number_of_inputs === num_inputs) return; // already set, do nothing
if(target_number_of_inputs < num_inputs){
const inputs_to_remove = num_inputs - target_number_of_inputs;
for(let i = 0; i < inputs_to_remove; i++) {
this.removeInput(this.inputs.length - 1);
}
}
else{
for(let i = this.inputs.length+1-this.inputs_offset; i <= target_number_of_inputs; ++i)
this.addInput(`string_${i}`, this._type)
}
});
}
break;
for(let i = num_inputs+1; i <= target_number_of_inputs; ++i)
this.addInput(`string_${i}`, this._type, {shape: 7});
}
});
}
break;
case "SoundReactive":
nodeType.prototype.onNodeCreated = function () {
let audioContext;
@@ -255,6 +381,17 @@ app.registerExtension({
this.addWidget("button", "Stop mic capture", null, stopMicrophoneCapture);
};
break;
case "SaveImageKJ":
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : void 0;
const widget = this.widgets.find((w) => w.name === "filename_prefix");
widget.serializeValue = () => {
return applyTextReplacements(app, widget.value);
};
return r;
};
break;
}
+734
View File
@@ -0,0 +1,734 @@
import { app } from '../../../scripts/app.js'
//from melmass
export function makeUUID() {
let dt = new Date().getTime()
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = ((dt + Math.random() * 16) % 16) | 0
dt = Math.floor(dt / 16)
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
})
return uuid
}
export const loadScript = (
FILE_URL,
async = true,
type = 'text/javascript',
) => {
return new Promise((resolve, reject) => {
try {
// Check if the script already exists
const existingScript = document.querySelector(`script[src="${FILE_URL}"]`)
if (existingScript) {
resolve({ status: true, message: 'Script already loaded' })
return
}
const scriptEle = document.createElement('script')
scriptEle.type = type
scriptEle.async = async
scriptEle.src = FILE_URL
scriptEle.addEventListener('load', (ev) => {
resolve({ status: true })
})
scriptEle.addEventListener('error', (ev) => {
reject({
status: false,
message: `Failed to load the script ${FILE_URL}`,
})
})
document.body.appendChild(scriptEle)
} catch (error) {
reject(error)
}
})
}
const create_documentation_stylesheet = () => {
const tag = 'kj-pointseditor-stylesheet'
let styleTag = document.head.querySelector(tag)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.type = 'text/css'
styleTag.id = tag
styleTag.innerHTML = `
.points-editor {
position: absolute;
font: 12px monospace;
line-height: 1.5em;
padding: 10px;
z-index: 0;
overflow: hidden;
}
`
document.head.appendChild(styleTag)
}
}
loadScript('kjweb_async/svg-path-properties.min.js').catch((e) => {
console.log(e)
})
loadScript('kjweb_async/protovis.min.js').catch((e) => {
console.log(e)
})
create_documentation_stylesheet()
function chainCallback(object, property, callback) {
if (object == undefined) {
//This should not happen.
console.error("Tried to add callback to non-existant object")
return;
}
if (property in object) {
const callback_orig = object[property]
object[property] = function () {
const r = callback_orig.apply(this, arguments);
callback.apply(this, arguments);
return r
};
} else {
object[property] = callback;
}
}
app.registerExtension({
name: 'KJNodes.PointEditor',
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name === 'PointsEditor') {
chainCallback(nodeType.prototype, "onNodeCreated", function () {
hideWidgetForGood(this, this.widgets.find(w => w.name === "coordinates"))
hideWidgetForGood(this, this.widgets.find(w => w.name === "neg_coordinates"))
hideWidgetForGood(this, this.widgets.find(w => w.name === "bboxes"))
var element = document.createElement("div");
this.uuid = makeUUID()
element.id = `points-editor-${this.uuid}`
this.previewMediaType = 'image'
this.pointsEditor = this.addDOMWidget(nodeData.name, "PointsEditorWidget", element, {
serialize: false,
hideOnZoom: false,
});
// context menu
this.contextMenu = document.createElement("div");
this.contextMenu.id = "context-menu";
this.contextMenu.style.display = "none";
this.contextMenu.style.position = "absolute";
this.contextMenu.style.backgroundColor = "#202020";
this.contextMenu.style.minWidth = "100px";
this.contextMenu.style.boxShadow = "0px 8px 16px 0px rgba(0,0,0,0.2)";
this.contextMenu.style.zIndex = "100";
this.contextMenu.style.padding = "5px";
function styleMenuItem(menuItem) {
menuItem.style.display = "block";
menuItem.style.padding = "5px";
menuItem.style.color = "#FFF";
menuItem.style.fontFamily = "Arial, sans-serif";
menuItem.style.fontSize = "16px";
menuItem.style.textDecoration = "none";
menuItem.style.marginBottom = "5px";
}
function createMenuItem(id, textContent) {
let menuItem = document.createElement("a");
menuItem.href = "#";
menuItem.id = `menu-item-${id}`;
menuItem.textContent = textContent;
styleMenuItem(menuItem);
return menuItem;
}
// Create an array of menu items using the createMenuItem function
this.menuItems = [
createMenuItem(0, "Load Image"),
createMenuItem(1, "Clear Image"),
];
// Add mouseover and mouseout event listeners to each menu item for styling
this.menuItems.forEach(menuItem => {
menuItem.addEventListener('mouseover', function () {
this.style.backgroundColor = "gray";
});
menuItem.addEventListener('mouseout', function () {
this.style.backgroundColor = "#202020";
});
});
// Append each menu item to the context menu
this.menuItems.forEach(menuItem => {
this.contextMenu.appendChild(menuItem);
});
document.body.appendChild(this.contextMenu);
this.addWidget("button", "New canvas", null, () => {
if (!this.properties || !("points" in this.properties)) {
this.editor = new PointsEditor(this);
this.addProperty("points", this.constructor.type, "string");
this.addProperty("neg_points", this.constructor.type, "string");
}
else {
this.editor = new PointsEditor(this, true);
}
});
this.setSize([550, 550]);
this.resizable = false;
this.pointsEditor.parentEl = document.createElement("div");
this.pointsEditor.parentEl.className = "points-editor";
this.pointsEditor.parentEl.id = `points-editor-${this.uuid}`
element.appendChild(this.pointsEditor.parentEl);
chainCallback(this, "onConfigure", function () {
try {
this.editor = new PointsEditor(this);
} catch (error) {
console.error("An error occurred while configuring the editor:", error);
}
});
chainCallback(this, "onExecuted", function (message) {
let bg_image = message["bg_image"];
this.properties.imgData = {
name: "bg_image",
base64: bg_image
};
this.editor.refreshBackgroundImage(this);
});
}); // onAfterGraphConfigured
}//node created
} //before register
})//register
class PointsEditor {
constructor(context, reset = false) {
this.node = context;
this.reset = reset;
const self = this; // Keep a reference to the main class context
console.log("creatingPointEditor")
this.node.pasteFile = (file) => {
if (file.type.startsWith("image/")) {
this.handleImageFile(file);
return true;
}
return false;
};
this.node.onDragOver = function (e) {
if (e.dataTransfer && e.dataTransfer.items) {
return [...e.dataTransfer.items].some(f => f.kind === "file" && f.type.startsWith("image/"));
}
return false;
};
// On drop upload files
this.node.onDragDrop = (e) => {
console.log("onDragDrop called");
let handled = false;
for (const file of e.dataTransfer.files) {
if (file.type.startsWith("image/")) {
this.handleImageFile(file);
handled = true;
}
}
return handled;
};
// context menu
this.createContextMenu();
if (reset && context.pointsEditor.element) {
context.pointsEditor.element.innerHTML = ''; // Clear the container
}
this.pos_coordWidget = context.widgets.find(w => w.name === "coordinates");
this.neg_coordWidget = context.widgets.find(w => w.name === "neg_coordinates");
this.pointsStoreWidget = context.widgets.find(w => w.name === "points_store");
this.widthWidget = context.widgets.find(w => w.name === "width");
this.heightWidget = context.widgets.find(w => w.name === "height");
this.bboxStoreWidget = context.widgets.find(w => w.name === "bbox_store");
this.bboxWidget = context.widgets.find(w => w.name === "bboxes");
//widget callbacks
this.widthWidget.callback = () => {
this.width = this.widthWidget.value;
if (this.width > 256) {
context.setSize([this.width + 45, context.size[1]]);
}
this.vis.width(this.width);
this.updateData();
}
this.heightWidget.callback = () => {
this.height = this.heightWidget.value
this.vis.height(this.height)
context.setSize([context.size[0], this.height + 300]);
this.updateData();
}
this.pointsStoreWidget.callback = () => {
this.points = JSON.parse(pointsStoreWidget.value).positive;
this.neg_points = JSON.parse(pointsStoreWidget.value).negative;
this.updateData();
}
this.bboxStoreWidget.callback = () => {
this.bbox = JSON.parse(bboxStoreWidget.value)
this.updateData();
}
this.width = this.widthWidget.value;
this.height = this.heightWidget.value;
var i = 3;
this.points = [];
this.neg_points = [];
this.bbox = [{}];
var drawing = false;
// Initialize or reset points array
if (!reset && this.pointsStoreWidget.value != "") {
this.points = JSON.parse(this.pointsStoreWidget.value).positive;
this.neg_points = JSON.parse(this.pointsStoreWidget.value).negative;
this.bbox = JSON.parse(this.bboxStoreWidget.value);
console.log(this.bbox)
} else {
this.points = [
{
x: this.width / 2, // Middle point horizontally centered
y: this.height / 2 // Middle point vertically centered
}
];
this.neg_points = [
{
x: 0, // Middle point horizontally centered
y: 0 // Middle point vertically centered
}
];
const combinedPoints = {
positive: this.points,
negative: this.neg_points,
};
this.pointsStoreWidget.value = JSON.stringify(combinedPoints);
this.bboxStoreWidget.value = JSON.stringify(this.bbox);
}
//create main canvas panel
this.vis = new pv.Panel()
.width(this.width)
.height(this.height)
.fillStyle("#222")
.strokeStyle("gray")
.lineWidth(2)
.antialias(false)
.margin(10)
.event("mousedown", function () {
if (pv.event.shiftKey && pv.event.button === 2) { // Use pv.event to access the event object
let scaledMouse = {
x: this.mouse().x / app.canvas.ds.scale,
y: this.mouse().y / app.canvas.ds.scale
};
i = self.neg_points.push(scaledMouse) - 1;
self.updateData();
return this;
}
else if (pv.event.shiftKey) {
let scaledMouse = {
x: this.mouse().x / app.canvas.ds.scale,
y: this.mouse().y / app.canvas.ds.scale
};
i = self.points.push(scaledMouse) - 1;
self.updateData();
return this;
}
else if (pv.event.ctrlKey) {
console.log("start drawing at " + this.mouse().x / app.canvas.ds.scale + ", " + this.mouse().y / app.canvas.ds.scale);
drawing = true;
self.bbox[0].startX = this.mouse().x / app.canvas.ds.scale;
self.bbox[0].startY = this.mouse().y / app.canvas.ds.scale;
}
else if (pv.event.button === 2) {
self.node.contextMenu.style.display = 'block';
self.node.contextMenu.style.left = `${pv.event.clientX}px`;
self.node.contextMenu.style.top = `${pv.event.clientY}px`;
}
})
.event("mousemove", function () {
if (drawing) {
self.bbox[0].endX = this.mouse().x / app.canvas.ds.scale;
self.bbox[0].endY = this.mouse().y / app.canvas.ds.scale;
self.vis.render();
}
})
.event("mouseup", function () {
console.log("end drawing at " + this.mouse().x / app.canvas.ds.scale + ", " + this.mouse().y / app.canvas.ds.scale);
drawing = false;
self.updateData();
});
this.backgroundImage = this.vis.add(pv.Image).visible(false)
//create bounding box
this.bounding_box = this.vis.add(pv.Area)
.data(function () {
if (drawing || (self.bbox && self.bbox[0] && Object.keys(self.bbox[0]).length > 0)) {
return [self.bbox[0].startX, self.bbox[0].endX];
} else {
return [];
}
})
.bottom(function () {return self.height - Math.max(self.bbox[0].startY, self.bbox[0].endY); })
.left(function (d) {return d; })
.height(function () {return Math.abs(self.bbox[0].startY - self.bbox[0].endY);})
.fillStyle("rgba(70, 130, 180, 0.5)")
.strokeStyle("steelblue")
.visible(function () {return drawing || Object.keys(self.bbox[0]).length > 0; })
.add(pv.Dot)
.visible(function () {return drawing || Object.keys(self.bbox[0]).length > 0; })
.data(() => {
if (self.bbox && Object.keys(self.bbox[0]).length > 0) {
return [{
x: self.bbox[0].endX,
y: self.bbox[0].endY
}];
} else {
return [];
}
})
.left(d => d.x)
.top(d => d.y)
.radius(Math.log(Math.min(self.width, self.height)) * 1)
.shape("square")
.cursor("move")
.strokeStyle("steelblue")
.lineWidth(2)
.fillStyle(function () { return "rgba(100, 100, 100, 0.6)"; })
.event("mousedown", pv.Behavior.drag())
.event("drag", function () {
let adjustedX = this.mouse().x / app.canvas.ds.scale; // Adjust the new position by the inverse of the scale factor
let adjustedY = this.mouse().y / app.canvas.ds.scale;
// Adjust the new position if it would place the dot outside the bounds of the vis.Panel
adjustedX = Math.max(0, Math.min(self.vis.width(), adjustedX));
adjustedY = Math.max(0, Math.min(self.vis.height(), adjustedY));
self.bbox[0].endX = this.mouse().x / app.canvas.ds.scale;
self.bbox[0].endY = this.mouse().y / app.canvas.ds.scale;
self.vis.render();
})
.event("dragend", function () {
self.updateData();
});
//create positive points
this.vis.add(pv.Dot)
.data(() => this.points)
.left(d => d.x)
.top(d => d.y)
.radius(Math.log(Math.min(self.width, self.height)) * 4)
.shape("circle")
.cursor("move")
.strokeStyle(function () { return i == this.index ? "#07f907" : "#139613"; })
.lineWidth(4)
.fillStyle(function () { return "rgba(100, 100, 100, 0.6)"; })
.event("mousedown", pv.Behavior.drag())
.event("dragstart", function () {
i = this.index;
})
.event("dragend", function () {
if (pv.event.button === 2 && i !== 0 && i !== self.points.length - 1) {
this.index = i;
self.points.splice(i--, 1);
}
self.updateData();
})
.event("drag", function () {
let adjustedX = this.mouse().x / app.canvas.ds.scale; // Adjust the new X position by the inverse of the scale factor
let adjustedY = this.mouse().y / app.canvas.ds.scale; // Adjust the new Y position by the inverse of the scale factor
// Determine the bounds of the vis.Panel
const panelWidth = self.vis.width();
const panelHeight = self.vis.height();
// Adjust the new position if it would place the dot outside the bounds of the vis.Panel
adjustedX = Math.max(0, Math.min(panelWidth, adjustedX));
adjustedY = Math.max(0, Math.min(panelHeight, adjustedY));
self.points[this.index] = { x: adjustedX, y: adjustedY }; // Update the point's position
self.vis.render(); // Re-render the visualization to reflect the new position
})
.anchor("center")
.add(pv.Label)
.left(d => d.x < this.width / 2 ? d.x + 30 : d.x - 35) // Shift label to right if on left half, otherwise shift to left
.top(d => d.y < this.height / 2 ? d.y + 25 : d.y - 25) // Shift label down if on top half, otherwise shift up
.font(25 + "px sans-serif")
.text(d => {return this.points.indexOf(d); })
.textStyle("#139613")
.textShadow("2px 2px 2px black")
.add(pv.Dot) // Add smaller point in the center
.data(() => this.points)
.left(d => d.x)
.top(d => d.y)
.radius(2) // Smaller radius for the center point
.shape("circle")
.fillStyle("red") // Color for the center point
.lineWidth(1); // Stroke thickness for the center point
//create negative points
this.vis.add(pv.Dot)
.data(() => this.neg_points)
.left(d => d.x)
.top(d => d.y)
.radius(Math.log(Math.min(self.width, self.height)) * 4)
.shape("circle")
.cursor("move")
.strokeStyle(function () { return i == this.index ? "#f91111" : "#891616"; })
.lineWidth(4)
.fillStyle(function () { return "rgba(100, 100, 100, 0.6)"; })
.event("mousedown", pv.Behavior.drag())
.event("dragstart", function () {
i = this.index;
})
.event("dragend", function () {
if (pv.event.button === 2 && i !== 0 && i !== self.neg_points.length - 1) {
this.index = i;
self.neg_points.splice(i--, 1);
}
self.updateData();
})
.event("drag", function () {
let adjustedX = this.mouse().x / app.canvas.ds.scale; // Adjust the new X position by the inverse of the scale factor
let adjustedY = this.mouse().y / app.canvas.ds.scale; // Adjust the new Y position by the inverse of the scale factor
// Determine the bounds of the vis.Panel
const panelWidth = self.vis.width();
const panelHeight = self.vis.height();
// Adjust the new position if it would place the dot outside the bounds of the vis.Panel
adjustedX = Math.max(0, Math.min(panelWidth, adjustedX));
adjustedY = Math.max(0, Math.min(panelHeight, adjustedY));
self.neg_points[this.index] = { x: adjustedX, y: adjustedY }; // Update the point's position
self.vis.render(); // Re-render the visualization to reflect the new position
})
.anchor("center")
.add(pv.Label)
.left(d => d.x < this.width / 2 ? d.x + 30 : d.x - 35) // Shift label to right if on left half, otherwise shift to left
.top(d => d.y < this.height / 2 ? d.y + 25 : d.y - 25) // Shift label down if on top half, otherwise shift up
.font(25 + "px sans-serif")
.text(d => {return this.neg_points.indexOf(d); })
.textStyle("red")
.textShadow("2px 2px 2px black")
.add(pv.Dot) // Add smaller point in the center
.data(() => this.neg_points)
.left(d => d.x)
.top(d => d.y)
.radius(2) // Smaller radius for the center point
.shape("circle")
.fillStyle("red") // Color for the center point
.lineWidth(1); // Stroke thickness for the center point
if (this.points.length != 0) {
this.vis.render();
}
var svgElement = this.vis.canvas();
svgElement.style['zIndex'] = "2"
svgElement.style['position'] = "relative"
this.node.pointsEditor.element.appendChild(svgElement);
if (this.width > 256) {
this.node.setSize([this.width + 45, this.node.size[1]]);
}
this.node.setSize([this.node.size[0], this.height + 300]);
this.updateData();
this.refreshBackgroundImage();
}//end constructor
updateData = () => {
if (!this.points || this.points.length === 0) {
console.log("no points");
return;
}
const combinedPoints = {
positive: this.points,
negative: this.neg_points,
};
this.pointsStoreWidget.value = JSON.stringify(combinedPoints);
this.pos_coordWidget.value = JSON.stringify(this.points);
this.neg_coordWidget.value = JSON.stringify(this.neg_points);
if (this.bbox.length != 0) {
let bboxString = JSON.stringify(this.bbox);
this.bboxStoreWidget.value = bboxString;
this.bboxWidget.value = bboxString;
}
this.vis.render();
};
handleImageLoad = (img, file, base64String) => {
console.log(img.width, img.height); // Access width and height here
this.widthWidget.value = img.width;
this.heightWidget.value = img.height;
if (img.width != this.vis.width() || img.height != this.vis.height()) {
if (img.width > 256) {
this.node.setSize([img.width + 45, this.node.size[1]]);
}
this.node.setSize([this.node.size[0], img.height + 300]);
this.vis.width(img.width);
this.vis.height(img.height);
this.height = img.height;
this.width = img.width;
this.updateData();
}
this.backgroundImage.url(file ? URL.createObjectURL(file) : `data:${this.node.properties.imgData.type};base64,${base64String}`).visible(true).root.render();
};
processImage = (img, file) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const maxWidth = 800; // maximum width
const maxHeight = 600; // maximum height
let width = img.width;
let height = img.height;
// Calculate the new dimensions while preserving the aspect ratio
if (width > height) {
if (width > maxWidth) {
height *= maxWidth / width;
width = maxWidth;
}
} else {
if (height > maxHeight) {
width *= maxHeight / height;
height = maxHeight;
}
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
// Get the compressed image data as a Base64 string
const base64String = canvas.toDataURL('image/jpeg', 0.5).replace('data:', '').replace(/^.+,/, ''); // 0.5 is the quality from 0 to 1
this.node.properties.imgData = {
name: file.name,
lastModified: file.lastModified,
size: file.size,
type: file.type,
base64: base64String
};
handleImageLoad(img, file, base64String);
};
handleImageFile = (file) => {
const reader = new FileReader();
reader.onloadend = () => {
const img = new Image();
img.src = reader.result;
img.onload = () => processImage(img, file);
};
reader.readAsDataURL(file);
const imageUrl = URL.createObjectURL(file);
const img = new Image();
img.src = imageUrl;
img.onload = () => this.handleImageLoad(img, file, null);
};
refreshBackgroundImage = () => {
if (this.node.properties.imgData && this.node.properties.imgData.base64) {
const base64String = this.node.properties.imgData.base64;
const imageUrl = `data:${this.node.properties.imgData.type};base64,${base64String}`;
const img = new Image();
img.src = imageUrl;
img.onload = () => this.handleImageLoad(img, null, base64String);
}
};
createContextMenu = () => {
self = this;
document.addEventListener('contextmenu', function (e) {
e.preventDefault();
});
document.addEventListener('click', function (e) {
if (!self.node.contextMenu.contains(e.target)) {
self.node.contextMenu.style.display = 'none';
}
});
this.node.menuItems.forEach((menuItem, index) => {
self = this;
menuItem.addEventListener('click', function (e) {
e.preventDefault();
switch (index) {
case 0:
// Create file input element
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*'; // Accept only image files
// Listen for file selection
fileInput.addEventListener('change', function (event) {
const file = event.target.files[0]; // Get the selected file
if (file) {
const imageUrl = URL.createObjectURL(file);
let img = new Image();
img.src = imageUrl;
img.onload = () => self.handleImageLoad(img, file, null);
}
});
fileInput.click();
self.node.contextMenu.style.display = 'none';
break;
case 1:
self.backgroundImage.visible(false).root.render();
self.node.properties.imgData = null;
self.node.contextMenu.style.display = 'none';
break;
}
});
});
}//end createContextMenu
}//end class
//from melmass
export function hideWidgetForGood(node, widget, suffix = '') {
widget.origType = widget.type
widget.origComputeSize = widget.computeSize
widget.origSerializeValue = widget.serializeValue
widget.computeSize = () => [0, -4] // -4 is due to the gap litegraph adds between widgets automatically
widget.type = "converted-widget" + suffix
// widget.serializeValue = () => {
// // Prevent serializing the widget if we have no input linked
// const w = node.inputs?.find((i) => i.widget?.name === widget.name);
// if (w?.link == null) {
// return undefined;
// }
// return widget.origSerializeValue ? widget.origSerializeValue() : widget.value;
// };
// Hide any linked widgets, e.g. seed+seedControl
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidgetForGood(node, w, ':' + widget.name)
}
}
}
+45 -23
View File
@@ -12,8 +12,13 @@ function setColorAndBgColor(type) {
"IMAGE": LGraphCanvas.node_colors.pale_blue,
"CLIP": LGraphCanvas.node_colors.yellow,
"FLOAT": LGraphCanvas.node_colors.green,
"MASK": LGraphCanvas.node_colors.cyan,
"MASK": { color: "#1c5715", bgcolor: "#1f401b"},
"INT": { color: "#1b4669", bgcolor: "#29699c"},
"CONTROL_NET": { color: "#156653", bgcolor: "#1c453b"},
"NOISE": { color: "#2e2e2e", bgcolor: "#242121"},
"GUIDER": { color: "#3c7878", bgcolor: "#1c453b"},
"SAMPLER": { color: "#614a4a", bgcolor: "#3b2c2c"},
"SIGMAS": { color: "#485248", bgcolor: "#272e27"},
};
@@ -23,20 +28,21 @@ function setColorAndBgColor(type) {
this.bgcolor = colors.bgcolor;
}
}
let isAlertShown = false;
let disablePrefix = app.ui.settings.getSettingValue("KJNodes.disablePrefix")
const LGraphNode = LiteGraph.LGraphNode
function showAlertWithThrottle(message, delay) {
if (!isAlertShown) {
isAlertShown = true;
alert(message);
setTimeout(() => isAlertShown = false, delay);
}
function showAlert(message) {
app.extensionManager.toast.add({
severity: 'warn',
summary: "KJ Get/Set",
detail: `${message}. Most likely you're missing custom nodes`,
life: 5000,
})
}
app.registerExtension({
name: "SetNode",
registerCustomNodes() {
class SetNode {
class SetNode extends LGraphNode {
defaultVisibility = true;
serialize_widgets = true;
drawConnection = false;
@@ -45,7 +51,8 @@ app.registerExtension({
canvas = app.canvas;
menuEntry = "Show connections";
constructor() {
constructor(title) {
super(title)
if (!this.properties) {
this.properties = {
"previousName": ""
@@ -89,10 +96,11 @@ app.registerExtension({
}
}
if (slotType == 2 && !isChangeConnect) {
this.outputs[slot].type = '*';
this.outputs[slot].name = '*';
}
if (this.outputs && this.outputs[slot]) {
this.outputs[slot].type = '*';
this.outputs[slot].name = '*';
}
}
//On Connect
if (link_info && node.graph && slotType == 1 && isChangeConnect) {
const fromNode = node.graph._nodes.find((otherNode) => otherNode.id == link_info.origin_id);
@@ -115,7 +123,7 @@ app.registerExtension({
setColorAndBgColor.call(this, type);
}
} else {
alert("Error: Set node input undefined. Most likely you're missing custom nodes");
showAlert("node input undefined.")
}
}
if (link_info && node.graph && slotType == 2 && isChangeConnect) {
@@ -127,7 +135,7 @@ app.registerExtension({
this.outputs[0].type = type;
this.outputs[0].name = type;
} else {
alert("Error: Get Set node output undefined. Most likely you're missing custom nodes");
showAlert('node output undefined');
}
}
@@ -320,6 +328,8 @@ app.registerExtension({
];
}
// Provide a default link object with necessary properties, to avoid errors as link can't be null anymore
const defaultLink = { type: 'default', color: this.slotColor };
for (const getter of this.currentGetters) {
if (!this.flags.collapsed) {
@@ -340,7 +350,7 @@ app.registerExtension({
ctx,
start_node_slotpos,
end_node_slotpos,
null,
defaultLink,
false,
null,
this.slotColor,
@@ -365,7 +375,7 @@ app.registerExtension({
app.registerExtension({
name: "GetNode",
registerCustomNodes() {
class GetNode {
class GetNode extends LGraphNode {
defaultVisibility = true;
serialize_widgets = true;
@@ -374,7 +384,8 @@ app.registerExtension({
currentSetter = null;
canvas = app.canvas;
constructor() {
constructor(title) {
super(title)
if (!this.properties) {
this.properties = {};
}
@@ -439,7 +450,7 @@ app.registerExtension({
if (this.outputs[0].type !== '*' && this.outputs[0].links) {
this.outputs[0].links.filter(linkId => {
const link = node.graph.links[linkId];
return link && (link.type !== this.outputs[0].type && link.type !== '*');
return link && (!link.type.split(",").includes(this.outputs[0].type) && link.type !== '*');
}).forEach(linkId => {
node.graph.removeLink(linkId);
});
@@ -470,6 +481,9 @@ app.registerExtension({
getInputLink(slot) {
const setter = this.findSetter(this.graph);
if (this.mode !== 0) {
return null;
}
if (setter) {
const slotInfo = setter.inputs[slot];
@@ -477,8 +491,8 @@ app.registerExtension({
return link;
} else {
const errorMessage = "No SetNode found for " + this.widgets[0].value + "(" + this.type + ")";
showAlertWithThrottle(errorMessage, 5000);
throw new Error(errorMessage);
showAlert(errorMessage);
//throw new Error(errorMessage);
}
}
onAdded(graph) {
@@ -509,6 +523,11 @@ app.registerExtension({
}
onDrawForeground(ctx, lGraphCanvas) {
if (this.mode === 4) {
console.log(`Mode is ${this.mode}, setting to disabled`)
this.mode = 2;
return null;
}
if (this.drawConnection) {
this._drawVirtualLink(lGraphCanvas, ctx);
}
@@ -520,6 +539,9 @@ app.registerExtension({
// }
_drawVirtualLink(lGraphCanvas, ctx) {
if (!this.currentSetter) return;
// Provide a default link object with necessary properties, to avoid errors as link can't be null anymore
const defaultLink = { type: 'default', color: this.slotColor };
let start_node_slotpos = this.currentSetter.getConnectionPos(false, 0);
start_node_slotpos = [
@@ -531,7 +553,7 @@ app.registerExtension({
ctx,
start_node_slotpos,
end_node_slotpos,
null,
defaultLink,
false,
null,
this.slotColor
+1092 -420
View File
File diff suppressed because it is too large Load Diff