diff --git a/comfy/ldm/ace/vae/music_vocoder.py b/comfy/ldm/ace/vae/music_vocoder.py index dc7c867daeb5..2f989fa86e8c 100755 --- a/comfy/ldm/ace/vae/music_vocoder.py +++ b/comfy/ldm/ace/vae/music_vocoder.py @@ -8,11 +8,7 @@ import numpy as np import torch.nn.functional as F -from torch.nn.utils import weight_norm from torch.nn.utils.parametrize import remove_parametrizations as remove_weight_norm -# from diffusers.models.modeling_utils import ModelMixin -# from diffusers.loaders import FromOriginalModelMixin -# from diffusers.configuration_utils import ConfigMixin, register_to_config from .music_log_mel import LogMelSpectrogram @@ -259,7 +255,7 @@ def __init__(self, channels, kernel_size=3, dilation=(1, 3, 5)): self.convs1 = nn.ModuleList( [ - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( channels, channels, @@ -269,7 +265,7 @@ def __init__(self, channels, kernel_size=3, dilation=(1, 3, 5)): padding=get_padding(kernel_size, dilation[0]), ) ), - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( channels, channels, @@ -279,7 +275,7 @@ def __init__(self, channels, kernel_size=3, dilation=(1, 3, 5)): padding=get_padding(kernel_size, dilation[1]), ) ), - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( channels, channels, @@ -294,7 +290,7 @@ def __init__(self, channels, kernel_size=3, dilation=(1, 3, 5)): self.convs2 = nn.ModuleList( [ - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( channels, channels, @@ -304,7 +300,7 @@ def __init__(self, channels, kernel_size=3, dilation=(1, 3, 5)): padding=get_padding(kernel_size, 1), ) ), - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( channels, channels, @@ -314,7 +310,7 @@ def __init__(self, channels, kernel_size=3, dilation=(1, 3, 5)): padding=get_padding(kernel_size, 1), ) ), - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( channels, channels, @@ -366,7 +362,7 @@ def __init__( prod(upsample_rates) == hop_length ), f"hop_length must be {prod(upsample_rates)}" - self.conv_pre = weight_norm( + self.conv_pre = torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( num_mels, upsample_initial_channel, @@ -386,7 +382,7 @@ def __init__( for i, (u, k) in enumerate(zip(upsample_rates, upsample_kernel_sizes)): c_cur = upsample_initial_channel // (2 ** (i + 1)) self.ups.append( - weight_norm( + torch.nn.utils.parametrizations.weight_norm( ops.ConvTranspose1d( upsample_initial_channel // (2**i), upsample_initial_channel // (2 ** (i + 1)), @@ -421,7 +417,7 @@ def __init__( self.resblocks.append(ResBlock1(ch, k, d)) self.activation_post = post_activation() - self.conv_post = weight_norm( + self.conv_post = torch.nn.utils.parametrizations.weight_norm( ops.Conv1d( ch, 1, diff --git a/comfy/ldm/audio/autoencoder.py b/comfy/ldm/audio/autoencoder.py index 9e7e7c87602f..78ed6ffa63ab 100644 --- a/comfy/ldm/audio/autoencoder.py +++ b/comfy/ldm/audio/autoencoder.py @@ -75,16 +75,10 @@ def forward(self, x): return x def WNConv1d(*args, **kwargs): - try: - return torch.nn.utils.parametrizations.weight_norm(ops.Conv1d(*args, **kwargs)) - except: - return torch.nn.utils.weight_norm(ops.Conv1d(*args, **kwargs)) #support pytorch 2.1 and older + return torch.nn.utils.parametrizations.weight_norm(ops.Conv1d(*args, **kwargs)) def WNConvTranspose1d(*args, **kwargs): - try: - return torch.nn.utils.parametrizations.weight_norm(ops.ConvTranspose1d(*args, **kwargs)) - except: - return torch.nn.utils.weight_norm(ops.ConvTranspose1d(*args, **kwargs)) #support pytorch 2.1 and older + return torch.nn.utils.parametrizations.weight_norm(ops.ConvTranspose1d(*args, **kwargs)) def get_activation(activation: Literal["elu", "snake", "none"], antialias=False, channels=None) -> nn.Module: if activation == "elu": diff --git a/comfy_api_nodes/nodes_kling.py b/comfy_api_nodes/nodes_kling.py index b2be83656a2f..2d0fd8883316 100644 --- a/comfy_api_nodes/nodes_kling.py +++ b/comfy_api_nodes/nodes_kling.py @@ -671,7 +671,6 @@ def api_call( negative_prompt=negative_prompt if negative_prompt else None, cfg_scale=cfg_scale, mode=KlingVideoGenMode(mode), - aspect_ratio=KlingVideoGenAspectRatio(aspect_ratio), duration=KlingVideoGenDuration(duration), camera_control=camera_control, ), diff --git a/comfy_extras/nodes_audio.py b/comfy_extras/nodes_audio.py index 136ad6159b8c..49af1eae43aa 100644 --- a/comfy_extras/nodes_audio.py +++ b/comfy_extras/nodes_audio.py @@ -1,5 +1,6 @@ from __future__ import annotations +import av import torchaudio import torch import comfy.model_management @@ -7,7 +8,6 @@ import os import io import json -import struct import random import hashlib import node_helpers @@ -90,61 +90,143 @@ def decode(self, vae, samples): return ({"waveform": audio, "sample_rate": 44100}, ) -def create_vorbis_comment_block(comment_dict, last_block): - vendor_string = b'ComfyUI' - vendor_length = len(vendor_string) +def save_audio(self, audio, filename_prefix="ComfyUI", format="flac", prompt=None, extra_pnginfo=None, quality="128k"): - comments = [] - for key, value in comment_dict.items(): - comment = f"{key}={value}".encode('utf-8') - comments.append(struct.pack('I', len(comment_data))[1:] + comment_data + # Opus supported sample rates + OPUS_RATES = [8000, 12000, 16000, 24000, 48000] - return comment_block + for (batch_number, waveform) in enumerate(audio["waveform"].cpu()): + filename_with_batch_num = filename.replace("%batch_num%", str(batch_number)) + file = f"{filename_with_batch_num}_{counter:05}_.{format}" + output_path = os.path.join(full_output_folder, file) -def insert_or_replace_vorbis_comment(flac_io, comment_dict): - if len(comment_dict) == 0: - return flac_io - - flac_io.seek(4) + # Use original sample rate initially + sample_rate = audio["sample_rate"] - blocks = [] - last_block = False + # Handle Opus sample rate requirements + if format == "opus": + if sample_rate > 48000: + sample_rate = 48000 + elif sample_rate not in OPUS_RATES: + # Find the next highest supported rate + for rate in sorted(OPUS_RATES): + if rate > sample_rate: + sample_rate = rate + break + if sample_rate not in OPUS_RATES: # Fallback if still not supported + sample_rate = 48000 + + # Resample if necessary + if sample_rate != audio["sample_rate"]: + waveform = torchaudio.functional.resample(waveform, audio["sample_rate"], sample_rate) + + # Create in-memory WAV buffer + wav_buffer = io.BytesIO() + torchaudio.save(wav_buffer, waveform, sample_rate, format="WAV") + wav_buffer.seek(0) # Rewind for reading + + # Use PyAV to convert and add metadata + input_container = av.open(wav_buffer) + + # Create output with specified format + output_buffer = io.BytesIO() + output_container = av.open(output_buffer, mode='w', format=format) + + # Set metadata on the container + for key, value in metadata.items(): + output_container.metadata[key] = value + + # Set up the output stream with appropriate properties + input_container.streams.audio[0] + if format == "opus": + out_stream = output_container.add_stream("libopus", rate=sample_rate) + if quality == "64k": + out_stream.bit_rate = 64000 + elif quality == "96k": + out_stream.bit_rate = 96000 + elif quality == "128k": + out_stream.bit_rate = 128000 + elif quality == "192k": + out_stream.bit_rate = 192000 + elif quality == "320k": + out_stream.bit_rate = 320000 + elif format == "mp3": + out_stream = output_container.add_stream("libmp3lame", rate=sample_rate) + if quality == "V0": + #TODO i would really love to support V3 and V5 but there doesn't seem to be a way to set the qscale level, the property below is a bool + out_stream.codec_context.qscale = 1 + elif quality == "128k": + out_stream.bit_rate = 128000 + elif quality == "320k": + out_stream.bit_rate = 320000 + else: #format == "flac": + out_stream = output_container.add_stream("flac", rate=sample_rate) + + + # Copy frames from input to output + for frame in input_container.decode(audio=0): + frame.pts = None # Let PyAV handle timestamps + output_container.mux(out_stream.encode(frame)) + + # Flush encoder + output_container.mux(out_stream.encode(None)) + + # Close containers + output_container.close() + input_container.close() + + # Write the output to file + output_buffer.seek(0) + with open(output_path, 'wb') as f: + f.write(output_buffer.getbuffer()) + + results.append({ + "filename": file, + "subfolder": subfolder, + "type": self.type + }) + counter += 1 + + return { "ui": { "audio": results } } - while not last_block: - header = flac_io.read(4) - last_block = (header[0] & 0x80) != 0 - block_type = header[0] & 0x7F - block_length = struct.unpack('>I', b'\x00' + header[1:])[0] - block_data = flac_io.read(block_length) +class SaveAudio: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" - if block_type == 4 or block_type == 1: - pass - else: - header = bytes([(header[0] & (~0x80))]) + header[1:] - blocks.append(header + block_data) + @classmethod + def INPUT_TYPES(s): + return {"required": { "audio": ("AUDIO", ), + "filename_prefix": ("STRING", {"default": "audio/ComfyUI"}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } - blocks.append(create_vorbis_comment_block(comment_dict, last_block=True)) + RETURN_TYPES = () + FUNCTION = "save_flac" - new_flac_io = io.BytesIO() - new_flac_io.write(b'fLaC') - for block in blocks: - new_flac_io.write(block) + OUTPUT_NODE = True - new_flac_io.write(flac_io.read()) - return new_flac_io + CATEGORY = "audio" + def save_flac(self, audio, filename_prefix="ComfyUI", format="flac", prompt=None, extra_pnginfo=None): + return save_audio(self, audio, filename_prefix, format, prompt, extra_pnginfo) -class SaveAudio: +class SaveAudioMP3: def __init__(self): self.output_dir = folder_paths.get_output_directory() self.type = "output" @@ -153,50 +235,46 @@ def __init__(self): @classmethod def INPUT_TYPES(s): return {"required": { "audio": ("AUDIO", ), - "filename_prefix": ("STRING", {"default": "audio/ComfyUI"})}, + "filename_prefix": ("STRING", {"default": "audio/ComfyUI"}), + "quality": (["V0", "128k", "320k"], {"default": "V0"}), + }, "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, } RETURN_TYPES = () - FUNCTION = "save_audio" + FUNCTION = "save_mp3" OUTPUT_NODE = True CATEGORY = "audio" - def save_audio(self, audio, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None): - filename_prefix += self.prefix_append - full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir) - results: list[FileLocator] = [] - - metadata = {} - if not args.disable_metadata: - if prompt is not None: - metadata["prompt"] = json.dumps(prompt) - if extra_pnginfo is not None: - for x in extra_pnginfo: - metadata[x] = json.dumps(extra_pnginfo[x]) + def save_mp3(self, audio, filename_prefix="ComfyUI", format="mp3", prompt=None, extra_pnginfo=None, quality="128k"): + return save_audio(self, audio, filename_prefix, format, prompt, extra_pnginfo, quality) - for (batch_number, waveform) in enumerate(audio["waveform"].cpu()): - filename_with_batch_num = filename.replace("%batch_num%", str(batch_number)) - file = f"{filename_with_batch_num}_{counter:05}_.flac" +class SaveAudioOpus: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" - buff = io.BytesIO() - torchaudio.save(buff, waveform, audio["sample_rate"], format="FLAC") + @classmethod + def INPUT_TYPES(s): + return {"required": { "audio": ("AUDIO", ), + "filename_prefix": ("STRING", {"default": "audio/ComfyUI"}), + "quality": (["64k", "96k", "128k", "192k", "320k"], {"default": "128k"}), + }, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } - buff = insert_or_replace_vorbis_comment(buff, metadata) + RETURN_TYPES = () + FUNCTION = "save_opus" - with open(os.path.join(full_output_folder, file), 'wb') as f: - f.write(buff.getbuffer()) + OUTPUT_NODE = True - results.append({ - "filename": file, - "subfolder": subfolder, - "type": self.type - }) - counter += 1 + CATEGORY = "audio" - return { "ui": { "audio": results } } + def save_opus(self, audio, filename_prefix="ComfyUI", format="opus", prompt=None, extra_pnginfo=None, quality="V3"): + return save_audio(self, audio, filename_prefix, format, prompt, extra_pnginfo, quality) class PreviewAudio(SaveAudio): def __init__(self): @@ -248,7 +326,20 @@ def VALIDATE_INPUTS(s, audio): "VAEEncodeAudio": VAEEncodeAudio, "VAEDecodeAudio": VAEDecodeAudio, "SaveAudio": SaveAudio, + "SaveAudioMP3": SaveAudioMP3, + "SaveAudioOpus": SaveAudioOpus, "LoadAudio": LoadAudio, "PreviewAudio": PreviewAudio, "ConditioningStableAudio": ConditioningStableAudio, } + +NODE_DISPLAY_NAME_MAPPINGS = { + "EmptyLatentAudio": "Empty Latent Audio", + "VAEEncodeAudio": "VAE Encode Audio", + "VAEDecodeAudio": "VAE Decode Audio", + "PreviewAudio": "Preview Audio", + "LoadAudio": "Load Audio", + "SaveAudio": "Save Audio (FLAC)", + "SaveAudioMP3": "Save Audio (MP3)", + "SaveAudioOpus": "Save Audio (Opus)", +} diff --git a/comfy_extras/nodes_load_3d.py b/comfy_extras/nodes_load_3d.py index 53d892bc480c..d5b4d9111aaf 100644 --- a/comfy_extras/nodes_load_3d.py +++ b/comfy_extras/nodes_load_3d.py @@ -2,6 +2,10 @@ import folder_paths import os +from comfy.comfy_types import IO +from comfy_api.input_impl import VideoFromFile + + def normalize_path(path): return path.replace('\\', '/') @@ -21,8 +25,8 @@ def INPUT_TYPES(s): "height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), }} - RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE", "IMAGE", "LOAD3D_CAMERA") - RETURN_NAMES = ("image", "mask", "mesh_path", "normal", "lineart", "camera_info") + RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE", "IMAGE", "LOAD3D_CAMERA", IO.VIDEO) + RETURN_NAMES = ("image", "mask", "mesh_path", "normal", "lineart", "camera_info", "recording_video") FUNCTION = "process" EXPERIMENTAL = True @@ -41,7 +45,14 @@ def process(self, model_file, image, **kwargs): normal_image, ignore_mask2 = load_image_node.load_image(image=normal_path) lineart_image, ignore_mask3 = load_image_node.load_image(image=lineart_path) - return output_image, output_mask, model_file, normal_image, lineart_image, image['camera_info'] + video = None + + if image['recording'] != "": + recording_video_path = folder_paths.get_annotated_filepath(image['recording']) + + video = VideoFromFile(recording_video_path) + + return output_image, output_mask, model_file, normal_image, lineart_image, image['camera_info'], video class Load3DAnimation(): @classmethod @@ -59,8 +70,8 @@ def INPUT_TYPES(s): "height": ("INT", {"default": 1024, "min": 1, "max": 4096, "step": 1}), }} - RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE", "LOAD3D_CAMERA") - RETURN_NAMES = ("image", "mask", "mesh_path", "normal", "camera_info") + RETURN_TYPES = ("IMAGE", "MASK", "STRING", "IMAGE", "LOAD3D_CAMERA", IO.VIDEO) + RETURN_NAMES = ("image", "mask", "mesh_path", "normal", "camera_info", "recording_video") FUNCTION = "process" EXPERIMENTAL = True @@ -77,7 +88,14 @@ def process(self, model_file, image, **kwargs): ignore_image, output_mask = load_image_node.load_image(image=mask_path) normal_image, ignore_mask2 = load_image_node.load_image(image=normal_path) - return output_image, output_mask, model_file, normal_image, image['camera_info'] + video = None + + if image['recording'] != "": + recording_video_path = folder_paths.get_annotated_filepath(image['recording']) + + video = VideoFromFile(recording_video_path) + + return output_image, output_mask, model_file, normal_image, image['camera_info'], video class Preview3D(): @classmethod diff --git a/comfy_extras/nodes_string.py b/comfy_extras/nodes_string.py new file mode 100644 index 000000000000..a852326e5498 --- /dev/null +++ b/comfy_extras/nodes_string.py @@ -0,0 +1,322 @@ +import re + +from comfy.comfy_types.node_typing import IO + +class StringConcatenate(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string_a": (IO.STRING, {"multiline": True}), + "string_b": (IO.STRING, {"multiline": True}) + } + } + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string_a, string_b, **kwargs): + return string_a + string_b, + +class StringSubstring(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "start": (IO.INT, {}), + "end": (IO.INT, {}), + } + } + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, start, end, **kwargs): + return string[start:end], + +class StringLength(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}) + } + } + + RETURN_TYPES = (IO.INT,) + RETURN_NAMES = ("length",) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, **kwargs): + length = len(string) + + return length, + +class CaseConverter(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "mode": (IO.COMBO, {"options": ["UPPERCASE", "lowercase", "Capitalize", "Title Case"]}) + } + } + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, mode, **kwargs): + if mode == "UPPERCASE": + result = string.upper() + elif mode == "lowercase": + result = string.lower() + elif mode == "Capitalize": + result = string.capitalize() + elif mode == "Title Case": + result = string.title() + else: + result = string + + return result, + + +class StringTrim(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "mode": (IO.COMBO, {"options": ["Both", "Left", "Right"]}) + } + } + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, mode, **kwargs): + if mode == "Both": + result = string.strip() + elif mode == "Left": + result = string.lstrip() + elif mode == "Right": + result = string.rstrip() + else: + result = string + + return result, + +class StringReplace(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "find": (IO.STRING, {"multiline": True}), + "replace": (IO.STRING, {"multiline": True}) + } + } + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, find, replace, **kwargs): + result = string.replace(find, replace) + return result, + + +class StringContains(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "substring": (IO.STRING, {"multiline": True}), + "case_sensitive": (IO.BOOLEAN, {"default": True}) + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("contains",) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, substring, case_sensitive, **kwargs): + if case_sensitive: + contains = substring in string + else: + contains = substring.lower() in string.lower() + + return contains, + + +class StringCompare(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string_a": (IO.STRING, {"multiline": True}), + "string_b": (IO.STRING, {"multiline": True}), + "mode": (IO.COMBO, {"options": ["Starts With", "Ends With", "Equal"]}), + "case_sensitive": (IO.BOOLEAN, {"default": True}) + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string_a, string_b, mode, case_sensitive, **kwargs): + if case_sensitive: + a = string_a + b = string_b + else: + a = string_a.lower() + b = string_b.lower() + + if mode == "Equal": + return a == b, + elif mode == "Starts With": + return a.startswith(b), + elif mode == "Ends With": + return a.endswith(b), + +class RegexMatch(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "regex_pattern": (IO.STRING, {"multiline": True}), + "case_insensitive": (IO.BOOLEAN, {"default": True}), + "multiline": (IO.BOOLEAN, {"default": False}), + "dotall": (IO.BOOLEAN, {"default": False}) + } + } + + RETURN_TYPES = (IO.BOOLEAN,) + RETURN_NAMES = ("matches",) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, regex_pattern, case_insensitive, multiline, dotall, **kwargs): + flags = 0 + + if case_insensitive: + flags |= re.IGNORECASE + if multiline: + flags |= re.MULTILINE + if dotall: + flags |= re.DOTALL + + try: + match = re.search(regex_pattern, string, flags) + result = match is not None + + except re.error: + result = False + + return result, + + +class RegexExtract(): + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "string": (IO.STRING, {"multiline": True}), + "regex_pattern": (IO.STRING, {"multiline": True}), + "mode": (IO.COMBO, {"options": ["First Match", "All Matches", "First Group", "All Groups"]}), + "case_insensitive": (IO.BOOLEAN, {"default": True}), + "multiline": (IO.BOOLEAN, {"default": False}), + "dotall": (IO.BOOLEAN, {"default": False}), + "group_index": (IO.INT, {"default": 1, "min": 0, "max": 100}) + } + } + + RETURN_TYPES = (IO.STRING,) + FUNCTION = "execute" + CATEGORY = "utils/string" + + def execute(self, string, regex_pattern, mode, case_insensitive, multiline, dotall, group_index, **kwargs): + join_delimiter = "\n" + + flags = 0 + if case_insensitive: + flags |= re.IGNORECASE + if multiline: + flags |= re.MULTILINE + if dotall: + flags |= re.DOTALL + + try: + if mode == "First Match": + match = re.search(regex_pattern, string, flags) + if match: + result = match.group(0) + else: + result = "" + + elif mode == "All Matches": + matches = re.findall(regex_pattern, string, flags) + if matches: + if isinstance(matches[0], tuple): + result = join_delimiter.join([m[0] for m in matches]) + else: + result = join_delimiter.join(matches) + else: + result = "" + + elif mode == "First Group": + match = re.search(regex_pattern, string, flags) + if match and len(match.groups()) >= group_index: + result = match.group(group_index) + else: + result = "" + + elif mode == "All Groups": + matches = re.finditer(regex_pattern, string, flags) + results = [] + for match in matches: + if match.groups() and len(match.groups()) >= group_index: + results.append(match.group(group_index)) + result = join_delimiter.join(results) + else: + result = "" + + except re.error: + result = "" + + return result, + +NODE_CLASS_MAPPINGS = { + "StringConcatenate": StringConcatenate, + "StringSubstring": StringSubstring, + "StringLength": StringLength, + "CaseConverter": CaseConverter, + "StringTrim": StringTrim, + "StringReplace": StringReplace, + "StringContains": StringContains, + "StringCompare": StringCompare, + "RegexMatch": RegexMatch, + "RegexExtract": RegexExtract +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "StringConcatenate": "Concatenate", + "StringSubstring": "Substring", + "StringLength": "Length", + "CaseConverter": "Case Converter", + "StringTrim": "Trim", + "StringReplace": "Replace", + "StringContains": "Contains", + "StringCompare": "Compare", + "RegexMatch": "Regex Match", + "RegexExtract": "Regex Extract" +} diff --git a/comfyui_version.py b/comfyui_version.py index 5a73f76e4f7e..b740b378ddc0 100644 --- a/comfyui_version.py +++ b/comfyui_version.py @@ -1,3 +1,3 @@ # This file is automatically generated by the build process when version is # updated in pyproject.toml. -__version__ = "0.3.33" +__version__ = "0.3.34" diff --git a/nodes.py b/nodes.py index a1ddf2dd6fc7..a26a138facd7 100644 --- a/nodes.py +++ b/nodes.py @@ -2263,6 +2263,7 @@ def init_builtin_extra_nodes(): "nodes_fresca.py", "nodes_preview_any.py", "nodes_ace.py", + "nodes_string.py", ] import_failed = [] diff --git a/pyproject.toml b/pyproject.toml index e0be329de2ce..80061b39a079 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ComfyUI" -version = "0.3.33" +version = "0.3.34" readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.9" diff --git a/requirements.txt b/requirements.txt index 01aab4ca2700..8f7a78984d2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -comfyui-frontend-package==1.18.10 +comfyui-frontend-package==1.19.9 comfyui-workflow-templates==0.1.14 torch torchsde