From fc4327087bc208f43d2bc83aac9d17b23412f9be Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Mon, 30 Jun 2025 15:10:34 +0800 Subject: [PATCH] Add WanVideo Lora Select node and related functionality. Fixes #266 - Implemented the WanVideo Lora Select node in Python with input handling for low memory loading and LORA syntax processing. - Updated the JavaScript side to register the new node and manage its widget interactions. - Enhanced constants files to include the new node type and its corresponding ID. - Modified existing Lora Loader and Stacker references to accommodate the new node in various workflows and UI components. - Added example workflow JSON for the new node to demonstrate its usage. --- __init__.py | 4 +- ...wanvideo_1_3B_control_lora_example_01.json | 1 + py/nodes/lora_stacker.py | 2 - py/nodes/wanvideo_lora_select.py | 92 ++++++++++++ py/utils/constants.py | 3 +- static/js/utils/constants.js | 9 +- static/js/utils/uiHelpers.js | 2 +- web/comfyui/lora_loader.js | 7 +- web/comfyui/lora_stacker.js | 1 + web/comfyui/usage_stats.js | 4 +- web/comfyui/wanvideo_lora_select.js | 131 ++++++++++++++++++ 11 files changed, 245 insertions(+), 11 deletions(-) create mode 100644 example_workflows/wanvideo_1_3B_control_lora_example_01.json create mode 100644 py/nodes/wanvideo_lora_select.py create mode 100644 web/comfyui/wanvideo_lora_select.js diff --git a/__init__.py b/__init__.py index 375dfc13..991d3792 100644 --- a/__init__.py +++ b/__init__.py @@ -4,6 +4,7 @@ from .py.nodes.trigger_word_toggle import TriggerWordToggle from .py.nodes.lora_stacker import LoraStacker from .py.nodes.save_image import SaveImage from .py.nodes.debug_metadata import DebugMetadata +from .py.nodes.wanvideo_lora_select import WanVideoLoraSelect # Import metadata collector to install hooks on startup from .py.metadata_collector import init as init_metadata_collector @@ -12,7 +13,8 @@ NODE_CLASS_MAPPINGS = { TriggerWordToggle.NAME: TriggerWordToggle, LoraStacker.NAME: LoraStacker, SaveImage.NAME: SaveImage, - DebugMetadata.NAME: DebugMetadata + DebugMetadata.NAME: DebugMetadata, + WanVideoLoraSelect.NAME: WanVideoLoraSelect } WEB_DIRECTORY = "./web/comfyui" diff --git a/example_workflows/wanvideo_1_3B_control_lora_example_01.json b/example_workflows/wanvideo_1_3B_control_lora_example_01.json new file mode 100644 index 00000000..ed4d8adb --- /dev/null +++ b/example_workflows/wanvideo_1_3B_control_lora_example_01.json @@ -0,0 +1 @@ +{"id":"04f1de76-e363-4c27-bec7-eb184ac6e476","revision":0,"last_node_id":108,"last_link_id":151,"nodes":[{"id":36,"type":"Note","pos":[160,-1010],"size":[374.3061828613281,171.9547576904297],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["fp8_fast seems to cause huge quality degradation\n\nfp_16_fast enables \"Full FP16 Accmumulation in FP16 GEMMs\" feature available in the very latest pytorch nightly, this is around 20% speed boost. \n\nSageattn if you have it installed can be used for almost double inference speed"],"color":"#432","bgcolor":"#653"},{"id":35,"type":"WanVideoTorchCompileSettings","pos":[-276.8500671386719,-1050.6326904296875],"size":[390.5999755859375,202],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"backend","name":"backend","type":"COMBO","widget":{"name":"backend"},"link":null},{"localized_name":"fullgraph","name":"fullgraph","type":"BOOLEAN","widget":{"name":"fullgraph"},"link":null},{"localized_name":"mode","name":"mode","type":"COMBO","widget":{"name":"mode"},"link":null},{"localized_name":"dynamic","name":"dynamic","type":"BOOLEAN","widget":{"name":"dynamic"},"link":null},{"localized_name":"dynamo_cache_size_limit","name":"dynamo_cache_size_limit","type":"INT","widget":{"name":"dynamo_cache_size_limit"},"link":null},{"localized_name":"compile_transformer_blocks_only","name":"compile_transformer_blocks_only","type":"BOOLEAN","widget":{"name":"compile_transformer_blocks_only"},"link":null},{"localized_name":"dynamo_recompile_limit","name":"dynamo_recompile_limit","shape":7,"type":"INT","widget":{"name":"dynamo_recompile_limit"},"link":null}],"outputs":[{"localized_name":"torch_compile_args","name":"torch_compile_args","type":"WANCOMPILEARGS","slot_index":0,"links":[]}],"properties":{"cnr_id":"comfyui-wanvideowrapper","ver":"1.1.9","Node name for S&R":"WanVideoTorchCompileSettings"},"widgets_values":["inductor",false,"default",false,64,true,128],"color":"#223","bgcolor":"#335"},{"id":44,"type":"Note","pos":[-620.9041137695312,-1049.732421875],"size":[303.0501403808594,88],"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["If you have Triton installed, connect this for ~30% speed increase"],"color":"#432","bgcolor":"#653"},{"id":52,"type":"WanVideoTeaCache","pos":[1334.42626953125,-588.2476196289062],"size":[315,178],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"rel_l1_thresh","name":"rel_l1_thresh","type":"FLOAT","widget":{"name":"rel_l1_thresh"},"link":null},{"localized_name":"start_step","name":"start_step","type":"INT","widget":{"name":"start_step"},"link":null},{"localized_name":"end_step","name":"end_step","type":"INT","widget":{"name":"end_step"},"link":null},{"localized_name":"cache_device","name":"cache_device","type":"COMBO","widget":{"name":"cache_device"},"link":null},{"localized_name":"use_coefficients","name":"use_coefficients","type":"BOOLEAN","widget":{"name":"use_coefficients"},"link":null},{"localized_name":"mode","name":"mode","shape":7,"type":"COMBO","widget":{"name":"mode"},"link":null}],"outputs":[{"localized_name":"teacache_args","name":"teacache_args","type":"TEACACHEARGS","links":[103]}],"properties":{"cnr_id":"comfyui-wanvideowrapper","ver":"1.1.9","Node name for S&R":"WanVideoTeaCache"},"widgets_values":[0.1,1,-1,"offload_device","true","e"]},{"id":53,"type":"Note","pos":[1329.0076904296875,-810.7708740234375],"size":[324.64129638671875,159.47401428222656],"flags":{},"order":4,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["TeaCache could be considered to be sort of an automated step skipper \n\nThe relative l1 threshold -value determines how aggressive this is, higher values are faster but quality suffers more. Very first steps should NEVER be skipped with this model or it kills the motion. When using the pre-calculated coefficients, the treshold value should be much higher than with the default coefficients."],"color":"#432","bgcolor":"#653"},{"id":28,"type":"WanVideoDecode","pos":[1704.86572265625,-604.0441284179688],"size":[315,174],"flags":{},"order":18,"mode":0,"inputs":[{"localized_name":"vae","name":"vae","type":"WANVAE","link":43},{"localized_name":"samples","name":"samples","type":"LATENT","link":33},{"localized_name":"enable_vae_tiling","name":"enable_vae_tiling","type":"BOOLEAN","widget":{"name":"enable_vae_tiling"},"link":null},{"localized_name":"tile_x","name":"tile_x","type":"INT","widget":{"name":"tile_x"},"link":null},{"localized_name":"tile_y","name":"tile_y","type":"INT","widget":{"name":"tile_y"},"link":null},{"localized_name":"tile_stride_x","name":"tile_stride_x","type":"INT","widget":{"name":"tile_stride_x"},"link":null},{"localized_name":"tile_stride_y","name":"tile_stride_y","type":"INT","widget":{"name":"tile_stride_y"},"link":null}],"outputs":[{"localized_name":"images","name":"images","type":"IMAGE","slot_index":0,"links":[145]}],"properties":{"cnr_id":"comfyui-wanvideowrapper","ver":"1.1.9","Node name for S&R":"WanVideoDecode"},"widgets_values":[false,272,272,144,128],"color":"#322","bgcolor":"#533"},{"id":16,"type":"WanVideoTextEncode","pos":[710.38916015625,-381.6058044433594],"size":[420.30511474609375,261.5306701660156],"flags":{},"order":13,"mode":0,"inputs":[{"localized_name":"t5","name":"t5","type":"WANTEXTENCODER","link":15},{"localized_name":"model_to_offload","name":"model_to_offload","shape":7,"type":"WANVIDEOMODEL","link":null},{"localized_name":"positive_prompt","name":"positive_prompt","type":"STRING","widget":{"name":"positive_prompt"},"link":null},{"localized_name":"negative_prompt","name":"negative_prompt","type":"STRING","widget":{"name":"negative_prompt"},"link":null},{"localized_name":"force_offload","name":"force_offload","shape":7,"type":"BOOLEAN","widget":{"name":"force_offload"},"link":null}],"outputs":[{"localized_name":"text_embeds","name":"text_embeds","type":"WANVIDEOTEXTEMBEDS","slot_index":0,"links":[30]}],"properties":{"cnr_id":"comfyui-wanvideowrapper","ver":"1.1.9","Node name for S&R":"WanVideoTextEncode"},"widgets_values":["video of a wolf","色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走",true],"color":"#332922","bgcolor":"#593930"},{"id":30,"type":"VHS_VideoCombine","pos":[1709.29345703125,-334.3517150878906],"size":[1037.934326171875,856.9671630859375],"flags":{},"order":20,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":146},{"localized_name":"audio","name":"audio","shape":7,"type":"AUDIO","link":null},{"localized_name":"meta_batch","name":"meta_batch","shape":7,"type":"VHS_BatchManager","link":null},{"localized_name":"vae","name":"vae","shape":7,"type":"VAE","link":null},{"localized_name":"frame_rate","name":"frame_rate","type":"FLOAT","widget":{"name":"frame_rate"},"link":null},{"localized_name":"loop_count","name":"loop_count","type":"INT","widget":{"name":"loop_count"},"link":null},{"localized_name":"filename_prefix","name":"filename_prefix","type":"STRING","widget":{"name":"filename_prefix"},"link":null},{"localized_name":"format","name":"format","type":"COMBO","widget":{"name":"format"},"link":null},{"localized_name":"pingpong","name":"pingpong","type":"BOOLEAN","widget":{"name":"pingpong"},"link":null},{"localized_name":"save_output","name":"save_output","type":"BOOLEAN","widget":{"name":"save_output"},"link":null}],"outputs":[{"localized_name":"Filenames","name":"Filenames","type":"VHS_FILENAMES","links":null}],"properties":{"cnr_id":"comfyui-videohelpersuite","ver":"1.6.1","Node name for S&R":"VHS_VideoCombine"},"widgets_values":{"frame_rate":16,"loop_count":0,"filename_prefix":"WanVideoWrapper_I2V","format":"video/h264-mp4","pix_fmt":"yuv420p","crf":19,"save_metadata":true,"trim_to_audio":false,"pingpong":false,"save_output":true,"videopreview":{"hidden":false,"paused":false,"params":{"filename":"WanVideoWrapper_I2V_00002.mp4","subfolder":"","type":"output","format":"video/h264-mp4","frame_rate":16,"workflow":"WanVideoWrapper_I2V_00002.png","fullpath":"D:\\Workspace\\ComfyUI\\output\\WanVideoWrapper_I2V_00002.mp4"}}}},{"id":97,"type":"VHS_LoadVideo","pos":[-257.8260498046875,26.503103256225586],"size":[247.455078125,551.455078125],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"meta_batch","name":"meta_batch","shape":7,"type":"VHS_BatchManager","link":null},{"localized_name":"vae","name":"vae","shape":7,"type":"VAE","link":null},{"localized_name":"video","name":"video","type":"COMBO","widget":{"name":"video"},"link":null},{"localized_name":"force_rate","name":"force_rate","type":"FLOAT","widget":{"name":"force_rate"},"link":null},{"localized_name":"custom_width","name":"custom_width","type":"INT","widget":{"name":"custom_width"},"link":null},{"localized_name":"custom_height","name":"custom_height","type":"INT","widget":{"name":"custom_height"},"link":null},{"localized_name":"frame_load_cap","name":"frame_load_cap","type":"INT","widget":{"name":"frame_load_cap"},"link":null},{"localized_name":"skip_first_frames","name":"skip_first_frames","type":"INT","widget":{"name":"skip_first_frames"},"link":null},{"localized_name":"select_every_nth","name":"select_every_nth","type":"INT","widget":{"name":"select_every_nth"},"link":null},{"localized_name":"format","name":"format","shape":7,"type":"COMBO","widget":{"name":"format"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","slot_index":0,"links":[147]},{"localized_name":"frame_count","name":"frame_count","type":"INT","links":null},{"localized_name":"audio","name":"audio","type":"AUDIO","links":null},{"localized_name":"video_info","name":"video_info","type":"VHS_VIDEOINFO","links":null}],"properties":{"cnr_id":"comfyui-videohelpersuite","ver":"1.6.1","Node name for S&R":"VHS_LoadVideo"},"widgets_values":{"video":"wolf_interpolated.mp4","force_rate":0,"custom_width":0,"custom_height":0,"frame_load_cap":0,"skip_first_frames":0,"select_every_nth":1,"format":"AnimateDiff","choose video to upload":"image","videopreview":{"hidden":false,"paused":false,"params":{"filename":"wolf_interpolated.mp4","type":"input","format":"video/mp4","force_rate":0,"custom_width":0,"custom_height":0,"frame_load_cap":0,"skip_first_frames":0,"select_every_nth":1}}}},{"id":95,"type":"WanVideoEncode","pos":[280.9074401855469,-116.20671844482422],"size":[315,242],"flags":{},"order":15,"mode":0,"inputs":[{"localized_name":"vae","name":"vae","type":"WANVAE","link":135},{"localized_name":"image","name":"image","type":"IMAGE","link":148},{"localized_name":"mask","name":"mask","shape":7,"type":"MASK","link":null},{"localized_name":"enable_vae_tiling","name":"enable_vae_tiling","type":"BOOLEAN","widget":{"name":"enable_vae_tiling"},"link":null},{"localized_name":"tile_x","name":"tile_x","type":"INT","widget":{"name":"tile_x"},"link":null},{"localized_name":"tile_y","name":"tile_y","type":"INT","widget":{"name":"tile_y"},"link":null},{"localized_name":"tile_stride_x","name":"tile_stride_x","type":"INT","widget":{"name":"tile_stride_x"},"link":null},{"localized_name":"tile_stride_y","name":"tile_stride_y","type":"INT","widget":{"name":"tile_stride_y"},"link":null},{"localized_name":"noise_aug_strength","name":"noise_aug_strength","shape":7,"type":"FLOAT","widget":{"name":"noise_aug_strength"},"link":null},{"localized_name":"latent_strength","name":"latent_strength","shape":7,"type":"FLOAT","widget":{"name":"latent_strength"},"link":null}],"outputs":[{"localized_name":"samples","name":"samples","type":"LATENT","links":[132]}],"properties":{"cnr_id":"comfyui-wanvideowrapper","ver":"1.1.9","Node name for S&R":"WanVideoEncode"},"widgets_values":[false,272,272,144,128,0,1.0000000000000002]},{"id":104,"type":"ImageBlur","pos":[89.50601959228516,217.970947265625],"size":[315,82],"flags":{},"order":12,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":147},{"localized_name":"blur_radius","name":"blur_radius","type":"INT","widget":{"name":"blur_radius"},"link":null},{"localized_name":"sigma","name":"sigma","type":"FLOAT","widget":{"name":"sigma"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","slot_index":0,"links":[148,149]}],"properties":{"cnr_id":"comfy-core","ver":"0.3.43","Node name for S&R":"ImageBlur"},"widgets_values":[4,1]},{"id":103,"type":"ImageConcatMulti","pos":[2055.855224609375,-543.0326538085938],"size":[315,150],"flags":{},"order":19,"mode":0,"inputs":[{"localized_name":"image_1","name":"image_1","type":"IMAGE","link":149},{"localized_name":"image_2","name":"image_2","type":"IMAGE","link":145},{"localized_name":"inputcount","name":"inputcount","type":"INT","widget":{"name":"inputcount"},"link":null},{"localized_name":"direction","name":"direction","type":"COMBO","widget":{"name":"direction"},"link":null},{"localized_name":"match_image_size","name":"match_image_size","type":"BOOLEAN","widget":{"name":"match_image_size"},"link":null}],"outputs":[{"localized_name":"images","name":"images","type":"IMAGE","slot_index":0,"links":[146]}],"properties":{"cnr_id":"comfyui-kjnodes","ver":"1.1.2"},"widgets_values":[2,"right",false,null]},{"id":33,"type":"Note","pos":[170,-1150],"size":[359.0753479003906,88],"flags":{},"order":6,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["Models:\nhttps://huggingface.co/Kijai/WanVideo_comfy/tree/main"],"color":"#432","bgcolor":"#653"},{"id":96,"type":"WanVideoControlEmbeds","pos":[784.249755859375,-17.228534698486328],"size":[289.79998779296875,102],"flags":{},"order":16,"mode":0,"inputs":[{"localized_name":"latents","name":"latents","type":"LATENT","link":132},{"localized_name":"fun_ref_image","name":"fun_ref_image","shape":7,"type":"LATENT","link":null},{"localized_name":"start_percent","name":"start_percent","type":"FLOAT","widget":{"name":"start_percent"},"link":null},{"localized_name":"end_percent","name":"end_percent","type":"FLOAT","widget":{"name":"end_percent"},"link":null}],"outputs":[{"localized_name":"image_embeds","name":"image_embeds","type":"WANVIDIMAGE_EMBEDS","slot_index":0,"links":[133]}],"properties":{"cnr_id":"comfyui-wanvideowrapper","ver":"1.1.9","Node name for S&R":"WanVideoControlEmbeds"},"widgets_values":[0,0.7]},{"id":27,"type":"WanVideoSampler","pos":[1335.9598388671875,-353.7341003417969],"size":[315,699],"flags":{},"order":17,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"WANVIDEOMODEL","link":29},{"localized_name":"text_embeds","name":"text_embeds","type":"WANVIDEOTEXTEMBEDS","link":30},{"localized_name":"image_embeds","name":"image_embeds","type":"WANVIDIMAGE_EMBEDS","link":133},{"localized_name":"samples","name":"samples","shape":7,"type":"LATENT","link":null},{"localized_name":"feta_args","name":"feta_args","shape":7,"type":"FETAARGS","link":null},{"localized_name":"context_options","name":"context_options","shape":7,"type":"WANVIDCONTEXT","link":null},{"localized_name":"teacache_args","name":"teacache_args","shape":7,"type":"TEACACHEARGS","link":103},{"localized_name":"flowedit_args","name":"flowedit_args","shape":7,"type":"FLOWEDITARGS","link":null},{"localized_name":"slg_args","name":"slg_args","shape":7,"type":"SLGARGS","link":null},{"localized_name":"loop_args","name":"loop_args","shape":7,"type":"LOOPARGS","link":null},{"localized_name":"experimental_args","name":"experimental_args","shape":7,"type":"EXPERIMENTALARGS","link":null},{"localized_name":"sigmas","name":"sigmas","shape":7,"type":"SIGMAS","link":null},{"localized_name":"unianimate_poses","name":"unianimate_poses","shape":7,"type":"UNIANIMATE_POSE","link":null},{"localized_name":"fantasytalking_embeds","name":"fantasytalking_embeds","shape":7,"type":"FANTASYTALKING_EMBEDS","link":null},{"localized_name":"uni3c_embeds","name":"uni3c_embeds","shape":7,"type":"UNI3C_EMBEDS","link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"shift","name":"shift","type":"FLOAT","widget":{"name":"shift"},"link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"force_offload","name":"force_offload","type":"BOOLEAN","widget":{"name":"force_offload"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"riflex_freq_index","name":"riflex_freq_index","type":"INT","widget":{"name":"riflex_freq_index"},"link":null},{"localized_name":"denoise_strength","name":"denoise_strength","shape":7,"type":"FLOAT","widget":{"name":"denoise_strength"},"link":null},{"localized_name":"batched_cfg","name":"batched_cfg","shape":7,"type":"BOOLEAN","widget":{"name":"batched_cfg"},"link":null},{"localized_name":"rope_function","name":"rope_function","shape":7,"type":"COMBO","widget":{"name":"rope_function"},"link":null}],"outputs":[{"localized_name":"samples","name":"samples","type":"LATENT","slot_index":0,"links":[33]},{"name":"maskimagetest","type":"IMAGE","slot_index":1,"links":[]}],"properties":{"cnr_id":"comfyui-wanvideowrapper","ver":"1.1.9","Node name for S&R":"WanVideoSampler"},"widgets_values":[30,6,5,0,"fixed",true,"unipc",0,1,"","comfy"]},{"id":64,"type":"WanVideoTorchCompileSettings","pos":[-276.8500671386719,-1050.6326904296875],"size":[390.5999755859375,202],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"backend","name":"backend","type":"COMBO","widget":{"name":"backend"},"link":null},{"localized_name":"fullgraph","name":"fullgraph","type":"BOOLEAN","widget":{"name":"fullgraph"},"link":null},{"localized_name":"mode","name":"mode","type":"COMBO","widget":{"name":"mode"},"link":null},{"localized_name":"dynamic","name":"dynamic","type":"BOOLEAN","widget":{"name":"dynamic"},"link":null},{"localized_name":"dynamo_cache_size_limit","name":"dynamo_cache_size_limit","type":"INT","widget":{"name":"dynamo_cache_size_limit"},"link":null},{"localized_name":"compile_transformer_blocks_only","name":"compile_transformer_blocks_only","type":"BOOLEAN","widget":{"name":"compile_transformer_blocks_only"},"link":null},{"localized_name":"dynamo_recompile_limit","name":"dynamo_recompile_limit","shape":7,"type":"INT","widget":{"name":"dynamo_recompile_limit"},"link":null}],"outputs":[{"localized_name":"torch_compile_args","name":"torch_compile_args","type":"WANCOMPILEARGS","slot_index":0,"links":[]}],"properties":{"cnr_id":"comfyui-wanvideowrapper","ver":"1.1.9","Node name for S&R":"WanVideoTorchCompileSettings"},"widgets_values":["inductor",false,"default",false,64,true,128]},{"id":38,"type":"WanVideoVAELoader","pos":[-253.87718200683594,-160.96878051757812],"size":[372.7727966308594,82],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"model_name","name":"model_name","type":"COMBO","widget":{"name":"model_name"},"link":null},{"localized_name":"precision","name":"precision","shape":7,"type":"COMBO","widget":{"name":"precision"},"link":null}],"outputs":[{"localized_name":"vae","name":"vae","type":"WANVAE","slot_index":0,"links":[43,135]}],"properties":{"cnr_id":"comfyui-wanvideowrapper","ver":"1.1.9","Node name for S&R":"WanVideoVAELoader"},"widgets_values":["wan_2.1_vae.safetensors","bf16"],"color":"#322","bgcolor":"#533"},{"id":11,"type":"LoadWanVideoT5TextEncoder","pos":[171.43104553222656,-437.4266357421875],"size":[377.1661376953125,130],"flags":{},"order":9,"mode":0,"inputs":[{"localized_name":"model_name","name":"model_name","type":"COMBO","widget":{"name":"model_name"},"link":null},{"localized_name":"precision","name":"precision","type":"COMBO","widget":{"name":"precision"},"link":null},{"localized_name":"load_device","name":"load_device","shape":7,"type":"COMBO","widget":{"name":"load_device"},"link":null},{"localized_name":"quantization","name":"quantization","shape":7,"type":"COMBO","widget":{"name":"quantization"},"link":null}],"outputs":[{"localized_name":"wan_t5_model","name":"wan_t5_model","type":"WANTEXTENCODER","slot_index":0,"links":[15]}],"properties":{"cnr_id":"comfyui-wanvideowrapper","ver":"1.1.9","Node name for S&R":"LoadWanVideoT5TextEncoder"},"widgets_values":["umt5-xxl-enc-bf16.safetensors","bf16","offload_device","disabled"],"color":"#332922","bgcolor":"#593930"},{"id":106,"type":"MarkdownNote","pos":[-225.10594177246094,-354.0140075683594],"size":[358.3768310546875,88],"flags":{},"order":10,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["[https://huggingface.co/spacepxl/Wan2.1-control-loras/blob/main/wan2.1-1.3b-control-lora-tile-v0.1_comfy.safetensors](https://huggingface.co/spacepxl/Wan2.1-control-loras/blob/main/wan2.1-1.3b-control-lora-tile-v0.1_comfy.safetensors)"],"color":"#432","bgcolor":"#653"},{"id":22,"type":"WanVideoModelLoader","pos":[148.01803588867188,-780],"size":[477.4410095214844,254],"flags":{},"order":14,"mode":0,"inputs":[{"localized_name":"compile_args","name":"compile_args","shape":7,"type":"WANCOMPILEARGS","link":null},{"localized_name":"block_swap_args","name":"block_swap_args","shape":7,"type":"BLOCKSWAPARGS","link":null},{"localized_name":"lora","name":"lora","shape":7,"type":"WANVIDLORA","link":151},{"localized_name":"vram_management_args","name":"vram_management_args","shape":7,"type":"VRAM_MANAGEMENTARGS","link":null},{"localized_name":"vace_model","name":"vace_model","shape":7,"type":"VACEPATH","link":null},{"localized_name":"fantasytalking_model","name":"fantasytalking_model","shape":7,"type":"FANTASYTALKINGMODEL","link":null},{"localized_name":"model","name":"model","type":"COMBO","widget":{"name":"model"},"link":null},{"localized_name":"base_precision","name":"base_precision","type":"COMBO","widget":{"name":"base_precision"},"link":null},{"localized_name":"quantization","name":"quantization","type":"COMBO","widget":{"name":"quantization"},"link":null},{"localized_name":"load_device","name":"load_device","type":"COMBO","widget":{"name":"load_device"},"link":null},{"localized_name":"attention_mode","name":"attention_mode","shape":7,"type":"COMBO","widget":{"name":"attention_mode"},"link":null}],"outputs":[{"localized_name":"model","name":"model","type":"WANVIDEOMODEL","slot_index":0,"links":[29]}],"properties":{"cnr_id":"comfyui-wanvideowrapper","ver":"1.1.9","Node name for S&R":"WanVideoModelLoader"},"widgets_values":["wan2.1_t2v_1.3B_fp16.safetensors","fp16","disabled","offload_device","sdpa"],"color":"#223","bgcolor":"#335"},{"id":107,"type":"WanVideo Lora Select (LoraManager)","pos":[-296.61053466796875,-778.7354736328125],"size":[396.30816650390625,356],"flags":{},"order":11,"mode":0,"inputs":[{"localized_name":"low_mem_load","name":"low_mem_load","type":"BOOLEAN","widget":{"name":"low_mem_load"},"link":null},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null},{"name":"prev_lora","shape":7,"type":"WANVIDLORA","link":null},{"name":"blocks","shape":7,"type":"SELECTEDBLOCKS","link":null}],"outputs":[{"localized_name":"lora","name":"lora","type":"WANVIDLORA","links":[151]},{"localized_name":"trigger_words","name":"trigger_words","type":"STRING","links":null},{"localized_name":"active_loras","name":"active_loras","type":"STRING","links":null}],"properties":{"cnr_id":"comfyui-lora-manager","ver":"71762d788f45efb012391e538c1240275a190384","Node name for S&R":"WanVideo Lora Select (LoraManager)"},"widgets_values":[false,"",[{"name":"wan2.1-1.3b-control-lora-tile-v1.0_comfy","strength":1,"active":true,"clipStrength":1,"expanded":false}]],"color":"#233","bgcolor":"#355"}],"links":[[15,11,0,16,0,"WANTEXTENCODER"],[29,22,0,27,0,"WANVIDEOMODEL"],[30,16,0,27,1,"WANVIDEOTEXTEMBEDS"],[33,27,0,28,1,"LATENT"],[43,38,0,28,0,"VAE"],[103,52,0,27,6,"TEACACHEARGS"],[132,95,0,96,0,"LATENT"],[133,96,0,27,2,"WANVIDIMAGE_EMBEDS"],[135,38,0,95,0,"WANVAE"],[145,28,0,103,1,"IMAGE"],[146,103,0,30,0,"IMAGE"],[147,97,0,104,0,"IMAGE"],[148,104,0,95,1,"IMAGE"],[149,104,0,103,0,"IMAGE"],[151,107,0,22,2,"WANVIDLORA"]],"groups":[],"config":{},"extra":{"ds":{"scale":1.3109994191500804,"offset":[525.569626004449,848.5221516412072]},"node_versions":{"ComfyUI-WanVideoWrapper":"5a2383621a05825d0d0437781afcb8552d9590fd","ComfyUI-VideoHelperSuite":"0a75c7958fe320efcb052f1d9f8451fd20c730a8","comfy-core":"0.3.26","ComfyUI-KJNodes":"a5bd3c86c8ed6b83c55c2d0e7a59515b15a0137f"},"VHS_latentpreview":true,"VHS_latentpreviewrate":0,"VHS_MetadataImage":true,"VHS_KeepIntermediate":true},"version":0.4} \ No newline at end of file diff --git a/py/nodes/lora_stacker.py b/py/nodes/lora_stacker.py index 9d87e13d..50a8c510 100644 --- a/py/nodes/lora_stacker.py +++ b/py/nodes/lora_stacker.py @@ -1,6 +1,4 @@ from comfy.comfy_types import IO # type: ignore -from ..services.lora_scanner import LoraScanner -from ..config import config import asyncio import os from .utils import FlexibleOptionalInputType, any_type, get_lora_info, extract_lora_name, get_loras_list diff --git a/py/nodes/wanvideo_lora_select.py b/py/nodes/wanvideo_lora_select.py new file mode 100644 index 00000000..056ed2da --- /dev/null +++ b/py/nodes/wanvideo_lora_select.py @@ -0,0 +1,92 @@ +from comfy.comfy_types import IO # type: ignore +import asyncio +import folder_paths # type: ignore +from .utils import FlexibleOptionalInputType, any_type, get_lora_info, extract_lora_name, get_loras_list +import logging + +logger = logging.getLogger(__name__) + +class WanVideoLoraSelect: + NAME = "WanVideo Lora Select (LoraManager)" + CATEGORY = "Lora Manager/stackers" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "low_mem_load": ("BOOLEAN", {"default": False, "tooltip": "Load the LORA model with less VRAM usage, slower loading"}), + "text": (IO.STRING, { + "multiline": True, + "dynamicPrompts": True, + "tooltip": "Format: separated by spaces or punctuation", + "placeholder": "LoRA syntax input: " + }), + }, + "optional": FlexibleOptionalInputType(any_type), + } + + RETURN_TYPES = ("WANVIDLORA", IO.STRING, IO.STRING) + RETURN_NAMES = ("lora", "trigger_words", "active_loras") + FUNCTION = "process_loras" + + def process_loras(self, text, low_mem_load=False, **kwargs): + loras_list = [] + all_trigger_words = [] + active_loras = [] + + # Process existing prev_lora if available + prev_lora = kwargs.get('prev_lora', None) + if prev_lora is not None: + loras_list.extend(prev_lora) + + # Get blocks if available + blocks = kwargs.get('blocks', {}) + selected_blocks = blocks.get("selected_blocks", {}) + layer_filter = blocks.get("layer_filter", "") + + # Process loras from kwargs with support for both old and new formats + loras_from_widget = get_loras_list(kwargs) + for lora in loras_from_widget: + if not lora.get('active', False): + continue + + lora_name = lora['name'] + model_strength = float(lora['strength']) + clip_strength = float(lora.get('clipStrength', model_strength)) + + # Get lora path and trigger words + lora_path, trigger_words = asyncio.run(get_lora_info(lora_name)) + + # Create lora item for WanVideo format + lora_item = { + "path": folder_paths.get_full_path("loras", lora_path), + "strength": model_strength, + "name": lora_path.split(".")[0], + "blocks": selected_blocks, + "layer_filter": layer_filter, + "low_mem_load": low_mem_load, + } + + # Add to list and collect active loras + loras_list.append(lora_item) + active_loras.append((lora_name, model_strength, clip_strength)) + + # Add trigger words to collection + all_trigger_words.extend(trigger_words) + + # Format trigger_words for output + trigger_words_text = ",, ".join(all_trigger_words) if all_trigger_words else "" + + # Format active_loras for output + formatted_loras = [] + for name, model_strength, clip_strength in active_loras: + if abs(model_strength - clip_strength) > 0.001: + # Different model and clip strengths + formatted_loras.append(f"") + else: + # Same strength for both + formatted_loras.append(f"") + + active_loras_text = " ".join(formatted_loras) + + return (loras_list, trigger_words_text, active_loras_text) diff --git a/py/utils/constants.py b/py/utils/constants.py index f5b2820e..955dc227 100644 --- a/py/utils/constants.py +++ b/py/utils/constants.py @@ -10,7 +10,8 @@ NSFW_LEVELS = { # Node type constants NODE_TYPES = { "Lora Loader (LoraManager)": 1, - "Lora Stacker (LoraManager)": 2 + "Lora Stacker (LoraManager)": 2, + "WanVideo Lora Select (LoraManager)": 3 } # Default ComfyUI node color when bgcolor is null diff --git a/static/js/utils/constants.js b/static/js/utils/constants.js index 63d3997b..7b84b61a 100644 --- a/static/js/utils/constants.js +++ b/static/js/utils/constants.js @@ -108,19 +108,22 @@ export const NSFW_LEVELS = { // Node type constants export const NODE_TYPES = { LORA_LOADER: 1, - LORA_STACKER: 2 + LORA_STACKER: 2, + WAN_VIDEO_LORA_SELECT: 3 }; // Node type names to IDs mapping export const NODE_TYPE_NAMES = { "Lora Loader (LoraManager)": NODE_TYPES.LORA_LOADER, - "Lora Stacker (LoraManager)": NODE_TYPES.LORA_STACKER + "Lora Stacker (LoraManager)": NODE_TYPES.LORA_STACKER, + "WanVideo Lora Select (LoraManager)": NODE_TYPES.WAN_VIDEO_LORA_SELECT }; // Node type icons export const NODE_TYPE_ICONS = { [NODE_TYPES.LORA_LOADER]: "fas fa-l", - [NODE_TYPES.LORA_STACKER]: "fas fa-s" + [NODE_TYPES.LORA_STACKER]: "fas fa-s", + [NODE_TYPES.WAN_VIDEO_LORA_SELECT]: "fas fa-w" }; // Default ComfyUI node color when bgcolor is null diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index a9ba3335..5dc2982d 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -337,7 +337,7 @@ export async function sendLoraToWorkflow(loraSyntax, replaceMode = false, syntax // Success case - check node count if (registryData.data.node_count === 0) { // No nodes found - show warning - showToast('No Lora Loader or Lora Stacker nodes found in workflow', 'warning'); + showToast('No supported target nodes found in workflow', 'warning'); return false; } else if (registryData.data.node_count > 1) { // Multiple nodes - show selector diff --git a/web/comfyui/lora_loader.js b/web/comfyui/lora_loader.js index cd8fcb95..a5e40c41 100644 --- a/web/comfyui/lora_loader.js +++ b/web/comfyui/lora_loader.js @@ -76,7 +76,9 @@ app.registerExtension({ // Standard mode - update a specific node const node = app.graph.getNodeById(+id); - if (!node || (node.comfyClass !== "Lora Loader (LoraManager)" && node.comfyClass !== "Lora Stacker (LoraManager)")) { + if (!node || (node.comfyClass !== "Lora Loader (LoraManager)" && + node.comfyClass !== "Lora Stacker (LoraManager)" && + node.comfyClass !== "WanVideo Lora Select (LoraManager)")) { console.warn("Node not found or not a LoraLoader:", id); return; } @@ -87,7 +89,7 @@ app.registerExtension({ // Helper method to update a single node's lora code updateNodeLoraCode(node, loraCode, mode) { // Update the input widget with new lora code - const inputWidget = node.widgets[0]; + const inputWidget = node.inputWidget; if (!inputWidget) return; // Get the current lora code @@ -182,6 +184,7 @@ app.registerExtension({ // Update input widget callback const inputWidget = this.widgets[0]; + this.inputWidget = inputWidget; inputWidget.callback = (value) => { if (isUpdating) return; isUpdating = true; diff --git a/web/comfyui/lora_stacker.js b/web/comfyui/lora_stacker.js index 38c01799..81d2338e 100644 --- a/web/comfyui/lora_stacker.js +++ b/web/comfyui/lora_stacker.js @@ -105,6 +105,7 @@ app.registerExtension({ // Update input widget callback const inputWidget = this.widgets[0]; + this.inputWidget = inputWidget; inputWidget.callback = (value) => { if (isUpdating) return; isUpdating = true; diff --git a/web/comfyui/usage_stats.js b/web/comfyui/usage_stats.js index 6babd34c..cce7011b 100644 --- a/web/comfyui/usage_stats.js +++ b/web/comfyui/usage_stats.js @@ -52,7 +52,9 @@ app.registerExtension({ // Find all Lora nodes const loraNodes = []; for (const node of workflow.nodes.values()) { - if (node.type === "Lora Loader (LoraManager)" || node.type === "Lora Stacker (LoraManager)") { + if (node.type === "Lora Loader (LoraManager)" || + node.type === "Lora Stacker (LoraManager)" || + node.type === "WanVideo Lora Select (LoraManager)") { loraNodes.push({ node_id: node.id, bgcolor: node.bgcolor || null, diff --git a/web/comfyui/wanvideo_lora_select.js b/web/comfyui/wanvideo_lora_select.js new file mode 100644 index 00000000..144530de --- /dev/null +++ b/web/comfyui/wanvideo_lora_select.js @@ -0,0 +1,131 @@ +import { app } from "../../scripts/app.js"; +import { + LORA_PATTERN, + getActiveLorasFromNode, + collectActiveLorasFromChain, + updateConnectedTriggerWords, + chainCallback +} from "./utils.js"; +import { addLorasWidget } from "./loras_widget.js"; + +function mergeLoras(lorasText, lorasArr) { + const result = []; + let match; + + // Reset pattern index before using + LORA_PATTERN.lastIndex = 0; + + // Parse text input and create initial entries + while ((match = LORA_PATTERN.exec(lorasText)) !== null) { + const name = match[1]; + const modelStrength = Number(match[2]); + // Extract clip strength if provided, otherwise use model strength + const clipStrength = match[3] ? Number(match[3]) : modelStrength; + + // Find if this lora exists in the array data + const existingLora = lorasArr.find(l => l.name === name); + + result.push({ + name: name, + // Use existing strength if available, otherwise use input strength + strength: existingLora ? existingLora.strength : modelStrength, + active: existingLora ? existingLora.active : true, + clipStrength: existingLora ? existingLora.clipStrength : clipStrength, + }); + } + + return result; +} + +app.registerExtension({ + name: "LoraManager.WanVideoLoraSelect", + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeType.comfyClass === "WanVideo Lora Select (LoraManager)") { + chainCallback(nodeType.prototype, "onNodeCreated", async function() { + // Enable widget serialization + this.serialize_widgets = true; + + // Add optional inputs + this.addInput("prev_lora", 'WANVIDLORA', { + "shape": 7 // 7 is the shape of the optional input + }); + + this.addInput("blocks", 'SELECTEDBLOCKS', { + "shape": 7 // 7 is the shape of the optional input + }); + + // Restore saved value if exists + let existingLoras = []; + if (this.widgets_values && this.widgets_values.length > 0) { + // 0 for low_mem_load, 1 for text widget, 2 for loras widget + const savedValue = this.widgets_values[2]; + existingLoras = savedValue || []; + } + // Merge the loras data + const mergedLoras = mergeLoras(this.widgets[1].value, existingLoras); + + // Add flag to prevent callback loops + let isUpdating = false; + + const result = addLorasWidget(this, "loras", { + defaultVal: mergedLoras // Pass object directly + }, (value) => { + // Prevent recursive calls + if (isUpdating) return; + isUpdating = true; + + try { + // Remove loras that are not in the value array + const inputWidget = this.widgets[1]; + const currentLoras = value.map(l => l.name); + + // Use the constant pattern here as well + let newText = inputWidget.value.replace(LORA_PATTERN, (match, name, strength) => { + return currentLoras.includes(name) ? match : ''; + }); + + // Clean up multiple spaces and trim + newText = newText.replace(/\s+/g, ' ').trim(); + + inputWidget.value = newText; + + // Update this node's direct trigger toggles with its own active loras + const activeLoraNames = new Set(); + value.forEach(lora => { + if (lora.active) { + activeLoraNames.add(lora.name); + } + }); + updateConnectedTriggerWords(this, activeLoraNames); + } finally { + isUpdating = false; + } + }); + + this.lorasWidget = result.widget; + + // Update input widget callback + const inputWidget = this.widgets[1]; + this.inputWidget = inputWidget; + inputWidget.callback = (value) => { + if (isUpdating) return; + isUpdating = true; + + try { + const currentLoras = this.lorasWidget.value || []; + const mergedLoras = mergeLoras(value, currentLoras); + + this.lorasWidget.value = mergedLoras; + + // Update this node's direct trigger toggles with its own active loras + const activeLoraNames = getActiveLorasFromNode(this); + updateConnectedTriggerWords(this, activeLoraNames); + } finally { + isUpdating = false; + } + }; + }); + } + }, +});