commit 026ffa3b47d7acf90727b757ba6e3209202177cf Author: dseditor Date: Sat Jun 21 16:47:32 2025 +0800 Initial commit for ComfyUI-ListHelper diff --git a/Example/AudioListExample.json b/Example/AudioListExample.json new file mode 100644 index 0000000..51aec4e --- /dev/null +++ b/Example/AudioListExample.json @@ -0,0 +1,247 @@ +{ + "id": "1d9dc8ae-713b-4667-b27f-c57cd7e60f83", + "revision": 0, + "last_node_id": 5, + "last_link_id": 4, + "nodes": [ + { + "id": 1, + "type": "ImpactMakeAnyList", + "pos": [ + -950, + -500 + ], + "size": [ + 140, + 66 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [ + { + "name": "value1", + "shape": 7, + "type": "AUDIO", + "link": 2 + }, + { + "name": "value2", + "type": "AUDIO", + "link": 3 + }, + { + "name": "value3", + "type": "AUDIO", + "link": null + } + ], + "outputs": [ + { + "label": "AUDIO", + "name": "AUDIO", + "shape": 6, + "type": "AUDIO", + "links": [ + 1 + ] + } + ], + "properties": { + "cnr_id": "comfyui-impact-pack", + "ver": "2346b677666e14ad53a6e65e16a33289a78106c7", + "Node name for S&R": "ImpactMakeAnyList" + } + }, + { + "id": 2, + "type": "AudioListCombine", + "pos": [ + -780, + -540 + ], + "size": [ + 270, + 130 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "audio_list", + "type": "AUDIO", + "link": 1 + } + ], + "outputs": [ + { + "name": "AUDIO", + "type": "AUDIO", + "links": [ + 4 + ] + } + ], + "properties": { + "Node name for S&R": "AudioListCombine" + }, + "widgets_values": [ + "concatenate", + 0, + true, + 44100 + ] + }, + { + "id": 3, + "type": "LoadAudio", + "pos": [ + -1260, + -640 + ], + "size": [ + 274.080078125, + 136 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "AUDIO", + "type": "AUDIO", + "links": [ + 2 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.41", + "Node name for S&R": "LoadAudio" + }, + "widgets_values": [ + "normalsoft.flac", + null, + "" + ] + }, + { + "id": 4, + "type": "LoadAudio", + "pos": [ + -1260, + -380 + ], + "size": [ + 274.080078125, + 136 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "AUDIO", + "type": "AUDIO", + "links": [ + 3 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.41", + "Node name for S&R": "LoadAudio" + }, + "widgets_values": [ + "ComfyUI_00071_.flac", + null, + "" + ] + }, + { + "id": 5, + "type": "PreviewAudio", + "pos": [ + -490, + -540 + ], + "size": [ + 270, + 88 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "AUDIO", + "link": 4 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.41", + "Node name for S&R": "PreviewAudio" + }, + "widgets_values": [] + } + ], + "links": [ + [ + 1, + 1, + 0, + 2, + 0, + "AUDIO" + ], + [ + 2, + 3, + 0, + 1, + 0, + "AUDIO" + ], + [ + 3, + 4, + 0, + 1, + 1, + "AUDIO" + ], + [ + 4, + 2, + 0, + 5, + 0, + "AUDIO" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 1.1671841070450026, + "offset": [ + 1408.156109060273, + 720.1126698569929 + ] + }, + "frontendVersion": "1.21.7", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file diff --git a/Example/FanstasyTalkingLoopTest.json b/Example/FanstasyTalkingLoopTest.json new file mode 100644 index 0000000..9fb3ec6 --- /dev/null +++ b/Example/FanstasyTalkingLoopTest.json @@ -0,0 +1,2023 @@ +{ + "id": "206247b6-9fec-4ed2-8927-e4f388c674d4", + "revision": 0, + "last_node_id": 123, + "last_link_id": 219, + "nodes": [ + { + "id": 120, + "type": "easy batchAnything", + "pos": [ + -270, + 410 + ], + "size": [ + 140, + 46 + ], + "flags": {}, + "order": 27, + "mode": 0, + "inputs": [ + { + "name": "any_1", + "type": "*", + "link": 202 + }, + { + "name": "any_2", + "type": "*", + "link": 203 + } + ], + "outputs": [ + { + "name": "batch", + "type": "*", + "links": [ + 200 + ] + } + ], + "properties": { + "cnr_id": "comfyui-easy-use", + "ver": "71c7865d2d3c934ccb99f24171e08ae5a81148ac", + "Node name for S&R": "easy batchAnything" + }, + "widgets_values": [] + }, + { + "id": 84, + "type": "SetNode", + "pos": [ + -260, + 520 + ], + "size": [ + 210, + 60 + ], + "flags": { + "collapsed": true + }, + "order": 21, + "mode": 0, + "inputs": [ + { + "name": "*", + "type": "*", + "link": 205 + } + ], + "outputs": [ + { + "name": "*", + "type": "*", + "links": [ + 121 + ] + } + ], + "title": "Set_InputAudio", + "properties": { + "previousName": "InputAudio" + }, + "widgets_values": [ + "InputAudio" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 121, + "type": "easy forLoopEnd", + "pos": [ + -60, + 440 + ], + "size": [ + 163.08984375, + 86 + ], + "flags": {}, + "order": 28, + "mode": 0, + "inputs": [ + { + "name": "flow", + "shape": 5, + "type": "FLOW_CONTROL", + "link": 199 + }, + { + "name": "initial_value1", + "shape": 7, + "type": "*", + "link": 198 + }, + { + "name": "initial_value2", + "type": "*", + "link": 200 + }, + { + "name": "initial_value3", + "type": "*", + "link": null + } + ], + "outputs": [ + { + "name": "value1", + "type": "*", + "links": [] + }, + { + "name": "value2", + "type": "*", + "links": [ + 208 + ] + }, + { + "name": "value3", + "type": "*", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-easy-use", + "ver": "71c7865d2d3c934ccb99f24171e08ae5a81148ac", + "Node name for S&R": "easy forLoopEnd" + }, + "widgets_values": [], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 119, + "type": "Reroute", + "pos": [ + -1090, + 10 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 14, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 193 + } + ], + "outputs": [ + { + "name": "", + "type": "IMAGE", + "links": [ + 194 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 110, + "type": "easy forLoopStart", + "pos": [ + -890, + 330 + ], + "size": [ + 270, + 138 + ], + "flags": {}, + "order": 18, + "mode": 0, + "inputs": [ + { + "name": "initial_value1", + "shape": 7, + "type": "*", + "link": 195 + }, + { + "name": "total", + "type": "INT", + "widget": { + "name": "total" + }, + "link": 215 + }, + { + "name": "initial_value2", + "type": "*", + "link": null + }, + { + "name": "initial_value3", + "type": "*", + "link": null + } + ], + "outputs": [ + { + "name": "flow", + "shape": 5, + "type": "FLOW_CONTROL", + "links": [ + 199 + ] + }, + { + "name": "index", + "type": "INT", + "links": [ + 206 + ] + }, + { + "name": "value1", + "type": "*", + "links": [ + 196, + 197 + ] + }, + { + "name": "value2", + "type": "*", + "links": [ + 203 + ] + }, + { + "name": "value3", + "type": "*", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-easy-use", + "ver": "71c7865d2d3c934ccb99f24171e08ae5a81148ac", + "Node name for S&R": "easy forLoopStart" + }, + "widgets_values": [ + 3 + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 59, + "type": "CLIPVisionLoader", + "pos": [ + -940, + -220 + ], + "size": [ + 315, + 58 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "CLIP_VISION", + "type": "CLIP_VISION", + "links": [ + 70 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.26", + "Node name for S&R": "CLIPVisionLoader" + }, + "widgets_values": [ + "clip_vision_h.safetensors" + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 74, + "type": "ImageResizeKJv2", + "pos": [ + -950, + -100 + ], + "size": [ + 315, + 266 + ], + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 194 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 195 + ] + }, + { + "name": "width", + "type": "INT", + "links": [ + 109 + ] + }, + { + "name": "height", + "type": "INT", + "links": [ + 110 + ] + } + ], + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "c3dc82108a2a86c17094107ead61d63f8c76200e", + "Node name for S&R": "ImageResizeKJv2" + }, + "widgets_values": [ + 512, + 512, + "lanczos", + "crop", + "0, 0, 0", + "center", + 2, + "cpu" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 65, + "type": "WanVideoClipVisionEncode", + "pos": [ + -570, + -210 + ], + "size": [ + 327.5999755859375, + 262 + ], + "flags": {}, + "order": 20, + "mode": 0, + "inputs": [ + { + "name": "clip_vision", + "type": "CLIP_VISION", + "link": 70 + }, + { + "name": "image_1", + "type": "IMAGE", + "link": 196 + }, + { + "name": "image_2", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "name": "negative_image", + "shape": 7, + "type": "IMAGE", + "link": null + } + ], + "outputs": [ + { + "name": "image_embeds", + "type": "WANVIDIMAGE_CLIPEMBEDS", + "links": [ + 82 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoClipVisionEncode" + }, + "widgets_values": [ + 1, + 1, + "center", + "average", + true, + 0, + 0.20000000000000004 + ], + "color": "#233", + "bgcolor": "#355" + }, + { + "id": 63, + "type": "WanVideoImageToVideoEncode", + "pos": [ + -200, + -160 + ], + "size": [ + 352.79998779296875, + 390 + ], + "flags": {}, + "order": 22, + "mode": 0, + "inputs": [ + { + "name": "vae", + "type": "WANVAE", + "link": 219 + }, + { + "name": "clip_embeds", + "shape": 7, + "type": "WANVIDIMAGE_CLIPEMBEDS", + "link": 82 + }, + { + "name": "start_image", + "shape": 7, + "type": "IMAGE", + "link": 197 + }, + { + "name": "end_image", + "shape": 7, + "type": "IMAGE", + "link": null + }, + { + "name": "control_embeds", + "shape": 7, + "type": "WANVIDIMAGE_EMBEDS", + "link": null + }, + { + "name": "temporal_mask", + "shape": 7, + "type": "MASK", + "link": null + }, + { + "name": "extra_latents", + "shape": 7, + "type": "LATENT", + "link": null + }, + { + "name": "realisdance_latents", + "shape": 7, + "type": "REALISDANCELATENTS", + "link": null + }, + { + "name": "width", + "type": "INT", + "widget": { + "name": "width" + }, + "link": 109 + }, + { + "name": "height", + "type": "INT", + "widget": { + "name": "height" + }, + "link": 110 + }, + { + "name": "num_frames", + "type": "INT", + "widget": { + "name": "num_frames" + }, + "link": 111 + } + ], + "outputs": [ + { + "name": "image_embeds", + "type": "WANVIDIMAGE_EMBEDS", + "links": [ + 87 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoImageToVideoEncode" + }, + "widgets_values": [ + 832, + 480, + 81, + 0.030000000000000006, + 1, + 0.9440000000000002, + true, + false, + false + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 36, + "type": "Note", + "pos": [ + 140, + -1390 + ], + "size": [ + 374.3061828613281, + 171.9547576904297 + ], + "flags": {}, + "order": 1, + "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": 122, + "type": "easy indexAnything", + "pos": [ + -540, + 520 + ], + "size": [ + 270, + 58 + ], + "flags": {}, + "order": 19, + "mode": 0, + "inputs": [ + { + "name": "any", + "type": "*", + "link": 216 + }, + { + "name": "index", + "type": "INT", + "widget": { + "name": "index" + }, + "link": 206 + } + ], + "outputs": [ + { + "name": "out", + "type": "*", + "links": [ + 205 + ] + } + ], + "properties": { + "cnr_id": "comfyui-easy-use", + "ver": "71c7865d2d3c934ccb99f24171e08ae5a81148ac", + "Node name for S&R": "easy indexAnything" + }, + "widgets_values": [ + 0 + ] + }, + { + "id": 123, + "type": "AudioListGenerator", + "pos": [ + -870, + -530 + ], + "size": [ + 270, + 126 + ], + "flags": {}, + "order": 15, + "mode": 0, + "inputs": [ + { + "name": "waveform", + "type": "AUDIO", + "link": 214 + }, + { + "name": "videofps", + "type": "FLOAT", + "widget": { + "name": "videofps" + }, + "link": 213 + }, + { + "name": "samplefps", + "type": "INT", + "widget": { + "name": "samplefps" + }, + "link": 217 + } + ], + "outputs": [ + { + "name": "cycle", + "type": "INT", + "links": [ + 215 + ] + }, + { + "name": "audio_list", + "shape": 6, + "type": "AUDIO", + "links": [ + 216 + ] + } + ], + "properties": { + "Node name for S&R": "AudioListGenerator" + }, + "widgets_values": [ + 23.976, + 81, + false + ] + }, + { + "id": 112, + "type": "ImageFromBatch+", + "pos": [ + -870, + 520 + ], + "size": [ + 270, + 82 + ], + "flags": {}, + "order": 26, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 178 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 198 + ] + } + ], + "properties": { + "cnr_id": "comfyui_essentials", + "ver": "33ff89fd354d8ec3ab6affb605a79a931b445d99", + "Node name for S&R": "ImageFromBatch+" + }, + "widgets_values": [ + 64, + 1 + ] + }, + { + "id": 71, + "type": "DownloadAndLoadWav2VecModel", + "pos": [ + -520, + -580 + ], + "size": [ + 355.20001220703125, + 106 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "wav2vec_model", + "type": "WAV2VECMODEL", + "links": [ + 99 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "df95c85283d7625fbdf664d0133a2e1c114ba14a", + "Node name for S&R": "DownloadAndLoadWav2VecModel" + }, + "widgets_values": [ + "facebook/wav2vec2-base-960h", + "fp16", + "main_device" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 68, + "type": "FantasyTalkingModelLoader", + "pos": [ + -510, + -420 + ], + "size": [ + 340.20001220703125, + 82 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "model", + "type": "FANTASYTALKINGMODEL", + "links": [ + 84, + 100 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "df95c85283d7625fbdf664d0133a2e1c114ba14a", + "Node name for S&R": "FantasyTalkingModelLoader" + }, + "widgets_values": [ + "fantasytalking_fp16.safetensors", + "fp16" + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 11, + "type": "LoadWanVideoT5TextEncoder", + "pos": [ + -120, + -430 + ], + "size": [ + 377.1661376953125, + 130 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "wan_t5_model", + "type": "WANTEXTENCODER", + "slot_index": 0, + "links": [ + 15 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "LoadWanVideoT5TextEncoder" + }, + "widgets_values": [ + "umt5-xxl-enc-fp8_e4m3fn.safetensors", + "bf16", + "offload_device", + "disabled" + ], + "color": "#332922", + "bgcolor": "#593930" + }, + { + "id": 78, + "type": "CreateCFGScheduleFloatList", + "pos": [ + 20, + -690 + ], + "size": [ + 340, + 180 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "float_list", + "type": "FLOAT", + "links": [ + 113 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "e3afc7fc758add9ba0ca7e6e219c30f312758484", + "Node name for S&R": "CreateCFGScheduleFloatList" + }, + "widgets_values": [ + 10, + 5, + 5, + "linear", + 0, + 0.1 + ] + }, + { + "id": 16, + "type": "WanVideoTextEncode", + "pos": [ + 390, + -450 + ], + "size": [ + 420.30511474609375, + 261.5306701660156 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "t5", + "type": "WANTEXTENCODER", + "link": 15 + }, + { + "name": "model_to_offload", + "shape": 7, + "type": "WANVIDEOMODEL", + "link": 79 + } + ], + "outputs": [ + { + "name": "text_embeds", + "type": "WANVIDEOTEXTEMBEDS", + "slot_index": 0, + "links": [ + 86 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoTextEncode" + }, + "widgets_values": [ + "A woman is talking.", + "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走", + true + ], + "color": "#332922", + "bgcolor": "#593930" + }, + { + "id": 52, + "type": "WanVideoTeaCache", + "pos": [ + 400, + -690 + ], + "size": [ + 315, + 178 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "cache_args", + "type": "CACHEARGS", + "links": [ + 89 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoTeaCache" + }, + "widgets_values": [ + 0.225, + 6, + -1, + "offload_device", + "true", + "e" + ] + }, + { + "id": 39, + "type": "WanVideoBlockSwap", + "pos": [ + 0, + -960 + ], + "size": [ + 315, + 154 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "block_swap_args", + "type": "BLOCKSWAPARGS", + "slot_index": 0, + "links": [ + 96 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoBlockSwap" + }, + "widgets_values": [ + 30, + false, + false, + true, + 0 + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 22, + "type": "WanVideoModelLoader", + "pos": [ + 360, + -990 + ], + "size": [ + 477.4410095214844, + 254 + ], + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "compile_args", + "shape": 7, + "type": "WANCOMPILEARGS", + "link": null + }, + { + "name": "block_swap_args", + "shape": 7, + "type": "BLOCKSWAPARGS", + "link": 96 + }, + { + "name": "lora", + "shape": 7, + "type": "WANVIDLORA", + "link": null + }, + { + "name": "vram_management_args", + "shape": 7, + "type": "VRAM_MANAGEMENTARGS", + "link": null + }, + { + "name": "vace_model", + "shape": 7, + "type": "VACEPATH", + "link": null + }, + { + "name": "fantasytalking_model", + "shape": 7, + "type": "FANTASYTALKINGMODEL", + "link": 84 + } + ], + "outputs": [ + { + "name": "model", + "type": "WANVIDEOMODEL", + "slot_index": 0, + "links": [ + 79, + 85 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoModelLoader" + }, + "widgets_values": [ + "Wan2_1-I2V-14B-480P_fp8_e4m3fn.safetensors", + "fp16_fast", + "fp8_e4m3fn", + "offload_device", + "sageattn" + ], + "color": "#223", + "bgcolor": "#335" + }, + { + "id": 73, + "type": "FantasyTalkingWav2VecEmbeds", + "pos": [ + 250, + -110 + ], + "size": [ + 531.5999755859375, + 170 + ], + "flags": {}, + "order": 23, + "mode": 0, + "inputs": [ + { + "name": "wav2vec_model", + "type": "WAV2VECMODEL", + "link": 99 + }, + { + "name": "fantasytalking_model", + "type": "FANTASYTALKINGMODEL", + "link": 100 + }, + { + "name": "audio", + "type": "AUDIO", + "link": 121 + }, + { + "name": "num_frames", + "type": "INT", + "widget": { + "name": "num_frames" + }, + "link": 112 + }, + { + "name": "fps", + "type": "FLOAT", + "widget": { + "name": "fps" + }, + "link": 189 + }, + { + "name": "audio_cfg_scale", + "type": "FLOAT", + "widget": { + "name": "audio_cfg_scale" + }, + "link": 113 + } + ], + "outputs": [ + { + "name": "fantasytalking_embeds", + "type": "FANTASYTALKING_EMBEDS", + "links": [ + 101 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "df95c85283d7625fbdf664d0133a2e1c114ba14a", + "Node name for S&R": "FantasyTalkingWav2VecEmbeds" + }, + "widgets_values": [ + 81, + 30, + 1, + 1 + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 69, + "type": "WanVideoSampler", + "pos": [ + 860, + -650 + ], + "size": [ + 317.4000244140625, + 869.4000244140625 + ], + "flags": {}, + "order": 24, + "mode": 0, + "inputs": [ + { + "name": "model", + "type": "WANVIDEOMODEL", + "link": 85 + }, + { + "name": "image_embeds", + "type": "WANVIDIMAGE_EMBEDS", + "link": 87 + }, + { + "name": "text_embeds", + "shape": 7, + "type": "WANVIDEOTEXTEMBEDS", + "link": 86 + }, + { + "name": "samples", + "shape": 7, + "type": "LATENT", + "link": null + }, + { + "name": "feta_args", + "shape": 7, + "type": "FETAARGS", + "link": null + }, + { + "name": "context_options", + "shape": 7, + "type": "WANVIDCONTEXT", + "link": null + }, + { + "name": "cache_args", + "shape": 7, + "type": "CACHEARGS", + "link": null + }, + { + "name": "flowedit_args", + "shape": 7, + "type": "FLOWEDITARGS", + "link": null + }, + { + "name": "slg_args", + "shape": 7, + "type": "SLGARGS", + "link": null + }, + { + "name": "loop_args", + "shape": 7, + "type": "LOOPARGS", + "link": null + }, + { + "name": "experimental_args", + "shape": 7, + "type": "EXPERIMENTALARGS", + "link": null + }, + { + "name": "sigmas", + "shape": 7, + "type": "SIGMAS", + "link": null + }, + { + "name": "unianimate_poses", + "shape": 7, + "type": "UNIANIMATE_POSE", + "link": null + }, + { + "name": "fantasytalking_embeds", + "shape": 7, + "type": "FANTASYTALKING_EMBEDS", + "link": 101 + }, + { + "name": "uni3c_embeds", + "shape": 7, + "type": "UNI3C_EMBEDS", + "link": null + }, + { + "name": "teacache_args", + "shape": 7, + "type": "TEACACHEARGS", + "link": 89 + } + ], + "outputs": [ + { + "name": "samples", + "type": "LATENT", + "links": [ + 90 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "df95c85283d7625fbdf664d0133a2e1c114ba14a", + "Node name for S&R": "WanVideoSampler" + }, + "widgets_values": [ + 10, + 5.000000000000001, + 5, + 0, + "fixed", + true, + "unipc", + 0, + 1, + false, + "comfy" + ] + }, + { + "id": 28, + "type": "WanVideoDecode", + "pos": [ + 1220, + -550 + ], + "size": [ + 315, + 174 + ], + "flags": {}, + "order": 25, + "mode": 0, + "inputs": [ + { + "name": "vae", + "type": "WANVAE", + "link": 218 + }, + { + "name": "samples", + "type": "LATENT", + "link": 90 + } + ], + "outputs": [ + { + "name": "images", + "type": "IMAGE", + "slot_index": 0, + "links": [ + 178, + 202 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoDecode" + }, + "widgets_values": [ + false, + 272, + 272, + 144, + 128 + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 30, + "type": "VHS_VideoCombine", + "pos": [ + 1200, + -310 + ], + "size": [ + 370, + 334 + ], + "flags": {}, + "order": 29, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 208 + }, + { + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": 173 + }, + { + "name": "meta_batch", + "shape": 7, + "type": "VHS_BatchManager", + "link": null + }, + { + "name": "vae", + "shape": 7, + "type": "VAE", + "link": null + }, + { + "name": "frame_rate", + "type": "FLOAT", + "widget": { + "name": "frame_rate" + }, + "link": 190 + } + ], + "outputs": [ + { + "name": "Filenames", + "type": "VHS_FILENAMES", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-videohelpersuite", + "ver": "0a75c7958fe320efcb052f1d9f8451fd20c730a8", + "Node name for S&R": "VHS_VideoCombine" + }, + "widgets_values": { + "frame_rate": 23, + "loop_count": 0, + "filename_prefix": "WanVideoWrapper_I2V_FantasyTalking", + "format": "video/h264-mp4", + "pix_fmt": "yuv420p", + "crf": 19, + "save_metadata": true, + "trim_to_audio": false, + "pingpong": false, + "save_output": true, + "videopreview": { + "hidden": true, + "paused": false, + "params": { + "filename": "WanVideoWrapper_I2V_FantasyTalking_00011-audio.mp4", + "subfolder": "", + "type": "output", + "format": "video/h264-mp4", + "frame_rate": 23, + "workflow": "WanVideoWrapper_I2V_FantasyTalking_00011.png", + "fullpath": "C:\\Users\\dseditor\\CUI\\ComfyUI\\output\\WanVideoWrapper_I2V_FantasyTalking_00011-audio.mp4" + } + } + }, + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 38, + "type": "WanVideoVAELoader", + "pos": [ + -540, + -720 + ], + "size": [ + 372.7727966308594, + 82 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "vae", + "type": "WANVAE", + "slot_index": 0, + "links": [ + 218, + 219 + ] + } + ], + "properties": { + "cnr_id": "ComfyUI-WanVideoWrapper", + "ver": "d9b1f4d1a5aea91d101ae97a54714a5861af3f50", + "Node name for S&R": "WanVideoVAELoader" + }, + "widgets_values": [ + "Wan2_1_VAE_bf16.safetensors", + "bf16" + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 58, + "type": "LoadImage", + "pos": [ + -1550, + -270 + ], + "size": [ + 413.10479736328125, + 498.3180847167969 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 193 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.26", + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "real_00101_.png", + "image" + ], + "color": "#2a363b", + "bgcolor": "#3f5159" + }, + { + "id": 72, + "type": "LoadAudio", + "pos": [ + -1230, + -810 + ], + "size": [ + 315, + 136 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "AUDIO", + "type": "AUDIO", + "links": [ + 173, + 214 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.29", + "Node name for S&R": "LoadAudio" + }, + "widgets_values": [ + "ComfyUI_00063_.flac", + null, + null + ], + "color": "#323", + "bgcolor": "#535" + }, + { + "id": 117, + "type": "easy float", + "pos": [ + -1200, + -600 + ], + "size": [ + 270, + 58 + ], + "flags": {}, + "order": 11, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "float", + "type": "FLOAT", + "links": [ + 189, + 190, + 213 + ] + } + ], + "properties": { + "cnr_id": "comfyui-easy-use", + "ver": "71c7865d2d3c934ccb99f24171e08ae5a81148ac", + "Node name for S&R": "easy float" + }, + "widgets_values": [ + 30.000000000000007 + ] + }, + { + "id": 75, + "type": "INTConstant", + "pos": [ + -1160, + -460 + ], + "size": [ + 210, + 58 + ], + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "value", + "type": "INT", + "links": [ + 111, + 112, + 217 + ] + } + ], + "title": "Frames", + "properties": { + "cnr_id": "comfyui-kjnodes", + "ver": "c3dc82108a2a86c17094107ead61d63f8c76200e", + "Node name for S&R": "INTConstant" + }, + "widgets_values": [ + 81 + ], + "color": "#1b4669", + "bgcolor": "#29699c" + } + ], + "links": [ + [ + 15, + 11, + 0, + 16, + 0, + "WANTEXTENCODER" + ], + [ + 70, + 59, + 0, + 65, + 0, + "CLIP_VISION" + ], + [ + 79, + 22, + 0, + 16, + 1, + "WANVIDEOMODEL" + ], + [ + 82, + 65, + 0, + 63, + 1, + "WANVIDIMAGE_CLIPEMBEDS" + ], + [ + 84, + 68, + 0, + 22, + 5, + "FANTASYTALKINGMODEL" + ], + [ + 85, + 22, + 0, + 69, + 0, + "WANVIDEOMODEL" + ], + [ + 86, + 16, + 0, + 69, + 2, + "WANVIDEOTEXTEMBEDS" + ], + [ + 87, + 63, + 0, + 69, + 1, + "WANVIDIMAGE_EMBEDS" + ], + [ + 89, + 52, + 0, + 69, + 15, + "TEACACHEARGS" + ], + [ + 90, + 69, + 0, + 28, + 1, + "LATENT" + ], + [ + 96, + 39, + 0, + 22, + 1, + "BLOCKSWAPARGS" + ], + [ + 99, + 71, + 0, + 73, + 0, + "WAV2VECMODEL" + ], + [ + 100, + 68, + 0, + 73, + 1, + "FANTASYTALKINGMODEL" + ], + [ + 101, + 73, + 0, + 69, + 13, + "FANTASYTALKING_EMBEDS" + ], + [ + 109, + 74, + 1, + 63, + 8, + "INT" + ], + [ + 110, + 74, + 2, + 63, + 9, + "INT" + ], + [ + 111, + 75, + 0, + 63, + 10, + "INT" + ], + [ + 112, + 75, + 0, + 73, + 3, + "INT" + ], + [ + 113, + 78, + 0, + 73, + 5, + "FLOAT" + ], + [ + 121, + 84, + 0, + 73, + 2, + "AUDIO" + ], + [ + 173, + 72, + 0, + 30, + 1, + "AUDIO" + ], + [ + 178, + 28, + 0, + 112, + 0, + "IMAGE" + ], + [ + 189, + 117, + 0, + 73, + 4, + "FLOAT" + ], + [ + 190, + 117, + 0, + 30, + 4, + "FLOAT" + ], + [ + 193, + 58, + 0, + 119, + 0, + "*" + ], + [ + 194, + 119, + 0, + 74, + 0, + "IMAGE" + ], + [ + 195, + 74, + 0, + 110, + 0, + "*" + ], + [ + 196, + 110, + 2, + 65, + 1, + "IMAGE" + ], + [ + 197, + 110, + 2, + 63, + 2, + "IMAGE" + ], + [ + 198, + 112, + 0, + 121, + 1, + "*" + ], + [ + 199, + 110, + 0, + 121, + 0, + "FLOW_CONTROL" + ], + [ + 200, + 120, + 0, + 121, + 2, + "*" + ], + [ + 202, + 28, + 0, + 120, + 0, + "*" + ], + [ + 203, + 110, + 3, + 120, + 1, + "*" + ], + [ + 205, + 122, + 0, + 84, + 0, + "AUDIO" + ], + [ + 206, + 110, + 1, + 122, + 1, + "INT" + ], + [ + 208, + 121, + 1, + 30, + 0, + "IMAGE" + ], + [ + 213, + 117, + 0, + 123, + 1, + "FLOAT" + ], + [ + 214, + 72, + 0, + 123, + 0, + "AUDIO" + ], + [ + 215, + 123, + 0, + 110, + 1, + "INT" + ], + [ + 216, + 123, + 1, + 122, + 0, + "*" + ], + [ + 217, + 75, + 0, + 123, + 2, + "INT" + ], + [ + 218, + 38, + 0, + 28, + 0, + "WANVAE" + ], + [ + 219, + 38, + 0, + 63, + 0, + "WANVAE" + ] + ], + "groups": [ + { + "id": 1, + "title": "LoopPart", + "bounding": [ + -900, + 260, + 1013.08984375, + 355.6000061035156 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "config": {}, + "extra": { + "ds": { + "scale": 0.7247295000000009, + "offset": [ + 1600.7756029751072, + 829.0485564930697 + ] + }, + "frontendVersion": "1.21.7", + "node_versions": { + "ComfyUI-WanVideoWrapper": "5a2383621a05825d0d0437781afcb8552d9590fd", + "comfy-core": "0.3.26", + "ComfyUI-KJNodes": "a5bd3c86c8ed6b83c55c2d0e7a59515b15a0137f", + "ComfyUI-VideoHelperSuite": "0a75c7958fe320efcb052f1d9f8451fd20c730a8" + }, + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file diff --git a/Example/PhotoWithAudio.json b/Example/PhotoWithAudio.json new file mode 100644 index 0000000..a486ccc --- /dev/null +++ b/Example/PhotoWithAudio.json @@ -0,0 +1,399 @@ +{ + "id": "c9f9b81c-c01e-4e95-b8bb-833b15af740d", + "revision": 0, + "last_node_id": 9, + "last_link_id": 9, + "nodes": [ + { + "id": 1, + "type": "AudioToFrameCount", + "pos": [ + 1540, + 220 + ], + "size": [ + 270, + 58 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "audio", + "type": "AUDIO", + "link": 1 + }, + { + "name": "fps", + "type": "FLOAT", + "widget": { + "name": "fps" + }, + "link": 7 + } + ], + "outputs": [ + { + "name": "frames", + "type": "INT", + "links": [ + 2, + 4 + ] + } + ], + "properties": { + "Node name for S&R": "AudioToFrameCount" + }, + "widgets_values": [ + 25 + ] + }, + { + "id": 6, + "type": "PreviewAny", + "pos": [ + 1560, + 40 + ], + "size": [ + 250, + 120 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "source", + "type": "*", + "link": 4 + } + ], + "outputs": [], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.41", + "Node name for S&R": "PreviewAny" + }, + "widgets_values": [] + }, + { + "id": 3, + "type": "RepeatImageBatch", + "pos": [ + 1540, + 360 + ], + "size": [ + 270, + 58 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 9 + }, + { + "name": "amount", + "type": "INT", + "widget": { + "name": "amount" + }, + "link": 2 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 5 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.41", + "Node name for S&R": "RepeatImageBatch" + }, + "widgets_values": [ + 1 + ] + }, + { + "id": 2, + "type": "LoadAudio", + "pos": [ + 1240, + 40 + ], + "size": [ + 274.080078125, + 136 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "AUDIO", + "type": "AUDIO", + "links": [ + 1, + 6 + ] + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.41", + "Node name for S&R": "LoadAudio" + }, + "widgets_values": [ + "normalsoft.flac", + null, + "" + ] + }, + { + "id": 8, + "type": "easy float", + "pos": [ + 1240, + 230 + ], + "size": [ + 270, + 58 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "float", + "type": "FLOAT", + "links": [ + 7, + 8 + ] + } + ], + "properties": { + "cnr_id": "comfyui-easy-use", + "ver": "71c7865d2d3c934ccb99f24171e08ae5a81148ac", + "Node name for S&R": "easy float" + }, + "widgets_values": [ + 25.000000000000004 + ] + }, + { + "id": 4, + "type": "VHS_VideoCombine", + "pos": [ + 1860, + 40 + ], + "size": [ + 260, + 350 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 5 + }, + { + "name": "audio", + "shape": 7, + "type": "AUDIO", + "link": 6 + }, + { + "name": "meta_batch", + "shape": 7, + "type": "VHS_BatchManager", + "link": null + }, + { + "name": "vae", + "shape": 7, + "type": "VAE", + "link": null + }, + { + "name": "frame_rate", + "type": "FLOAT", + "widget": { + "name": "frame_rate" + }, + "link": 8 + } + ], + "outputs": [ + { + "name": "Filenames", + "type": "VHS_FILENAMES", + "links": null + } + ], + "properties": { + "cnr_id": "comfyui-videohelpersuite", + "ver": "a7ce59e381934733bfae03b1be029756d6ce936d", + "Node name for S&R": "VHS_VideoCombine" + }, + "widgets_values": { + "frame_rate": 8, + "loop_count": 0, + "filename_prefix": "AnimateDiff", + "format": "video/nvenc_hevc-mp4", + "pix_fmt": "yuv420p", + "bitrate": 10, + "megabit": true, + "save_metadata": true, + "pingpong": false, + "save_output": true, + "videopreview": { + "hidden": false, + "paused": false, + "params": {} + } + } + }, + { + "id": 9, + "type": "LoadImage", + "pos": [ + 1240, + 340 + ], + "size": [ + 274.080078125, + 314 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 9 + ] + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "cnr_id": "comfy-core", + "ver": "0.3.41", + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "db94662b-1b65-4118-b24d-da2f6dbba76b.jpg", + "image" + ] + } + ], + "links": [ + [ + 1, + 2, + 0, + 1, + 0, + "AUDIO" + ], + [ + 2, + 1, + 0, + 3, + 1, + "INT" + ], + [ + 4, + 1, + 0, + 6, + 0, + "*" + ], + [ + 5, + 3, + 0, + 4, + 0, + "IMAGE" + ], + [ + 6, + 2, + 0, + 4, + 1, + "AUDIO" + ], + [ + 7, + 8, + 0, + 1, + 1, + "FLOAT" + ], + [ + 8, + 8, + 0, + 4, + 4, + "FLOAT" + ], + [ + 9, + 9, + 0, + 3, + 0, + "IMAGE" + ] + ], + "groups": [], + "config": {}, + "extra": { + "ds": { + "scale": 0.9090909090909091, + "offset": [ + -1029.8093402426837, + 68.20082052175825 + ] + }, + "frontendVersion": "1.21.7", + "VHS_latentpreview": false, + "VHS_latentpreviewrate": 0, + "VHS_MetadataImage": true, + "VHS_KeepIntermediate": true + }, + "version": 0.4 +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..234e319 --- /dev/null +++ b/README.md @@ -0,0 +1,551 @@ +# ListHelper Nodes Collection + +[中文版本](#中文版本) | [English Version](#english-version) + +--- + +## English Version + +### Overview + +The **ListHelper** collection is a comprehensive set of custom nodes for ComfyUI that provides powerful list manipulation capabilities. This collection includes audio processing, text splitting, and number generation tools for enhanced workflow automation. + +### Included Nodes + +1. [AudioListCombine](#audiolistcombine-node) +2. [NumberListGenerator](#numberlistgenerator-node) +3. [PromptSplitByDelimiter](#promptsplitbydelimiter-node) +4. [AudioToFrameCount] +5. [AudioSplitToList] +6. [CeilDivide] + +--- + +## AudioListCombine Node + +![Demo](readme/demo.jpg) + +### Overview + +The **AudioListCombine** node is a powerful custom node for ComfyUI that allows you to combine multiple audio files from a list into a single audio output. It supports various combination modes and audio processing options. + +### Features + +- **Multiple Combination Modes**: Concatenate, mix, or overlay audio files +- **Automatic Sample Rate Conversion**: Unifies different sample rates to target rate +- **Channel Normalization**: Automatically handles mono/stereo conversion +- **Crossfade Support**: Smooth transitions between audio segments +- **Audio Normalization**: Optional output level normalization +- **Flexible Input**: Accepts audio lists from Impact Pack or other list-making nodes + +### Requirements + +- ComfyUI +- Audio list creation nodes (e.g., Impact Pack's MakeAnyList, or custom list nodes) +- Python libraries: `torch`, `torchaudio` + +### Usage + +#### Input Parameters + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `audio_list` | AUDIO | - | List of audio files (from Impact Pack or other list nodes) | +| `combine_mode` | COMBO | "concatenate" | How to combine audio: concatenate/mix/overlay | +| `fade_duration` | FLOAT | 0.0 | Crossfade duration in seconds (0.0-5.0) | +| `normalize_output` | BOOLEAN | True | Whether to normalize output audio | +| `target_sample_rate` | INT | 44100 | Target sample rate for output | + +#### Combine Modes + +1. **Concatenate**: Join audio files end-to-end in sequence + - Supports crossfade transitions + - Maintains chronological order + - Best for: Creating audio sequences, podcasts, music playlists + +2. **Mix**: Average all audio files together + - Pads shorter files with silence + - Equal weight blending + - Best for: Creating audio mashups, averaging multiple takes + +3. **Overlay**: Add all audio files together + - Direct addition (may cause clipping) + - Preserves original volumes + - Best for: Adding sound effects, layering instruments + +#### Output + +| Output | Type | Description | +|--------|------|-------------| +| `audio` | AUDIO | Combined audio result | + +### Examples + +#### Example 1: Creating a Music Playlist +``` +Audio File 1 → +Audio File 2 → MakeAnyList → AudioListCombine (concatenate, fade=0.5s) → Save Audio +Audio File 3 → +``` + +#### Example 2: Mixing Multiple Recordings +``` +Recording 1 → +Recording 2 → MakeAnyList → AudioListCombine (mix, normalize=True) → Save Audio +Recording 3 → +``` + +#### Example 3: Adding Sound Effects +``` +Background Music → +Sound Effect 1 → MakeAnyList → AudioListCombine (overlay) → Save Audio +Sound Effect 2 → +``` + +--- + +## NumberListGenerator Node + +![Demo](readme/demo1.jpg) + +### Overview +The NumberListGenerator node creates lists of numbers with customizable parameters, supporting both sequential and randomized output. It's perfect for batch processing, parameter sweeping, or any workflow requiring controlled number sequences. + +### Features +- **Dual Output Format**: Generates both integer and float lists simultaneously +- **Flexible Range Control**: Set minimum, maximum values and step size +- **Sequential or Random**: Toggle between ordered and shuffled output +- **Reproducible Results**: Optional seed parameter for consistent random generation +- **Count Tracking**: Returns total number of generated values + +### Parameters + +**Required Inputs:** +- **min_value** (Float): Starting value for the sequence (Range: -10,000 to 10,000, Default: 0.0) +- **max_value** (Float): Maximum value upper bound (Range: -10,000 to 10,000, Default: 10.0) +- **step** (Float): Increment between consecutive values (Range: 0.01 to 1,000, Default: 1.0) +- **count** (Int): Number of values to generate (Range: 1 to 10,000, Default: 10) +- **random** (Boolean): Enable random shuffling of the generated list (Default: False) + +**Optional Inputs:** +- **seed** (Int): Random seed for reproducible results when random=True (Range: -1 to 1,000,000, Default: -1) + +**Outputs:** +- **int_list**: List of integer values +- **float_list**: List of float values +- **total_count**: Total number of generated values + +### Usage Examples + +**Sequential Generation:** +``` +min_value: 0, max_value: 20, step: 2, count: 10, random: False +Output: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] +``` + +**Random Generation:** +``` +min_value: 1, max_value: 100, step: 5, count: 8, random: True, seed: 42 +Output: [16, 1, 31, 6, 21, 11, 26, 36] (shuffled) +``` + +--- + +## PromptSplitByDelimiter Node + +![Demo](readme/demo2.jpg) + +### Overview + +The **PromptSplitByDelimiter** node is a versatile text processing tool that splits text content using customizable delimiters. It supports both simple string delimiters and advanced regular expressions, with optional random ordering and delimiter preservation. + +### Features + +- **Flexible Delimiter Support**: Use simple strings or regular expressions as delimiters +- **Multi-language Support**: Native support for CJK (Chinese, Japanese, Korean) characters +- **Regular Expression Mode**: Advanced pattern matching for complex splitting rules +- **Delimiter Preservation**: Option to keep delimiters in the output +- **Random Ordering**: Shuffle results with reproducible seed control +- **Advanced Text Processing**: Handle multiple newlines, skip empty segments +- **Selective Processing**: Skip content before first delimiter occurrence + +### Parameters + +| Parameter | Type | Default | Range | Description | +|-----------|------|---------|--------|-------------| +| `text` | STRING | - | - | Multiline text input to be split | +| `delimiter` | STRING | "," | - | Delimiter string or regex pattern | +| `use_regex` | BOOLEAN | False | - | Enable regular expression mode | +| `keep_delimiter` | BOOLEAN | False | - | Preserve delimiters in output | +| `start_index` | INT | 0 | 0-1000 | Starting index for selection | +| `skip_every` | INT | 0 | 0-10 | Skip every N items | +| `max_count` | INT | 10 | 1-1000 | Maximum items to return | +| `skip_first_index` | BOOLEAN | False | - | Skip content before first delimiter | +| `random_order` | BOOLEAN | False | - | Randomize output order | +| `seed` | INT | 0 | 0-2147483647 | Random seed for reproducible results | + +### Output + +| Output | Type | Description | +|--------|------|-------------| +| `text_list` | STRING | List of split text segments | +| `total_index` | INT | Total number of segments found | + +### Usage Examples + +#### Example 1: Simple Comma Splitting +``` +Input: "apple,banana,cherry,date" +Delimiter: "," +Output: ["apple", "banana", "cherry", "date"] +``` + +#### Example 2: Chinese Chapter Splitting +``` +Input: "前言第一章内容第二章内容第三章结尾" +Delimiter: "第.*?章" (regex mode) +Output: ["前言", "内容", "内容", "结尾"] +``` + +#### Example 3: Delimiter Preservation +``` +Input: "AAA//BBB//CCC" +Delimiter: "//" +Keep Delimiter: True +Output: ["AAA", "//BBB", "//CCC"] +``` + +#### Example 4: Random Chapter Selection +``` +Input: "Chapter1\nChapter2\nChapter3\nChapter4" +Delimiter: "\n" +Random Order: True +Max Count: 2 +Output: ["Chapter3", "Chapter1"] (randomized) +``` + +### Advanced Features + +#### Regular Expression Support +- **Pattern Matching**: Use regex patterns for complex delimiter rules +- **CJK Character Support**: `第\d+章` matches "第1章", "第2章", etc. +- **Flexible Patterns**: `(章|節|段)` matches any of "章", "節", or "段" + +#### Text Processing Rules +- **Newline Normalization**: Multiple consecutive newlines are treated as single newline +- **Empty Delimiter Handling**: Empty delimiter automatically uses newline as fallback +- **Whitespace Trimming**: Automatic trimming of leading/trailing whitespace + +#### Selection and Filtering +- **Index Range**: Select specific range of results using `start_index` and `max_count` +- **Skip Pattern**: Use `skip_every` to select every Nth item +- **First Segment Skip**: Use `skip_first_index` to ignore content before first delimiter + +### Use Cases + +- **Document Processing**: Split books into chapters, articles into sections +- **Data Extraction**: Extract structured data from formatted text +- **Content Management**: Process multilingual content with CJK support +- **Batch Processing**: Generate lists for downstream processing nodes +- **Random Sampling**: Create randomized content selections + +--- + +## 中文版本 + +### 概述 + +**ListHelper** 集合是 ComfyUI 的全面自定義節點集,提供強大的列表操作功能。此集合包含音頻處理、文本分割和數字生成工具,用於增強工作流程自動化。 + +### 包含的節點 + +1. [AudioListCombine 音頻列表合併](#audiolistcombine-音頻列表合併節點) +2. [NumberListGenerator 數字列表生成器](#numberlistgenerator-數字列表生成節點) +3. [PromptSplitByDelimiter 提示分割器](#promptsplitbydelimiter-提示分割節點) + +--- + +## AudioListCombine 音頻列表合併節點 + +### 概述 + +**AudioListCombine** 節點是 ComfyUI 的強大自定義節點,允許您將音頻清單中的多個音頻文件合併為單一音頻輸出。支持多種合併模式和音頻處理選項。 + +### 功能特色 + +- **多種合併模式**:串接、混音或覆疊音頻文件 +- **自動採樣率轉換**:統一不同採樣率至目標採樣率 +- **聲道標準化**:自動處理單聲道/立體聲轉換 +- **交叉淡化支持**:音頻片段間的平滑過渡 +- **音頻標準化**:可選的輸出音量標準化 +- **靈活輸入**:接受來自 Impact Pack 或其他清單製作節點的音頻清單 + +### 使用方法 + +#### 輸入參數 + +| 參數 | 類型 | 預設值 | 說明 | +|------|------|--------|------| +| `audio_list` | AUDIO | - | 音頻文件清單(來自 Impact Pack 或其他清單節點)| +| `combine_mode` | COMBO | "concatenate" | 合併方式:concatenate/mix/overlay | +| `fade_duration` | FLOAT | 0.0 | 交叉淡化持續時間(秒,0.0-5.0)| +| `normalize_output` | BOOLEAN | True | 是否標準化輸出音頻 | +| `target_sample_rate` | INT | 44100 | 目標輸出採樣率 | + +#### 合併模式 + +1. **Concatenate(串接)**:按順序將音頻文件首尾相連 + - 支持交叉淡化過渡 + - 保持時間順序 + - 適用於:創建音頻序列、播客、音樂播放清單 + +2. **Mix(混音)**:將所有音頻文件平均混合 + - 較短文件用靜音填充 + - 等權重混合 + - 適用於:創建音頻混搭、平均多個錄音 + +3. **Overlay(覆疊)**:將所有音頻文件直接相加 + - 直接加法(可能造成削波) + - 保持原始音量 + - 適用於:添加音效、樂器分層 + +#### 輸出 + +| 輸出 | 類型 | 說明 | +|------|------|------| +| `audio` | AUDIO | 合併後的音頻結果 | + +### 使用範例 + +#### 範例 1:創建音樂播放清單 +``` +音頻文件 1 → +音頻文件 2 → MakeAnyList → AudioListCombine (concatenate, fade=0.5s) → 保存音頻 +音頻文件 3 → +``` + +#### 範例 2:混合多個錄音 +``` +錄音 1 → +錄音 2 → MakeAnyList → AudioListCombine (mix, normalize=True) → 保存音頻 +錄音 3 → +``` + +#### 範例 3:添加音效 +``` +背景音樂 → +音效 1 → MakeAnyList → AudioListCombine (overlay) → 保存音頻 +音效 2 → +``` + +--- + +## NumberListGenerator 數字列表生成節點 + +### 概述 +NumberListGenerator 節點可根據自訂參數創建數字列表,支援有序和隨機輸出。非常適合批次處理、參數掃描或任何需要受控數字序列的工作流程。 + +### 功能特色 +- **雙重輸出格式**: 同時生成整數和浮點數列表 +- **靈活範圍控制**: 設定最小值、最大值和步長 +- **有序或隨機**: 可切換有序和打亂輸出 +- **可重現結果**: 可選種子參數確保隨機生成的一致性 +- **計數追蹤**: 返回生成數值的總數 + +### 參數說明 + +**必需輸入:** +- **min_value / 最小值** (Float): 序列的起始值 (範圍: -10,000 到 10,000,預設: 0.0) +- **max_value / 最大值** (Float): 最大值上限 (範圍: -10,000 到 10,000,預設: 10.0) +- **step / 步長** (Float): 連續數值間的增量 (範圍: 0.01 到 1,000,預設: 1.0) +- **count / 數量** (Int): 要生成的數值數量 (範圍: 1 到 10,000,預設: 10) +- **random / 隨機** (Boolean): 啟用生成列表的隨機打亂 (預設: False) + +**可選輸入:** +- **seed / 種子** (Int): 隨機種子,用於可重現結果 (範圍: -1 到 1,000,000,預設: -1) + +**輸出:** +- **int_list / 整數列表**: 整數值列表 +- **float_list / 浮點數列表**: 浮點數值列表 +- **total_count / 總計數**: 生成數值的總數 + +### 使用範例 + +**有序數字生成:** +``` +最小值: 0,最大值: 20,步長: 2,數量: 10,隨機: False +輸出: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] +``` + +**隨機數字生成:** +``` +最小值: 1,最大值: 100,步長: 5,數量: 8,隨機: True,種子: 42 +輸出: [16, 1, 31, 6, 21, 11, 26, 36] (已打亂) +``` + +--- + +## PromptSplitByDelimiter 提示分割節點 + +### 概述 + +**PromptSplitByDelimiter** 節點是一個多功能的文本處理工具,使用可自訂的分隔符分割文本內容。支援簡單字符串分隔符和高級正規表示式,並具有可選的隨機排序和分隔符保留功能。 + +### 功能特色 + +- **靈活的分隔符支援**:使用簡單字符串或正規表示式作為分隔符 +- **多語言支援**:原生支援中日韓(CJK)文字 +- **正規表示式模式**:高級模式匹配,用於複雜的分割規則 +- **分隔符保留**:可選擇在輸出中保留分隔符 +- **隨機排序**:使用可重現的種子控制打亂結果 +- **高級文本處理**:處理多個換行符、跳過空白片段 +- **選擇性處理**:跳過第一個分隔符出現前的內容 + +### 參數說明 + +| 參數 | 類型 | 預設值 | 範圍 | 說明 | +|------|------|--------|------|------| +| `text` | STRING | - | - | 要分割的多行文本輸入 | +| `delimiter` | STRING | "," | - | 分隔符字符串或正規表示式模式 | +| `use_regex` | BOOLEAN | False | - | 啟用正規表示式模式 | +| `keep_delimiter` | BOOLEAN | False | - | 在輸出中保留分隔符 | +| `start_index` | INT | 0 | 0-1000 | 選擇的起始索引 | +| `skip_every` | INT | 0 | 0-10 | 跳過每 N 個項目 | +| `max_count` | INT | 10 | 1-1000 | 返回的最大項目數 | +| `skip_first_index` | BOOLEAN | False | - | 跳過第一個分隔符前的內容 | +| `random_order` | BOOLEAN | False | - | 隨機輸出順序 | +| `seed` | INT | 0 | 0-2147483647 | 可重現結果的隨機種子 | + +### 輸出 + +| 輸出 | 類型 | 說明 | +|------|------|------| +| `text_list` | STRING | 分割的文本片段列表 | +| `total_index` | INT | 找到的片段總數 | + +### 使用範例 + +#### 範例 1:簡單逗號分割 +``` +輸入: "蘋果,香蕉,櫻桃,棗子" +分隔符: "," +輸出: ["蘋果", "香蕉", "櫻桃", "棗子"] +``` + +#### 範例 2:中文章節分割 +``` +輸入: "前言第一章內容第二章內容第三章結尾" +分隔符: "第.*?章" (正規表示式模式) +輸出: ["前言", "內容", "內容", "結尾"] +``` + +#### 範例 3:分隔符保留 +``` +輸入: "AAA//BBB//CCC" +分隔符: "//" +保留分隔符: True +輸出: ["AAA", "//BBB", "//CCC"] +``` + +#### 範例 4:隨機章節選擇 +``` +輸入: "第一章\n第二章\n第三章\n第四章" +分隔符: "\n" +隨機順序: True +最大數量: 2 +輸出: ["第三章", "第一章"] (已隨機化) +``` + +### 進階功能 + +#### 正規表示式支援 +- **模式匹配**:使用正規表示式模式進行複雜的分隔符規則 +- **中日韓文字支援**:`第\d+章` 匹配 "第1章"、"第2章" 等 +- **靈活模式**:`(章|節|段)` 匹配 "章"、"節" 或 "段" 中的任何一個 + +#### 文本處理規則 +- **換行標準化**:多個連續換行符被視為單個換行符 +- **空分隔符處理**:空分隔符自動使用換行符作為後備 +- **空白修剪**:自動修剪前導/尾隨空白 + +#### 選擇和過濾 +- **索引範圍**:使用 `start_index` 和 `max_count` 選擇特定範圍的結果 +- **跳過模式**:使用 `skip_every` 選擇每第 N 個項目 +- **第一片段跳過**:使用 `skip_first_index` 忽略第一個分隔符前的內容 + +### 使用案例 + +- **文檔處理**:將書籍分割為章節,將文章分割為段落 +- **數據提取**:從格式化文本中提取結構化數據 +- **內容管理**:處理支援中日韓的多語言內容 +- **批次處理**:為下游處理節點生成列表 +- **隨機抽樣**:創建隨機化的內容選擇 + +### Performance Considerations / 性能考慮 +- **Memory Usage**: Large audio files and long text strings may require significant RAM +- **Processing Speed**: Regular expressions may be slower than simple string operations +- **File Formats**: AudioListCombine supports all formats compatible with torchaudio + +### 中文技術說明 +- **記憶體使用**:大型音頻文件和長文本字符串可能需要大量 RAM +- **處理速度**:正規表示式可能比簡單字符串操作慢 +- **文件格式**:AudioListCombine 支援所有與 torchaudio 兼容的格式 + +### Common Issues / 常見問題 + +**Audio list is empty / 音頻列表為空** +- Ensure list creation nodes have connected inputs / 確保列表創建節點已連接輸入 + +**Regular expression errors / 正規表示式錯誤** +- Check pattern syntax, node will fallback to string mode / 檢查模式語法,節點將回退到字符串模式 + +**Memory issues with large files / 大文件記憶體問題** +- Process files in smaller batches / 以較小批次處理文件 + +## AudioToFrameCount + +![Demo](readme/demo3.jpg) + +### 功能特色 + +**輸入音檔以串接圖片數目**:依照所需影片格數計算音檔長度,輸入為音檔,輸出為一固定值,可用於重複單一圖片配合音檔長度 + +## CeilDivide + +![Demo](readme/demo4.jpg) + +### 功能特色 + +**無條件進位**:將AB相除結果無條件進位,以避免因尾數被捨去迴圈數目不足,導致多段影片或音檔分離時,末段音檔未被採樣 + +## AudioSplitToList + +![Demo](readme/demo5.jpg) + +### 功能特色 + +**分割音檔為清單**:將聲音檔分割為清單,長篇數字人時可分段採樣。 + +**必需輸入:** +- **videofps / 畫格** (Float): 每秒多少格 +- **samplefps / 分段採樣格** (Int): 每段採樣多少格,範例,如果是25格,分段採樣格是75,則音檔將會分隔為每三秒一個單位 +- **pad_last_segment / 補足音檔** (Boolean): 將音檔長度插入空白,對齊最後一格的分段採樣 (預設: False) + +**輸出:** +- **cycle / 整數**: 分割為多少段 +- **audio_list / 音檔清單**: 分割完成的音檔,可直接放入audio輸入,會依序處理 + + +## License / 授權 + +MIT License + +## Contributing / 貢獻 + +歡迎提交 Issue 和 Pull Request! +Welcome to submit Issues and Pull Requests! + +## Support / 支援 + +如有問題請在 GitHub Issues 中回報。 +For questions, please report in GitHub Issues. \ No newline at end of file diff --git a/Readme/demo.jpg b/Readme/demo.jpg new file mode 100644 index 0000000..263c95f Binary files /dev/null and b/Readme/demo.jpg differ diff --git a/Readme/demo1.jpg b/Readme/demo1.jpg new file mode 100644 index 0000000..228436b Binary files /dev/null and b/Readme/demo1.jpg differ diff --git a/Readme/demo2.jpg b/Readme/demo2.jpg new file mode 100644 index 0000000..2421778 Binary files /dev/null and b/Readme/demo2.jpg differ diff --git a/Readme/demo3.jpg b/Readme/demo3.jpg new file mode 100644 index 0000000..7b2d7b2 Binary files /dev/null and b/Readme/demo3.jpg differ diff --git a/Readme/demo4.jpg b/Readme/demo4.jpg new file mode 100644 index 0000000..a0f69b0 Binary files /dev/null and b/Readme/demo4.jpg differ diff --git a/Readme/demo5.jpg b/Readme/demo5.jpg new file mode 100644 index 0000000..878b912 Binary files /dev/null and b/Readme/demo5.jpg differ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d721463 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/nodes.py b/nodes.py new file mode 100644 index 0000000..b26a5f2 --- /dev/null +++ b/nodes.py @@ -0,0 +1,841 @@ +import copy +import torch +import torch.nn.functional as F +import os +import re +import sys +import json +import math +import subprocess +import codecs +import time +import ffmpeg +import datetime +import random as rnd +import torchaudio +import folder_paths +import json +from comfy.comfy_types import IO +from comfy_api.input_impl import VideoFromFile, VideoFromComponents +from comfy_api.util import VideoContainer, VideoCodec, VideoComponents +from fractions import Fraction +from typing import Optional +from comfy.cli_args import args +from typing import List, Dict, Any, Tuple +from random import Random +from datetime import datetime + +class AudioListGenerator: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "waveform": ("AUDIO",), + "videofps": ("FLOAT", {"default": 23.976, "min": 1.0, "step": 0.001}), + "samplefps": ("INT", {"default": 81, "min": 1}), + "pad_last_segment": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("INT", "AUDIO",) + OUTPUT_IS_LIST = (False, True) + RETURN_NAMES = ("cycle", "audio_list") + FUNCTION = "split" + CATEGORY = "ListHelper" + + def split(self, waveform, videofps, samplefps, pad_last_segment): + audio_tensor = waveform["waveform"] # shape: [1, C, N] + sample_rate = waveform["sample_rate"] + total_samples = audio_tensor.shape[-1] + + segment_duration_seconds = samplefps / videofps + samples_per_segment = int(segment_duration_seconds * sample_rate) + + audio_list = [] + + for i in range(0, total_samples, samples_per_segment): + end_idx = min(i + samples_per_segment, total_samples) + segment = audio_tensor[:, :, i:end_idx].clone() + + segment_len = segment.shape[-1] + if pad_last_segment and segment_len < samples_per_segment: + pad_len = samples_per_segment - segment_len + segment = F.pad(segment, (0, pad_len)) + + audio_obj = { + "waveform": segment, + "sample_rate": sample_rate + } + + audio_list.append(copy.deepcopy(audio_obj)) + + return len(audio_list), audio_list + +class AudioToFrameCount: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "audio": ("AUDIO",), + "fps": ("FLOAT", {"default": 25.0, "min": 1.0, "step": 0.001}), + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("frames",) + FUNCTION = "calculate" + CATEGORY = "ListHelper" + + def calculate(self, audio, fps): + waveform = audio["waveform"] # shape: [1, channels, samples] + sample_rate = audio["sample_rate"] # e.g., 44100 + + total_samples = waveform.shape[-1] + duration_sec = total_samples / sample_rate + total_frames = int(duration_sec * fps) + + return (total_frames,) + + +class MergeVideoFilename: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_text": ("STRING", {"multiline": True, "default": ""}), + "total_rounds": ("INT", {"default": 3, "min": 1, "max": 20}), + "windows_path_format": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("merged_file_path",) + FUNCTION = "merge_files" + CATEGORY = "ListHelper" + + def __init__(self): + # 使用相對路徑的狀態檔案 + self.state_file = "merge_state.json" + + def merge_files(self, input_text, total_rounds, windows_path_format): + # 讀取已累積的檔案 + try: + with open(self.state_file, 'r', encoding='utf-8') as f: + state_data = json.load(f) + accumulated_files = state_data.get('files', []) + file_type = state_data.get('type', None) + except: + accumulated_files = [] + file_type = None + + # 檢查輸入是否包含-audio.mp4檔案 + audio_files = re.findall(r"'([^']*-audio\.mp4)'", input_text) + video_files = re.findall(r"'([^']*\.mp4)'", input_text) + + # 移除audio檔案,只保留純video檔案 + pure_video_files = [f for f in video_files if not f.endswith('-audio.mp4')] + + # 決定要處理的檔案類型 + if audio_files: + current_files = audio_files + current_type = 'audio' + elif pure_video_files: + current_files = pure_video_files + current_type = 'video' + else: + return ("未找到有效的檔案",) + + # 如果檔案類型改變,重置累積的檔案 + if file_type and file_type != current_type: + accumulated_files = [] + + file_type = current_type + + # 累積新檔案 + for file in current_files: + if file not in accumulated_files: + accumulated_files.append(file) + + # 儲存狀態 + state_data = { + 'files': accumulated_files, + 'type': file_type + } + with open(self.state_file, 'w', encoding='utf-8') as f: + json.dump(state_data, f, ensure_ascii=False, indent=2) + + # 如果只有一個檔案,直接返回 + if total_rounds == 1 and len(accumulated_files) >= 1: + file_path = accumulated_files[0] + # 清空狀態檔案 + if os.path.exists(self.state_file): + os.remove(self.state_file) + return (self._format_path(file_path, windows_path_format),) + + # 當累積到指定數量的檔案時合併 + if len(accumulated_files) >= total_rounds: + output_dir = os.path.dirname(accumulated_files[0]) + + # 產生帶時間戳的檔案名 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + if file_type == 'audio': + output_filename = f"merged_audio_{timestamp}.mp4" + else: + output_filename = f"merged_video_{timestamp}.mp4" + + output_file = os.path.join(output_dir, output_filename) + + # 構建ffmpeg命令 + cmd = ["ffmpeg", "-y"] + for file in accumulated_files: + cmd.extend(["-i", file]) + + # 根據檔案類型選擇不同的合併參數 + if file_type == 'audio': + # 有音軌的合併 + filter_complex = "".join([f"[{i}:v][{i}:a]" for i in range(len(accumulated_files))]) + filter_complex += f"concat=n={len(accumulated_files)}:v=1:a=1[outv][outa]" + cmd.extend(["-filter_complex", filter_complex, "-map", "[outv]", "-map", "[outa]"]) + else: + # 無音軌的合併 + filter_complex = "".join([f"[{i}:v]" for i in range(len(accumulated_files))]) + filter_complex += f"concat=n={len(accumulated_files)}:v=1:a=0[outv]" + cmd.extend(["-filter_complex", filter_complex, "-map", "[outv]"]) + + cmd.append(output_file) + + try: + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"FFmpeg error: {result.stderr}") + return (f"合併失敗: {result.stderr}",) + except Exception as e: + return (f"執行FFmpeg時發生錯誤: {str(e)}",) + + # 清空狀態檔案 + if os.path.exists(self.state_file): + os.remove(self.state_file) + + return (self._format_path(output_file, windows_path_format),) + else: + # 返回空字串或佔位符,直到收集完成 + return ("",) + + def _format_path(self, file_path, windows_format): + """格式化檔案路徑""" + if windows_format: + # Windows格式:先統一為正斜線,再轉換為單反斜線 + normalized_path = file_path.replace('\\\\', '/').replace('\\', '/') + return normalized_path.replace('/', '\\') + else: + # 雙斜線格式:確保使用雙反斜線 + return file_path.replace('\\', '\\\\').replace('/', '\\\\') + + +class PromptListGenerator: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "text": ("STRING", {"multiline": True, "dynamicPrompts": False}), + "delimiter": ("STRING", {"multiline": False, "default": ",", "dynamicPrompts": False}), + "use_regex": ("BOOLEAN", {"default": False}), + "keep_delimiter": ("BOOLEAN", {"default": False}), + "start_index": ("INT", {"default": 0, "min": 0, "max": 1000}), + "skip_every": ("INT", {"default": 0, "min": 0, "max": 10}), + "max_count": ("INT", {"default": 10, "min": 1, "max": 1000}), + "skip_first_index": ("BOOLEAN", {"default": False}), + "random_order": ("BOOLEAN", {"default": False}), + "seed": ("INT", {"default": 0, "min": 0, "max": 2147483647}), + } + } + + INPUT_IS_LIST = False + RETURN_TYPES = ("STRING", "INT") + RETURN_NAMES = ("text_list", "total_index") + FUNCTION = "run" + OUTPUT_IS_LIST = (True, False) + CATEGORY = "ListHelper" + + def run(self, text, delimiter, use_regex, keep_delimiter, start_index, skip_every, max_count, skip_first_index, random_order, seed): + # 處理多個換行符號為一個換行符號 + text = re.sub(r'\n+', '\n', text) + + # 如果delimiter為空,則使用換行符號作為分隔符 + if not delimiter.strip(): + delimiter = '\n' + # 直接使用delimiter進行搜尋分割,支援中日韓文字如"章"、"節"等 + + # 如果需要跳過第一個無分隔符號的部分 + if skip_first_index: + if use_regex: + # 使用正規表示式搜尋第一個匹配 + match = re.search(delimiter, text) + if match: + # 跳過第一個匹配之前的內容 + text = text[match.start():] + elif delimiter in text: + # 找到第一個分隔符號的位置 + first_delimiter_pos = text.find(delimiter) + if first_delimiter_pos > 0: + # 跳過第一個分隔符號之前的內容 + text = text[first_delimiter_pos:] + + # 分割文本 - 支援正規表示式或一般字符串,並可選擇保留分隔符 + if use_regex: + try: + if keep_delimiter: + # 使用正規表示式分割並保留分隔符 + arr = re.split(f'({delimiter})', text) + # 重新組合,讓每個片段都包含其前面的分隔符(除了第一個) + result = [] + for i in range(0, len(arr)): + if i == 0: + # 第一個片段 + if arr[i]: # 如果不為空 + result.append(arr[i]) + elif i % 2 == 1: + # 這是分隔符,與下一個片段合併 + if i + 1 < len(arr): + combined = arr[i] + arr[i + 1] + if combined.strip(): # 如果合併後不為空 + result.append(combined) + # i % 2 == 0 且 i > 0 的情況已經在上面處理過了 + arr = result + else: + # 使用正規表示式分割,不保留分隔符 + arr = re.split(delimiter, text) + except re.error: + # 如果正規表示式有錯誤,回退到一般字符串分割 + if keep_delimiter: + arr = self._split_with_delimiter(text, delimiter) + else: + arr = text.split(delimiter) + else: + # 使用一般字符串分割 + if keep_delimiter: + arr = self._split_with_delimiter(text, delimiter) + else: + arr = text.split(delimiter) + + # 過濾空白項目並去除首尾空格 + arr = [item.strip() for item in arr if item.strip()] + + # 計算總數 + total_index = len(arr) + + # 根據random_order參數決定是否隨機排序 + if arr: + if random_order: + # 使用種子創建隨機數生成器並打亂順序 + rng = Random(seed) + rng.shuffle(arr) + + # 根據參數選取項目 + selected_arr = arr[start_index:start_index + max_count * (skip_every + 1):(skip_every + 1)] + else: + selected_arr = [] + + return (selected_arr, total_index) + + def _split_with_delimiter(self, text, delimiter): + """輔助方法:用一般字符串分割並保留分隔符""" + if delimiter not in text: + return [text] if text.strip() else [] + + parts = text.split(delimiter) + result = [] + + for i, part in enumerate(parts): + if i == 0: + # 第一個部分 + if part.strip(): + result.append(part) + else: + # 其他部分都加上分隔符 + combined = delimiter + part + if combined.strip(): + result.append(combined) + + return result + + + +class NumberListGenerator: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "min_value": ("FLOAT", { + "default": 0.0, + "min": -10000.0, + "max": 10000.0, + "step": 0.01, + "display": "number" + }), + "max_value": ("FLOAT", { + "default": 10.0, + "min": -10000.0, + "max": 10000.0, + "step": 0.01, + "display": "number" + }), + "step": ("FLOAT", { + "default": 1.0, + "min": 0.01, + "max": 1000.0, + "step": 0.01, + "display": "number" + }), + "count": ("INT", { + "default": 10, + "min": 1, + "max": 10000, + "step": 1, + "display": "number" + }), + "random": ("BOOLEAN", { + "default": False + }) + }, + "optional": { + "seed": ("INT", { + "default": -1, + "min": -1, + "max": 1000000, + "step": 1, + "display": "number" + }) + } + } + + RETURN_TYPES = ("INT", "FLOAT", "INT") + RETURN_NAMES = ("int_list", "float_list", "total_count") + FUNCTION = "generate_number_list" + CATEGORY = "ListHelper" + INPUT_IS_LIST = False + OUTPUT_IS_LIST = (True, True, False) + + def generate_number_list(self, min_value, max_value, step, count, random, seed=-1): + """ + 生成數字列表 + + Args: + min_value: 起始值 + max_value: 最大值 + step: 步長 + count: 數量 + random: 是否隨機排列 + seed: 隨機種子 + """ + print(f"Generating number list - min: {min_value}, max: {max_value}, step: {step}, count: {count}, random: {random}, seed: {seed}") + + # 生成基礎數字列表 + float_list = [] + current_value = min_value + + for i in range(count): + if current_value > max_value: + break + float_list.append(current_value) + current_value += step + + # 生成整數列表 + int_list = [int(val) for val in float_list] + + # 如果啟用隨機排列 + if random: + # 設定隨機種子 + if seed >= 0: + rnd.seed(seed) + + # 隨機打亂兩個列表(保持對應關係) + combined = list(zip(int_list, float_list)) + rnd.shuffle(combined) + int_list, float_list = zip(*combined) + int_list = list(int_list) + float_list = list(float_list) + + # 總數量 + total_count = len(float_list) + + print(f"Generated {total_count} numbers") + return (int_list, float_list, total_count) + + +def create_number_list(min_value, max_value, step, count, random=False, seed=-1): + """ + 獨立的數字列表生成函數 + """ + node = NumberListGeneratorNode() + return node.generate_number_list(min_value, max_value, step, count, random, seed) + +class AudioListCombine: + """ + 合併音檔清單為單一音檔的節點 + 將多個音檔按順序串接,或進行混音處理 + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "audio_list": ("AUDIO",), # 接收音檔清單 + "combine_mode": (["concatenate", "mix", "overlay"], {"default": "concatenate"}), + }, + "optional": { + "fade_duration": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 5.0, "step": 0.1}), + "normalize_output": ("BOOLEAN", {"default": True}), + "target_sample_rate": ("INT", {"default": 44100, "min": 8000, "max": 192000}), + } + } + + RETURN_TYPES = ("AUDIO",) + FUNCTION = "combine_audio_list" + CATEGORY = "listhelper" + + # 標記此節點接收清單輸入 + INPUT_IS_LIST = True + + def combine_audio_list(self, audio_list: List[Dict], combine_mode: List[str], + fade_duration: List[float] = [0.0], + normalize_output: List[bool] = [True], + target_sample_rate: List[int] = [44100]) -> Tuple[Dict]: + """ + 合併音檔清單 + + Args: + audio_list: 音檔清單,每個元素包含 'waveform' 和 'sample_rate' + combine_mode: 合併模式 - concatenate(串接), mix(混音), overlay(覆疊) + fade_duration: 淡入淡出時長(秒) + normalize_output: 是否標準化輸出 + target_sample_rate: 目標採樣率 + + Returns: + 合併後的音檔字典 + """ + + # 取得參數(因為 INPUT_IS_LIST=True,所有參數都是清單) + mode = combine_mode[0] + fade_dur = fade_duration[0] + normalize = normalize_output[0] + target_sr = target_sample_rate[0] + + if not audio_list: + raise ValueError("音檔清單不能為空") + + # 預處理:統一採樣率和聲道數 + processed_audio = [] + for audio_dict in audio_list: + waveform = audio_dict['waveform'] # [B, C, T] + sample_rate = audio_dict['sample_rate'] + + # 重新採樣到目標採樣率 + if sample_rate != target_sr: + resampler = torchaudio.transforms.Resample(sample_rate, target_sr) + waveform = resampler(waveform) + + processed_audio.append(waveform) + + # 統一聲道數(取最大聲道數) + max_channels = max(audio.shape[1] for audio in processed_audio) + for i, audio in enumerate(processed_audio): + if audio.shape[1] < max_channels: + # 單聲道轉雙聲道或補齊聲道 + if audio.shape[1] == 1 and max_channels == 2: + processed_audio[i] = audio.repeat(1, 2, 1) + else: + # 用零填充缺少的聲道 + pad_channels = max_channels - audio.shape[1] + padding = torch.zeros(audio.shape[0], pad_channels, audio.shape[2]) + processed_audio[i] = torch.cat([audio, padding], dim=1) + + # 根據模式合併音檔 + if mode == "concatenate": + combined_waveform = self._concatenate_audio(processed_audio, fade_dur) + elif mode == "mix": + combined_waveform = self._mix_audio(processed_audio) + elif mode == "overlay": + combined_waveform = self._overlay_audio(processed_audio) + else: + raise ValueError(f"不支援的合併模式: {mode}") + + # 標準化輸出 + if normalize: + combined_waveform = self._normalize_audio(combined_waveform) + + # 確保輸出格式正確 + if combined_waveform.dim() == 2: + combined_waveform = combined_waveform.unsqueeze(0) # 添加批次維度 + + result_dict = { + 'waveform': combined_waveform, + 'sample_rate': target_sr + } + + return (result_dict,) + + def _concatenate_audio(self, audio_list: List[torch.Tensor], fade_duration: float) -> torch.Tensor: + """串接音檔""" + if len(audio_list) == 1: + return audio_list[0] + + result = audio_list[0] + + for next_audio in audio_list[1:]: + if fade_duration > 0: + result = self._crossfade_concat(result, next_audio, fade_duration) + else: + result = torch.cat([result, next_audio], dim=2) # 在時間維度串接 + + return result + + def _mix_audio(self, audio_list: List[torch.Tensor]) -> torch.Tensor: + """混音(平均)""" + # 找出最長的音檔長度 + max_length = max(audio.shape[2] for audio in audio_list) + batch_size = audio_list[0].shape[0] + channels = audio_list[0].shape[1] + + # 將所有音檔填充到相同長度 + padded_audio = [] + for audio in audio_list: + if audio.shape[2] < max_length: + padding = torch.zeros(batch_size, channels, max_length - audio.shape[2]) + audio = torch.cat([audio, padding], dim=2) + padded_audio.append(audio) + + # 疊加並平均 + mixed = torch.stack(padded_audio, dim=0).mean(dim=0) + return mixed + + def _overlay_audio(self, audio_list: List[torch.Tensor]) -> torch.Tensor: + """覆疊音檔(直接相加)""" + # 找出最長的音檔長度 + max_length = max(audio.shape[2] for audio in audio_list) + batch_size = audio_list[0].shape[0] + channels = audio_list[0].shape[1] + + # 初始化結果張量 + result = torch.zeros(batch_size, channels, max_length) + + # 逐個添加音檔 + for audio in audio_list: + result[:, :, :audio.shape[2]] += audio + + return result + + def _crossfade_concat(self, audio1: torch.Tensor, audio2: torch.Tensor, + fade_duration: float, sample_rate: int = 44100) -> torch.Tensor: + """交叉淡化串接""" + fade_samples = int(fade_duration * sample_rate) + + if fade_samples == 0 or audio1.shape[2] < fade_samples: + return torch.cat([audio1, audio2], dim=2) + + # 創建淡出和淡入曲線 + fade_out = torch.linspace(1.0, 0.0, fade_samples).unsqueeze(0).unsqueeze(0) + fade_in = torch.linspace(0.0, 1.0, fade_samples).unsqueeze(0).unsqueeze(0) + + # 分割音檔 + audio1_main = audio1[:, :, :-fade_samples] + audio1_tail = audio1[:, :, -fade_samples:] + + if audio2.shape[2] >= fade_samples: + audio2_head = audio2[:, :, :fade_samples] + audio2_main = audio2[:, :, fade_samples:] + else: + audio2_head = audio2 + audio2_main = torch.zeros(audio2.shape[0], audio2.shape[1], 0) + + # 應用交叉淡化 + crossfade_section = audio1_tail * fade_out + audio2_head * fade_in + + # 合併結果 + result = torch.cat([audio1_main, crossfade_section, audio2_main], dim=2) + return result + + def _normalize_audio(self, waveform: torch.Tensor) -> torch.Tensor: + """標準化音檔到 [-1, 1] 範圍""" + max_val = waveform.abs().max() + if max_val > 0: + return waveform / max_val + return waveform + +class CeilDivide: + """ + 將 a/b 的結果無條件進位為整數 + 例如: 21.02 -> 22, 21.99 -> 22, 21.00 -> 21 + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "a": ("INT", {"default": 1, "min": -999999, "max": 999999}), + "b": ("INT", {"default": 1, "min": -999999, "max": 999999}), + } + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("result",) + FUNCTION = "ceil_divide" + CATEGORY = "ListHelper" + + def ceil_divide(self, a: int, b: int) -> tuple: + """ + 計算 a/b 並無條件進位為整數 + + Args: + a: 被除數 + b: 除數 + + Returns: + 無條件進位後的整數結果 + """ + if b == 0: + raise ValueError("除數不能為零") + + # 計算除法結果 + division_result = a / b + + # 使用 math.ceil 進行無條件進位 + result = math.ceil(division_result) + + return (result,) + +class LoadVideoPath: + """ + 載入視頻檔案,輸出視頻物件和完整檔案路徑 + """ + + @classmethod + def INPUT_TYPES(cls): + input_dir = folder_paths.get_input_directory() + files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))] + files = folder_paths.filter_files_content_types(files, ["video"]) + return { + "required": { + "file": (sorted(files), {"video_upload": True}), + } + } + + CATEGORY = "ListHelper" + RETURN_TYPES = (IO.VIDEO, "STRING") + RETURN_NAMES = ("video", "path") + FUNCTION = "load_video_path" + + def load_video_path(self, file): + video_path = folder_paths.get_annotated_filepath(file) + video_object = VideoFromFile(video_path) + return (video_object, video_path) + + @classmethod + def IS_CHANGED(cls, file): + video_path = folder_paths.get_annotated_filepath(file) + return os.path.getmtime(video_path) + + @classmethod + def VALIDATE_INPUTS(cls, file): + if not folder_paths.exists_annotated_filepath(file): + return f"Invalid video file: {file}" + return True + + +class SaveVideoPath: + """ + 保存視頻檔案,輸出保存後的完整檔案路徑 + """ + + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "video": (IO.VIDEO, {"tooltip": "要保存的視頻"}), + "filename_prefix": ("STRING", {"default": "video/ComfyUI", + "tooltip": "檔案名前綴"}), + "format": (VideoContainer.as_input(), {"default": "auto", + "tooltip": "視頻格式"}), + "codec": (VideoCodec.as_input(), {"default": "auto", + "tooltip": "視頻編碼"}), + }, + "hidden": { + "prompt": "PROMPT", + "extra_pnginfo": "EXTRA_PNGINFO" + }, + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("path",) + FUNCTION = "save_video_path" + OUTPUT_NODE = True + CATEGORY = "ListHelper" + + def save_video_path(self, video, filename_prefix, format, codec, + prompt=None, extra_pnginfo=None): + filename_prefix += self.prefix_append + width, height = video.get_dimensions() + + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path( + filename_prefix, + self.output_dir, + width, + height + ) + + # 準備元數據 + saved_metadata = None + if not args.disable_metadata: + metadata = {} + if extra_pnginfo is not None: + metadata.update(extra_pnginfo) + if prompt is not None: + metadata["prompt"] = prompt + if len(metadata) > 0: + saved_metadata = metadata + + # 生成檔案名和完整路徑 + file = f"{filename}_{counter:05}_.{VideoContainer.get_extension(format)}" + full_path = os.path.join(full_output_folder, file) + + # 保存視頻 + video.save_to( + full_path, + format=format, + codec=codec, + metadata=saved_metadata + ) + + return (full_path,) + + + +NODE_CLASS_MAPPINGS = { + "AudioListGenerator": AudioListGenerator, + "AudioToFrameCount": AudioToFrameCount, + "MergeVideoFilename": MergeVideoFilename, + "PromptListGenerator": PromptListGenerator, + "NumberListGenerator": NumberListGenerator, + "AudioListCombine": AudioListCombine, + "CeilDivide": CeilDivide, + "LoadVideoPath": LoadVideoPath, + "SaveVideoPath": SaveVideoPath, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "AudioListGenerator": "Audio Split to List", + "AudioToFrameCount": "Audio to Frame Count", + "MergeVideoFilename": "MergeVideoFilename", + "PromptListGenerator": "PromptListGenerator", + "NumberListGenerator": "NumberListGenerator", + "AudioListCombine": "AudioListCombine", + "CeilDivide": "CeilDivide", + "LoadVideoPath": "LoadVideoPath", + "SaveVideoPath": "SaveVideoPath", +} +