Compare commits

...
Author SHA1 Message Date
shanexi bc62e8a44c feat: support input audio 2025-02-12 18:10:01 +08:00
Xumin Yu 078b3f5ea5 Add joint dependency for Easy use (ComfyUI_IPAdapter_plus) 2025-01-16 16:03:08 +08:00
wl-zhao 31f95035b2 Merge branch 'main' of https://github.com/myshell-ai/ComfyUI-ShellAgent-Plugin into main 2025-01-13 15:55:50 +08:00
wl-zhao 04d33d8a8e add easydict 2025-01-13 15:55:40 +08:00
Wenliang Zhao 4e2bfd5620 Update dependency_checker.py 2024-12-30 14:55:30 +08:00
wl-zhao 681f716bfa update safe open image function 2024-12-18 15:18:48 +08:00
wl-zhao 580ac932df add mac_addr to check realy exist 2024-12-18 14:49:38 +08:00
wl-zhao 37eb10c327 add check_exist 2024-12-18 14:42:28 +08:00
wl-zhao 2310c33966 add get mac_addr 2024-12-18 12:01:11 +08:00
wl-zhao 637bc88fec pass validation when os.path.isfile 2024-12-17 19:24:59 +08:00
wl-zhao 604d34900a support heif image 2024-12-16 17:25:23 +08:00
wl-zhao 82f3a05f1c fix mask bug 2024-12-13 14:52:16 +08:00
wl-zhao 07c080726e kMerge branch 'main' of https://github.com/myshell-ai/ComfyUI-ShellAgent-Plugin into main 2024-12-13 11:45:50 +08:00
wl-zhao 7238c1f40b fix error message when empty input image 2024-12-13 11:45:43 +08:00
Xumin Yu f1135ac55a Update node_deps_info.json 2024-12-12 14:01:48 +08:00
Xumin Yu 676c40691f Update node_deps_info.json 2024-12-12 14:00:55 +08:00
wl-zhao 623ea454cc support input/output audio backend 2024-12-10 16:35:26 +08:00
wl-zhao 2d21584447 add route to inspect version 2024-12-03 15:20:17 +08:00
wl-zhao 27613ed685 support models in nodes / skip configs / input image support mask 2024-11-22 11:49:10 +08:00
wl-zhao 92673900c5 add glob to search models, and raise error when no models founded / multiple models founded 2024-11-14 16:31:43 +08:00
wl-zhao d9d20018be improve abs_path of file dependency 2024-11-14 10:19:59 +08:00
wl-zhao ffa4123f07 support skip model check for some nodes 2024-11-12 16:58:27 +08:00
wl-zhao 35b2700251 add backtick 2024-11-11 15:30:45 +08:00
wl-zhao 42981b7889 add validation for variable name 2024-11-11 15:28:04 +08:00
wl-zhao 091d2ff930 merge 2024-11-07 10:56:29 +08:00
wl-zhao 85e01a8711 add tree_map for dependency checker 2024-11-07 10:56:06 +08:00
Xumin Yu 4d548bfb5a support .sft for model suffix 2024-11-06 20:39:39 +08:00
wl-zhao 24c17beccf fix output video path error 2024-11-04 12:18:01 +08:00
wl-zhao 4c8e720d05 fix output video path error 2024-11-04 12:17:40 +08:00
Xumin Yu 1aa0fc15e2 revert input_video.py 2024-11-03 22:38:56 +08:00
Xumin Yu 1faaad58f0 Update input_video.py 2024-11-03 22:38:13 +08:00
wl-zhao 0904716cd0 fix input video bug 2024-11-03 22:29:12 +08:00
wl-zhao c80f154659 input video 2024-11-01 20:21:27 +08:00
wl-zhao fb3e973b53 add boolean; fix output nodes input type bugs; fix input_video enum validate bug; compatible with none desc 2024-11-01 11:16:08 +08:00
wl-zhao 34f8feb2c0 add boolean; fix output nodes input type bugs; fix input_video enum validate bug; compatible with none desc 2024-11-01 11:15:21 +08:00
Wenliang Zhao 64ebfa42e9 Update output_image.py 2024-10-30 18:11:17 +08:00
wl-zhao deba76e0e7 windows to linux path filenames 2024-10-30 12:08:50 +08:00
wl-zhao 8e8d10b1c5 update output video 2024-10-30 12:07:56 +08:00
Xumin Yu ae2948048a Update dependency_checker.py 2024-10-29 12:55:25 +08:00
wl-zhao 4eb3e8b4f7 add message details 2024-10-29 11:36:13 +08:00
wl-zhao 73bfa4e7b0 add warning message when no inputs/outputs founded 2024-10-28 15:13:01 +08:00
wl-zhao a9d07ba4d3 hardcode hf packages 2024-10-28 11:43:54 +08:00
Wenliang Zhao 070fdb5132 Merge pull request #7 from myshell-ai/6-add-more-convert
6 add more convert
2024-10-28 11:28:29 +08:00
wl-zhao e47726eb77 Merge branch 'main' of https://github.com/myshell-ai/ComfyUI-ShellAgent-Plugin into main 2024-10-28 11:22:11 +08:00
wl-zhao 3b5a9b5220 fix file upload error 2024-10-28 11:22:04 +08:00
wl-zhao 344a886792 add pypi version info 2024-10-28 11:19:19 +08:00
Xumin Yu ddad7b8c40 Update README.md 2024-10-27 23:57:11 +08:00
yuxumin 134ccd3c2b fix dependencies deps 2024-10-27 17:54:59 +08:00
yuxumin 3c8a5ebc2b Merge branch 'main' of https://github.com/myshell-ai/ComfyUI-ShellAgent-Plugin into main 2024-10-27 17:29:48 +08:00
yuxumin 1a826fa746 update node_deps json 2024-10-27 17:29:46 +08:00
wl-zhao 752a0de95d add gguf 2024-10-27 15:39:16 +08:00
wl-zhao 5521822589 Merge branch 'main' of https://github.com/myshell-ai/ComfyUI-ShellAgent-Plugin into main 2024-10-26 11:07:25 +08:00
wl-zhao 2b19a132a6 addmap_legacy 2024-10-26 11:07:15 +08:00
yuxumin 6666050283 update node deps info 2024-10-25 23:29:57 +08:00
wl-zhao c0da8f916d update get_full_path_or_rase 2024-10-25 23:21:54 +08:00
wl-zhao 232bc67c9d handle relative path 2024-10-25 16:01:48 +08:00
wl-zhao a4d96afd5d add blacklist, update model search 2024-10-25 15:27:23 +08:00
wl-zhao 45e7caca72 use folder_path to find the models 2024-10-25 15:01:30 +08:00
17 changed files with 2012 additions and 95 deletions
+4
View File
@@ -17,6 +17,7 @@ To install, either:
- Input Image - Input Image
- Input Float - Input Float
- Input Integer - Input Integer
- Input Video
Each input node supports setting a default value and additional configuration options. Each input node supports setting a default value and additional configuration options.
@@ -25,6 +26,9 @@ Each input node supports setting a default value and additional configuration op
- Save Image - Save Image
- Save Images - Save Images
- Save Video - VHS - Save Video - VHS
- Output Text
- Output Float
- Output Integer
### Convert Widgets to ShellAgent Inputs ### Convert Widgets to ShellAgent Inputs
+195
View File
@@ -0,0 +1,195 @@
import folder_paths
import node_helpers
from PIL import Image, ImageOps, ImageSequence, ImageFile
import numpy as np
import torch
import os
import uuid
import tqdm
import torchaudio
import hashlib
from comfy_extras.nodes_audio import SaveAudio
class LoadAudio:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = folder_paths.filter_files_content_types(
os.listdir(input_dir), ["audio", "video"])
return {"required": {"audio": (sorted(files), {"audio_upload": True})}}
CATEGORY = "audio"
RETURN_TYPES = ("AUDIO", )
FUNCTION = "load"
def load(self, audio):
audio_path = folder_paths.get_annotated_filepath(audio)
waveform, sample_rate = torchaudio.load(audio_path)
audio = {"waveform": waveform.unsqueeze(0), "sample_rate": sample_rate}
return (audio, )
@classmethod
def IS_CHANGED(s, audio):
image_path = folder_paths.get_annotated_filepath(audio)
m = hashlib.sha256()
with open(image_path, 'rb') as f:
m.update(f.read())
return m.digest().hex()
@classmethod
def VALIDATE_INPUTS(s, audio):
if not folder_paths.exists_annotated_filepath(audio):
return "Invalid audio file: {}".format(audio)
return True
class ShellAgentPluginInputAudio:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = folder_paths.filter_files_content_types(
os.listdir(input_dir), ["audio", "video"])
return {
"required": {
"input_name": (
"STRING",
{"multiline": False, "default": "input_audio", "forceInput": False},
),
"default_value": (
sorted(files), {"audio_upload": True, "forceInput": False}
),
},
"optional": {
"description": (
"STRING",
{"multiline": True, "default": "", "forceInput": False},
),
}
}
RETURN_TYPES = ("AUDIO", )
FUNCTION = "load"
CATEGORY = "shellagent"
@classmethod
def validate(cls, **kwargs):
schema = {
"title": kwargs["input_name"],
"type": "string",
"default": kwargs["default_value"],
"description": kwargs.get("description", ""),
"url_type": "audio"
}
return schema
@classmethod
def VALIDATE_INPUTS(s, audio):
if not folder_paths.exists_annotated_filepath(audio):
return "Invalid audio file: {}".format(audio)
return True
@classmethod
def VALIDATE_INPUTS(s, input_name, default_value, description=""):
audio = default_value
if audio.startswith("http"):
return True
if not folder_paths.exists_annotated_filepath(audio):
return "Invalid audio file: {}".format(audio)
return True
def load(self, input_name, default_value=None, display_name=None, description=None):
input_dir = folder_paths.get_input_directory()
audio_path = default_value
try:
if audio_path.startswith('http'):
import requests
from io import BytesIO
print("Fetching audio from url: ", audio_path)
response = requests.get(audio_path)
response.raise_for_status()
audio_file = BytesIO(response.content)
waveform, sample_rate = torchaudio.load(audio_file)
else:
if not os.path.isfile(audio_path): # abs path
# local path
audio_path = os.path.join(input_dir, audio_path)
waveform, sample_rate = torchaudio.load(audio_path)
audio = {"waveform": waveform.unsqueeze(
0), "sample_rate": sample_rate}
return (audio, )
# image = ImageOps.exif_transpose(image)
# image = image.convert("RGB")
# image = np.array(image).astype(np.float32) / 255.0
# image = torch.from_numpy(image)[None,]
# return [image]
except Exception as e:
raise e
class ShellAgentSaveAudios(SaveAudio):
@classmethod
def INPUT_TYPES(s):
return {"required": {"audio": ("AUDIO", ),
"output_name": ("STRING", {"multiline": False, "default": "output_audio"},),
"filename_prefix": ("STRING", {"default": "audio/ComfyUI"})},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
# {
# "required": {
# "images": ("IMAGE", {"tooltip": "The audio to save."}),
# "output_name": ("STRING", {"multiline": False, "default": "output_image"},),
# "filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."})
# },
# "hidden": {
# "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
# },
# }
CATEGORY = "shellagent"
@classmethod
def validate(cls, **kwargs):
schema = {
"title": kwargs["output_name"],
"type": "array",
"items": {
"type": "string",
"url_type": "audio",
}
}
return schema
def save_audio(self, audio, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None, **extra_kwargs):
results = super().save_audio(audio, filename_prefix, prompt, extra_pnginfo)
results["shellagent_kwargs"] = extra_kwargs
return results
class ShellAgentSaveAudio(ShellAgentSaveAudios):
@classmethod
def validate(cls, **kwargs):
schema = {
"title": kwargs["output_name"],
"type": "string",
"url_type": "audio",
}
return schema
NODE_CLASS_MAPPINGS = {
"ShellAgentPluginInputAudio": ShellAgentPluginInputAudio,
"ShellAgentPluginSaveAudios": ShellAgentSaveAudios,
"ShellAgentPluginSaveAudio": ShellAgentSaveAudio,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ShellAgentPluginInputAudio": "Input Audio (ShellAgent Plugin)",
"ShellAgentPluginSaveAudios": "Save Audios (ShellAgent Plugin)",
"ShellAgentPluginSaveAudio": "Save Audio (ShellAgent Plugin)",
}
+88 -11
View File
@@ -1,11 +1,39 @@
import folder_paths import folder_paths
from PIL import Image, ImageOps import node_helpers
from PIL import Image, ImageOps, ImageSequence, ImageFile
import numpy as np import numpy as np
import torch import torch
import os import os
import uuid import uuid
import tqdm import tqdm
from io import BytesIO
import PIL
import cv2
from pillow_heif import register_heif_opener
register_heif_opener()
def safe_open_image(image_bytes):
try:
image_pil = Image.open(BytesIO(image_bytes))
except PIL.UnidentifiedImageError as e:
print(e)
# Convert response content (bytes) to a NumPy array
image_array = np.frombuffer(image_bytes, np.uint8)
# Decode the image from the NumPy array (OpenCV format: BGR)
image_cv = cv2.imdecode(image_array, cv2.IMREAD_COLOR)
if image_cv is not None:
# Convert the BGR image to RGB
image_rgb = cv2.cvtColor(image_cv, cv2.COLOR_BGR2RGB)
# Convert the RGB NumPy array to a PIL Image
image_pil = Image.fromarray(image_rgb)
else:
raise ValueError("The image cannot be identified by neither PIL nor OpenCV")
return image_pil
class ShellAgentPluginInputImage: class ShellAgentPluginInputImage:
@classmethod @classmethod
@@ -32,8 +60,8 @@ class ShellAgentPluginInputImage:
} }
} }
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("image",) # RETURN_NAMES = ("image",)
FUNCTION = "run" FUNCTION = "run"
@@ -45,7 +73,7 @@ class ShellAgentPluginInputImage:
"title": kwargs["input_name"], "title": kwargs["input_name"],
"type": "string", "type": "string",
"default": kwargs["default_value"], "default": kwargs["default_value"],
"description": kwargs["description"], "description": kwargs.get("description", ""),
"url_type": "image" "url_type": "image"
} }
return schema return schema
@@ -53,13 +81,61 @@ class ShellAgentPluginInputImage:
@classmethod @classmethod
def VALIDATE_INPUTS(s, input_name, default_value, description=""): def VALIDATE_INPUTS(s, input_name, default_value, description=""):
image = default_value image = default_value
if image.startswith("http"): if image.startswith("http"):
return True return True
if image == "":
return "Invalid image file: please check if the image is empty or invalid"
if os.path.isfile(image):
return True
if not folder_paths.exists_annotated_filepath(image): if not folder_paths.exists_annotated_filepath(image):
return "Invalid image file: {}".format(image) return "Invalid image file: {}".format(image)
return True return True
def convert_image_mask(self, img):
output_images = []
output_masks = []
w, h = None, None
excluded_formats = ['MPO']
for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i)
if i.mode == 'I':
i = i.point(lambda i: i * (1 / 255))
image = i.convert("RGB")
if len(output_images) == 0:
w = image.size[0]
h = image.size[1]
if image.size[0] != w or image.size[1] != h:
continue
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
if 'A' in i.getbands():
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((64,64), dtype=torch.float32, device="cpu")
output_images.append(image)
output_masks.append(mask.unsqueeze(0))
if len(output_images) > 1 and img.format not in excluded_formats:
output_image = torch.cat(output_images, dim=0)
output_mask = torch.cat(output_masks, dim=0)
else:
output_image = output_images[0]
output_mask = output_masks[0]
return (output_image, output_mask)
def run(self, input_name, default_value=None, display_name=None, description=None): def run(self, input_name, default_value=None, display_name=None, description=None):
input_dir = folder_paths.get_input_directory() input_dir = folder_paths.get_input_directory()
@@ -70,7 +146,7 @@ class ShellAgentPluginInputImage:
from io import BytesIO from io import BytesIO
print("Fetching image from url: ", image_path) print("Fetching image from url: ", image_path)
response = requests.get(image_path) response = requests.get(image_path)
image = Image.open(BytesIO(response.content)) image = safe_open_image(response.content)
elif image_path.startswith('data:image/png;base64,') or image_path.startswith('data:image/jpeg;base64,') or image_path.startswith('data:image/jpg;base64,'): elif image_path.startswith('data:image/png;base64,') or image_path.startswith('data:image/jpeg;base64,') or image_path.startswith('data:image/jpg;base64,'):
import base64 import base64
from io import BytesIO from io import BytesIO
@@ -82,13 +158,14 @@ class ShellAgentPluginInputImage:
if not os.path.isfile(image_path): # abs path if not os.path.isfile(image_path): # abs path
# local path # local path
image_path = os.path.join(input_dir, image_path) image_path = os.path.join(input_dir, image_path)
image = Image.open(image_path).convert("RGB") image = node_helpers.pillow(Image.open, image_path)
image = ImageOps.exif_transpose(image) return self.convert_image_mask(image)
image = image.convert("RGB") # image = ImageOps.exif_transpose(image)
image = np.array(image).astype(np.float32) / 255.0 # image = image.convert("RGB")
image = torch.from_numpy(image)[None,] # image = np.array(image).astype(np.float32) / 255.0
return [image] # image = torch.from_numpy(image)[None,]
# return [image]
except Exception as e: except Exception as e:
raise e raise e
+47 -3
View File
@@ -42,7 +42,7 @@ class ShellAgentPluginInputText:
"title": kwargs["input_name"], "title": kwargs["input_name"],
"type": "string", "type": "string",
"default": kwargs["default_value"], "default": kwargs["default_value"],
"description": kwargs["description"], "description": kwargs.get("description", ""),
} }
if kwargs.get("choices", "") != "": if kwargs.get("choices", "") != "":
schema["enums"] = eval(kwargs["choices"]) schema["enums"] = eval(kwargs["choices"])
@@ -101,7 +101,7 @@ class ShellAgentPluginInputFloat:
"title": kwargs["input_name"], "title": kwargs["input_name"],
"type": "number", "type": "number",
"default": kwargs["default_value"], "default": kwargs["default_value"],
"description": kwargs["description"], "description": kwargs.get("description", ""),
} }
if kwargs.get("choices", "") != "": if kwargs.get("choices", "") != "":
schema["enums"] = eval(kwargs["choices"]) schema["enums"] = eval(kwargs["choices"])
@@ -184,14 +184,58 @@ class ShellAgentPluginInputInteger:
def run(self, input_name, default_value=None, display_name=None, description=None, **kwargs): def run(self, input_name, default_value=None, display_name=None, description=None, **kwargs):
return [default_value] return [default_value]
class ShellAgentPluginInputBoolean:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_name": (
"STRING",
{"multiline": False, "default": "input_bool"},
),
},
"optional": {
"default_value": (
"BOOLEAN",
{"default": False},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
RETURN_TYPES = ("BOOLEAN",)
RETURN_NAMES = ("boolean",)
FUNCTION = "run"
CATEGORY = "shellagent"
@classmethod
def validate(cls, **kwargs):
schema = {
"title": kwargs["input_name"],
"type": "boolean",
"default": kwargs["default_value"],
"description": kwargs.get("description", ""),
}
return schema
def run(self, input_name, default_value=None, display_name=None, description=None, **kwargs):
return [default_value]
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
"ShellAgentPluginInputText": ShellAgentPluginInputText, "ShellAgentPluginInputText": ShellAgentPluginInputText,
"ShellAgentPluginInputFloat": ShellAgentPluginInputFloat, "ShellAgentPluginInputFloat": ShellAgentPluginInputFloat,
"ShellAgentPluginInputInteger": ShellAgentPluginInputInteger "ShellAgentPluginInputInteger": ShellAgentPluginInputInteger,
"ShellAgentPluginInputBoolean": ShellAgentPluginInputBoolean,
} }
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
"ShellAgentPluginInputText": "Input Text (ShellAgent Plugin)", "ShellAgentPluginInputText": "Input Text (ShellAgent Plugin)",
"ShellAgentPluginInputFloat": "Input Float (ShellAgent Plugin)", "ShellAgentPluginInputFloat": "Input Float (ShellAgent Plugin)",
"ShellAgentPluginInputInteger": "Input Integer (ShellAgent Plugin)", "ShellAgentPluginInputInteger": "Input Integer (ShellAgent Plugin)",
"ShellAgentPluginInputBoolean": "Input Boolean (ShellAgent Plugin)",
} }
+11 -2
View File
@@ -4,7 +4,7 @@ import numpy as np
import torch import torch
import os import os
import uuid import uuid
import tqdm from tqdm import tqdm
# class ShellAgentPluginInputImage: # class ShellAgentPluginInputImage:
@@ -120,6 +120,15 @@ class ShellAgentPluginInputVideo:
"url_type": "video" "url_type": "video"
} }
return schema return schema
@classmethod
def VALIDATE_INPUTS(s, input_name, default_value, description=""):
video = default_value
if video.startswith("http"):
return True
if not folder_paths.exists_annotated_filepath(video):
return "Invalid video file: {}".format(video)
return True
def run(self, input_name, default_value=None, description=None): def run(self, input_name, default_value=None, description=None):
input_dir = folder_paths.get_input_directory() input_dir = folder_paths.get_input_directory()
@@ -166,4 +175,4 @@ NODE_CLASS_MAPPINGS = {
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
# "ShellAgentPluginInputImage": "Input Image (ShellAgent Plugin)", # "ShellAgentPluginInputImage": "Input Image (ShellAgent Plugin)",
"ShellAgentPluginInputVideo": "Input Video (ShellAgent Plugin)" "ShellAgentPluginInputVideo": "Input Video (ShellAgent Plugin)"
} }
+8 -4
View File
@@ -75,11 +75,15 @@ class ShellAgentSaveVideoVHS:
return schema return schema
def save_video(self, filenames, **kwargs): def save_video(self, filenames, **kwargs):
status, (preview_image, video_path) = filenames status, output_files = filenames
if len(output_files) == 0:
raise ValueError("the filenames are empty")
print("output_files", output_files)
video_path = output_files[-1]
cwd = os.getcwd() cwd = os.getcwd()
preview_image = os.path.relpath(preview_image) # preview_image = os.path.relpath(preview_image)
video_path = os.path.relpath(video_path) video_path = os.path.relpath(video_path, folder_paths.base_path)
results = {"ui": {"image": [preview_image], "video": [video_path]}} results = {"ui": {"video": [video_path]}}
return results return results
+18 -4
View File
@@ -2,7 +2,8 @@
json_type_mapipng = { json_type_mapipng = {
"text": "string", "text": "string",
"float": "number", "float": "number",
"integer": "integer" "integer": "integer",
"boolean": "boolean",
} }
class ShellAgentOutputText: class ShellAgentOutputText:
@@ -43,7 +44,7 @@ class ShellAgentOutputFloat(ShellAgentOutputText):
def INPUT_TYPES(s): def INPUT_TYPES(s):
return { return {
"required": { "required": {
s.TYPE_STR: ("STRING", {"tooltip": f"The {s.TYPE_STR} to output."}), s.TYPE_STR: ("FLOAT", {"tooltip": f"The {s.TYPE_STR} to output."}),
"output_name": ("STRING", {"multiline": False, "default": f"output_{s.TYPE_STR}"},), "output_name": ("STRING", {"multiline": False, "default": f"output_{s.TYPE_STR}"},),
}, },
} }
@@ -56,7 +57,19 @@ class ShellAgentOutputInteger(ShellAgentOutputText):
def INPUT_TYPES(s): def INPUT_TYPES(s):
return { return {
"required": { "required": {
s.TYPE_STR: ("STRING", {"tooltip": f"The {s.TYPE_STR} to output."}), s.TYPE_STR: ("INT", {"tooltip": f"The {s.TYPE_STR} to output."}),
"output_name": ("STRING", {"multiline": False, "default": f"output_{s.TYPE_STR}"},),
},
}
class ShellAgentOutputBoolean(ShellAgentOutputText):
TYPE_STR = "boolean"
DESCRIPTION = "output the integer"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
s.TYPE_STR: ("BOOLEAN", {"tooltip": f"The {s.TYPE_STR} to output."}),
"output_name": ("STRING", {"multiline": False, "default": f"output_{s.TYPE_STR}"},), "output_name": ("STRING", {"multiline": False, "default": f"output_{s.TYPE_STR}"},),
}, },
} }
@@ -65,7 +78,8 @@ class ShellAgentOutputInteger(ShellAgentOutputText):
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
"ShellAgentPluginOutputText": ShellAgentOutputText, "ShellAgentPluginOutputText": ShellAgentOutputText,
"ShellAgentPluginOutputFloat": ShellAgentOutputFloat, "ShellAgentPluginOutputFloat": ShellAgentOutputFloat,
"ShellAgentPluginOutputInteger": ShellAgentOutputInteger "ShellAgentPluginOutputInteger": ShellAgentOutputInteger,
"ShellAgentPluginOutputBoolean": ShellAgentOutputBoolean,
} }
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
"ShellAgentPluginOutputText": "Output Text (ShellAgent Plugin)", "ShellAgentPluginOutputText": "Output Text (ShellAgent Plugin)",
+59 -5
View File
@@ -29,9 +29,12 @@ import atexit
from datetime import datetime from datetime import datetime
import nodes import nodes
import traceback import traceback
import re
import keyword
import uuid
from .dependency_checker import resolve_dependencies from .dependency_checker import resolve_dependencies, inspect_repo_version
from folder_paths import base_path as BASE_PATH
WORKFLOW_ROOT = "shellagent/comfy_workflow" WORKFLOW_ROOT = "shellagent/comfy_workflow"
@@ -45,6 +48,14 @@ CustomNodeTypeMap = {
"ShellAgentPluginSaveVideoVHS": "video", "ShellAgentPluginSaveVideoVHS": "video",
} }
# Regular expression for a valid Python variable name
variable_name_pattern = r'^[a-zA-Z_][a-zA-Z0-9_]*$'
def is_valid_variable_name(name):
# Check if it matches the pattern and is not a keyword
if re.match(variable_name_pattern, name) and not keyword.iskeyword(name):
return True
return False
def schema_validator(prompt): def schema_validator(prompt):
from nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS from nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
@@ -84,6 +95,9 @@ def schema_validator(prompt):
continue continue
if hasattr(node_cls, "validate"): if hasattr(node_cls, "validate"):
schema = node_cls.validate(**node_info["inputs"]) schema = node_cls.validate(**node_info["inputs"])
# validate schema
if not is_valid_variable_name(schema["title"]):
raise ValueError(f'`{schema["title"]}` is not a valid variable name!')
else: else:
raise NotImplementedError("the validate is not implemented") raise NotImplementedError("the validate is not implemented")
schemas[mode][node_id] = schema schemas[mode][node_id] = schema
@@ -158,16 +172,56 @@ async def shellagent_export(request):
# for fname, dict_to_save in fname_mapping.items(): # for fname, dict_to_save in fname_mapping.items():
# with open(os.path.join(save_root, fname), "w") as f: # with open(os.path.join(save_root, fname), "w") as f:
# json.dump(dict_to_save, f, indent=2) # json.dump(dict_to_save, f, indent=2)
warning_message = ""
if dependency_results.get("black_list_nodes", []):
warning_message = "The following nodes cannot be deployed to myshell:\n"
for item in dependency_results["black_list_nodes"]:
warning_message += f" {item['name']}: {item['reason']}\n"
if len(schemas["inputs"]) + len(schemas["outputs"]) == 0:
warning_message += f"The workflow contains neither inputs nor outputs!\n"
return_dict = { return_dict = {
"success": True, "success": True,
"dependencies": dependency_results, "dependencies": dependency_results["dependencies"],
"warning_message": warning_message,
"schemas": schemas "schemas": schemas
} }
except Exception as e: except Exception as e:
status = 400 status = 400
return_dict = { return_dict = {
"success": False, "success": False,
"message": str(traceback.format_exc()), "message_detail": str(traceback.format_exc()),
"message": str(e),
} }
return web.json_response(return_dict, status=status) return web.json_response(return_dict, status=status)
@server.PromptServer.instance.routes.post("/shellagent/inspect_version") # data same as queue prompt, plus workflow_name
async def shellagent_inspect_version(request):
data = await request.json()
comfyui_version = inspect_repo_version(BASE_PATH)
comfyui_shellagent_plugin_version = inspect_repo_version(os.path.dirname(__file__))
return_dict = {
"comfyui_version": comfyui_version,
"comfyui_shellagent_plugin_version": comfyui_shellagent_plugin_version,
}
return web.json_response(return_dict, status=200)
@server.PromptServer.instance.routes.post("/shellagent/get_mac_addr") # data same as queue prompt, plus workflow_name
async def shellagent_get_mac_addr(request):
data = await request.json()
return_dict = {
"mac_addr": uuid.getnode()
}
return web.json_response(return_dict, status=200)
@server.PromptServer.instance.routes.post("/shellagent/check_exist") # check if the file or folder exist
async def shellagent_check_exist(request):
data = await request.json()
return_dict = {
"exist": uuid.getnode() == data["mac_addr"] and os.path.exists(data["path"]) # really exist, instead of same name
}
return web.json_response(return_dict, status=200)
+173 -39
View File
@@ -5,25 +5,36 @@ import logging
from functools import partial from functools import partial
import re import re
import glob import glob
import sys
from folder_paths import models_dir as MODELS_DIR from folder_paths import models_dir as MODELS_DIR
from folder_paths import base_path as BASE_PATH from folder_paths import base_path as BASE_PATH
from folder_paths import get_full_path
from .utils import compute_sha256, windows_to_linux_path
from .utils.utils import compute_sha256, windows_to_linux_path
from .utils.pytree import tree_map
from .file_upload import collect_local_file, process_local_file_path_async from .file_upload import collect_local_file, process_local_file_path_async
model_list_json = json.load(open(os.path.join(os.path.dirname(__file__), "model_info.json"))) model_list_json = json.load(open(os.path.join(os.path.dirname(__file__), "model_info.json")))
model_loaders_info = json.load(open(os.path.join(os.path.dirname(__file__), "model_loader_info.json"))) model_loaders_info = json.load(open(os.path.join(os.path.dirname(__file__), "model_loader_info.json")))
node_deps_info = json.load(open(os.path.join(os.path.dirname(__file__), "node_deps_info.json"))) node_deps_info = json.load(open(os.path.join(os.path.dirname(__file__), "node_deps_info.json")))
node_blacklist = json.load(open(os.path.join(os.path.dirname(__file__), "node_blacklist.json")))
node_remote_skip_models = json.load(open(os.path.join(os.path.dirname(__file__), "node_remote.json")))
model_suffix = [".ckpt", ".safetensors", ".bin", ".pth", ".pt", ".onnx", ".gguf", ".sft", ".ttf"]
extra_packages = ["transformers", "timm", "diffusers", "accelerate"]
model_suffix = [".ckpt", ".safetensors", ".bin", ".pth", ".pt", ".onnx"] def get_full_path_or_raise(folder_name: str, filename: str) -> str:
full_path = get_full_path(folder_name, filename)
if full_path is None:
raise FileNotFoundError(f"Model in folder '{folder_name}' with filename '{filename}' not found.")
return full_path
def handle_model_info(ckpt_path):
def handle_model_info(ckpt_path, filename, rel_save_path):
ckpt_path = windows_to_linux_path(ckpt_path) ckpt_path = windows_to_linux_path(ckpt_path)
filename = os.path.basename(ckpt_path)
dirname = os.path.dirname(ckpt_path)
save_path = os.path.dirname(os.path.relpath(ckpt_path, MODELS_DIR))
metadata_path = ckpt_path + ".json" metadata_path = ckpt_path + ".json"
if os.path.isfile(metadata_path): if os.path.isfile(metadata_path):
metadata = json.load(open(metadata_path)) metadata = json.load(open(metadata_path))
@@ -35,7 +46,7 @@ def handle_model_info(ckpt_path):
model_id = compute_sha256(ckpt_path) model_id = compute_sha256(ckpt_path)
data = { data = {
"id": model_id, "id": model_id,
"save_path": save_path, "save_path": rel_save_path,
"filename": filename, "filename": filename,
} }
json.dump(data, open(metadata_path, "w")) json.dump(data, open(metadata_path, "w"))
@@ -45,8 +56,8 @@ def handle_model_info(ckpt_path):
urls = [] urls = []
item = { item = {
"filename": filename, "filename": windows_to_linux_path(filename),
"save_path": windows_to_linux_path(save_path), "save_path": windows_to_linux_path(rel_save_path),
"urls": urls, "urls": urls,
} }
return model_id, item return model_id, item
@@ -106,49 +117,143 @@ def fetch_model_searcher_results(model_ids):
results = None results = None
return results return results
def split_package_version(require_line):
require_line = require_line.strip()
pattern = r"^([a-zA-Z0-9_\-\[\]]+)(.*)$"
match = re.match(pattern, require_line.strip())
if match:
package_name = match.group(1) # First capturing group is the package name
version_specifier = match.group(2) if match.group(2) else "" # Second group is the version, if present
return package_name, version_specifier
else:
assert len(require_line) == 0 or require_line.strip()[0] == "#", require_line
return None, None
def get_package_version(package_name):
try:
if sys.version_info >= (3, 8):
from importlib.metadata import version, PackageNotFoundError
return version(package_name)
else:
from pkg_resources import get_distribution, DistributionNotFound
return get_distribution(package_name).version
except Exception:
return None
def resolve_dependencies(prompt, custom_dependencies): # resolve custom nodes and models at the same time def resolve_dependencies(prompt, custom_dependencies): # resolve custom nodes and models at the same time
from nodes import NODE_CLASS_MAPPINGS from nodes import NODE_CLASS_MAPPINGS
import folder_paths
custom_nodes = [] custom_nodes = []
ckpt_paths = [] ckpt_paths = {}
file_mapping_dict = {} file_mapping_dict = {}
SKIP_FOLDER_NAMES = ["configs", "custom_nodes"]
def collect_unknown_models(filename, node_id, node_info, custom_node_path):
if type(filename) != str:
return
is_model = False
for possible_suffix in model_suffix:
if filename.endswith(possible_suffix):
is_model = True
if is_model:
print(f"find {filename}, is_model=True")
# find possible paths
matching_files = {}
# Walk through all subdirectories and files in the directory
rel_save_path = None
for possible_folder_name in folder_paths.folder_names_and_paths:
if possible_folder_name in SKIP_FOLDER_NAMES:
print(f"skip {possible_folder_name}")
continue
full_path = folder_paths.get_full_path(possible_folder_name, filename)
if full_path is None:
continue
rel_save_path = os.path.relpath(folder_paths.folder_names_and_paths[possible_folder_name][0][0], folder_paths.models_dir)
matching_files[full_path] = {
"rel_save_path": rel_save_path
}
print(f"matched files: {matching_files}")
# step 2: search for all the files under "models"
for full_path in glob.glob(f"{folder_paths.models_dir}/**/*", recursive=True):
if os.path.isfile(full_path) and full_path.endswith(filename) and full_path not in matching_files:
folder_path = full_path[:-len(filename)]
rel_save_path = os.path.relpath(folder_path, folder_paths.models_dir)
matching_files[full_path] = {
"rel_save_path": rel_save_path
}
print(f"matched files: {matching_files}")
# step 3: search inside the custom nodes
if custom_node_path is not None:
for full_path in glob.glob(f"{custom_node_path}/**/*", recursive=True):
if os.path.isfile(full_path) and full_path.endswith(filename) and full_path not in matching_files:
folder_path = full_path[:-len(filename)]
rel_save_path = os.path.relpath(folder_path, folder_paths.models_dir)
matching_files[full_path] = {
"rel_save_path": rel_save_path
}
if len(matching_files) == 0:
raise ValueError(f"Cannot find model: `{filename}`, Node ID: `{node_id}`, Node Info: `{node_info}`")
elif len(matching_files) <= 3:
for full_path, info in matching_files.items():
ckpt_paths[full_path] = {
"filename": filename,
"rel_save_path": info["rel_save_path"]
}
return
else:
raise ValueError(f"Multiple models of `{filename}` founded, Node ID: `{node_id}`, Node Info: `{node_info}`, Possible paths: `{list(matching_files.keys())}`")
for node_id, node_info in prompt.items(): for node_id, node_info in prompt.items():
node_class_type = node_info.get("class_type") node_class_type = node_info.get("class_type")
if node_class_type is None: if node_class_type is None:
raise NotImplementedError(f"Missing nodes founded, please first install the missing nodes using ComfyUI Manager") raise NotImplementedError(f"Missing nodes founded, please first install the missing nodes using ComfyUI Manager")
node_cls = NODE_CLASS_MAPPINGS[node_class_type] node_cls = NODE_CLASS_MAPPINGS[node_class_type]
skip_model_check = False
custom_node_path = None
if hasattr(node_cls, "RELATIVE_PYTHON_MODULE") and node_cls.RELATIVE_PYTHON_MODULE.startswith("custom_nodes."): if hasattr(node_cls, "RELATIVE_PYTHON_MODULE") and node_cls.RELATIVE_PYTHON_MODULE.startswith("custom_nodes."):
print(node_cls.RELATIVE_PYTHON_MODULE) print(node_cls.RELATIVE_PYTHON_MODULE)
custom_nodes.append(node_cls.RELATIVE_PYTHON_MODULE) custom_nodes.append(node_cls.RELATIVE_PYTHON_MODULE)
custom_node_path = os.path.join(BASE_PATH, node_cls.RELATIVE_PYTHON_MODULE.replace(".", "/"))
if node_cls.RELATIVE_PYTHON_MODULE[len("custom_nodes."):] in node_remote_skip_models:
skip_model_check = True
print(f"skip model check for {node_class_type}")
if node_class_type in model_loaders_info: if node_class_type in model_loaders_info:
for field_name, filename in node_info["inputs"].items():
for item in model_loaders_info[node_class_type]:
pattern = item["field_name"]
if re.match(f"^{pattern}$", field_name):
ckpt_path = os.path.join(MODELS_DIR, item["save_path"], filename)
ckpt_paths.append(ckpt_path)
else:
for field_name, filename in node_info["inputs"].items(): for field_name, filename in node_info["inputs"].items():
if type(filename) != str: if type(filename) != str:
continue continue
is_model = False for item in model_loaders_info[node_class_type]:
for possible_suffix in model_suffix: pattern = item["field_name"]
if filename.endswith(possible_suffix): if re.match(f"^{pattern}$", field_name) and any([filename.endswith(possible_suffix) for possible_suffix in model_suffix]):
is_model = True ckpt_path = get_full_path_or_raise(item["save_path"], filename)
if is_model: if hasattr(folder_paths, "map_legacy"):
print(f"find {filename}, is_model=True") save_folder = folder_paths.map_legacy(item["save_path"])
# find possible paths else:
matching_files = [] save_folder = item["save_path"]
# Walk through all subdirectories and files in the directory rel_save_path = os.path.relpath(folder_paths.folder_names_and_paths[save_folder][0][0], folder_paths.models_dir)
for possible_filename in glob.glob(os.path.join(MODELS_DIR, "**", "*"), recursive=True): ckpt_paths[ckpt_path] = {
if os.path.isfile(possible_filename) and possible_filename.endswith(filename): "filename": filename,
matching_files.append(possible_filename) "rel_save_path": rel_save_path
print(f"matched files: {matching_files}") }
if len(matching_files) == 1: elif not skip_model_check:
ckpt_paths.append(matching_files[0]) tree_map(lambda x: collect_unknown_models(x, node_id, node_info, custom_node_path), node_info["inputs"])
list(map(partial(collect_local_file, mapping_dict=file_mapping_dict), node_info["inputs"].values())) list(map(partial(collect_local_file, mapping_dict=file_mapping_dict), node_info["inputs"].values()))
ckpt_paths = list(set(ckpt_paths))
print("ckpt_paths:", ckpt_paths) print("ckpt_paths:", ckpt_paths)
custom_nodes = list(set(custom_nodes)) custom_nodes = list(set(custom_nodes))
# step 0: comfyui version # step 0: comfyui version
@@ -163,6 +268,7 @@ def resolve_dependencies(prompt, custom_dependencies): # resolve custom nodes an
# step 1: custom nodes # step 1: custom nodes
custom_nodes_list = [] custom_nodes_list = []
custom_nodes_names = [] custom_nodes_names = []
requirements_lines = []
for custom_node in custom_nodes: for custom_node in custom_nodes:
try: try:
repo_info = inspect_repo_version(os.path.join(BASE_PATH, custom_node.replace(".", "/"))) repo_info = inspect_repo_version(os.path.join(BASE_PATH, custom_node.replace(".", "/")))
@@ -175,20 +281,39 @@ def resolve_dependencies(prompt, custom_dependencies): # resolve custom nodes an
custom_nodes_names.append(repo_info["name"]) custom_nodes_names.append(repo_info["name"])
except: except:
print(f"failed to resolve repo info of {custom_node}") print(f"failed to resolve repo info of {custom_node}")
requirement_file = os.path.join(BASE_PATH, custom_node.replace(".", "/"), "requirements.txt")
if os.path.isfile(requirement_file):
try:
requirements_lines += open(requirement_file).readlines()
except:
pass
requirements_lines = list(set(requirements_lines))
requirements_packages = [package_name for package_name, version_specifier in map(split_package_version, requirements_lines) if package_name is not None]
package_names = set(requirements_packages + extra_packages)
pypi_deps = {
package_name: get_package_version(package_name)
for package_name in package_names
}
for repo_name in custom_nodes_names: for repo_name in custom_nodes_names:
if repo_name in node_deps_info: if repo_name in node_deps_info:
for deps_node in node_deps_info[repo_name]: for deps_node in node_deps_info[repo_name]:
if deps_node["name"] not in custom_nodes_names: if deps_node["name"] not in custom_nodes_names:
repo_info = inspect_repo_version(os.path.join("custom_nodes", deps_node["name"])) repo_info = inspect_repo_version(os.path.join(BASE_PATH, "custom_nodes", deps_node["name"]))
deps_node["commit"] = repo_info["commit"] deps_node["commit"] = repo_info["commit"]
custom_nodes_list.append(deps_node) custom_nodes_list.append(deps_node)
custom_nodes_names.append(deps_node["name"])
black_list_nodes = []
for repo_name in custom_nodes_names:
if repo_name in node_blacklist:
black_list_nodes.append({"name": repo_name, "reason": node_blacklist[repo_name]["reason"]})
# step 2: models # step 2: models
models_dict = {} models_dict = {}
missing_model_ids = [] missing_model_ids = []
for ckpt_path in ckpt_paths: for ckpt_path, ckpt_info in ckpt_paths.items():
model_id, item = handle_model_info(ckpt_path) model_id, item = handle_model_info(ckpt_path, ckpt_info["filename"], ckpt_info["rel_save_path"])
models_dict[model_id] = item models_dict[model_id] = item
if len(item["urls"]) == 0: if len(item["urls"]) == 0:
item["require_recheck"] = True item["require_recheck"] = True
@@ -207,12 +332,21 @@ def resolve_dependencies(prompt, custom_dependencies): # resolve custom nodes an
# step 3: handle local files # step 3: handle local files
process_local_file_path_async(file_mapping_dict, max_workers=20) process_local_file_path_async(file_mapping_dict, max_workers=20)
files_dict = {v[0]: {"filename": windows_to_linux_path(os.path.relpath(v[2], BASE_PATH)), "urls": [v[1]]} for v in file_mapping_dict.values()} files_dict = {
v[0]: {
"filename": windows_to_linux_path(os.path.relpath(v[2], BASE_PATH)) if not v[3] else v[2],
"urls": [v[1]]} for v in file_mapping_dict.values()}
results = { depencencies = {
"comfyui_version": comfyui_version, "comfyui_version": comfyui_version,
"custom_nodes": custom_nodes_list, "custom_nodes": custom_nodes_list,
"models": models_dict, "models": models_dict,
"files": files_dict, "files": files_dict,
"pypi": pypi_deps
} }
return results
return_dict = {
"dependencies": depencencies,
"black_list_nodes": black_list_nodes,
}
return return_dict
+17 -7
View File
@@ -5,7 +5,7 @@ import time
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
import folder_paths import folder_paths
from .utils import compute_sha256 from .utils.utils import compute_sha256, get_alphanumeric_hash
ext_to_type = { ext_to_type = {
# image # image
@@ -27,7 +27,7 @@ ext_to_type = {
'.m4a': 'audio/mp4', '.m4a': 'audio/mp4',
} }
def upload_file_to_myshell(local_file: str) -> str: def upload_file_to_myshell(local_file: str, target_path: str, is_abs) -> str:
''' Now we only support upload file one-by-one ''' Now we only support upload file one-by-one
''' '''
MYSHELL_KEY = os.environ.get('MYSHELL_KEY', "OPENSOURCE_FIXED") MYSHELL_KEY = os.environ.get('MYSHELL_KEY', "OPENSOURCE_FIXED")
@@ -51,8 +51,8 @@ def upload_file_to_myshell(local_file: str) -> str:
response = requests.request("POST", server_url, headers=headers, files=files) response = requests.request("POST", server_url, headers=headers, files=files)
if response.status_code == 200: if response.status_code == 200:
end_time = time.time() end_time = time.time()
logging.info(f"{local_file} uploaded, time elapsed: {end_time - start_time}") logging.info(f"{local_file} uploaded, time elapsed: {end_time - start_time}, will be saved to {target_path}")
return [sha256sum, response.json()['url'], local_file] return [sha256sum, response.json()['url'], target_path, is_abs]
else: else:
raise Exception( raise Exception(
f"[HTTP ERROR] {response.status_code} - {response.text} \n" f"[HTTP ERROR] {response.status_code} - {response.text} \n"
@@ -66,8 +66,11 @@ def collect_local_file(item, mapping_dict={}):
abspath = os.path.abspath(item) abspath = os.path.abspath(item)
input_abspath = os.path.join(input_dir, item) input_abspath = os.path.join(input_dir, item)
# required file type # required file type
is_abs = False
if os.path.isfile(abspath): if os.path.isfile(abspath):
fpath = abspath fpath = abspath
is_abs = True
elif os.path.isfile(input_abspath): elif os.path.isfile(input_abspath):
fpath = input_abspath fpath = input_abspath
else: else:
@@ -75,7 +78,13 @@ def collect_local_file(item, mapping_dict={}):
if fpath is not None: if fpath is not None:
ext = os.path.splitext(fpath)[1] ext = os.path.splitext(fpath)[1]
if ext.lower() in ext_to_type.keys(): if ext.lower() in ext_to_type.keys():
mapping_dict[item] = fpath if is_abs: # if use abs path, replace it
filename_hash = get_alphanumeric_hash(abspath)[:16]
count = len(mapping_dict)
target_path = f"/ShellAgentDeploy/ComfyUI/input/{filename_hash}_{count:06d}{ext}"
mapping_dict[item] = (fpath, target_path, is_abs)
else:
mapping_dict[item] = (fpath, fpath, is_abs)
return return
else: else:
return return
@@ -86,7 +95,7 @@ def process_local_file_path_async(mapping_dict, max_workers=10):
start_time = time.time() start_time = time.time()
with ThreadPoolExecutor(max_workers=max_workers) as executor: with ThreadPoolExecutor(max_workers=max_workers) as executor:
# Submit tasks to the executor # Submit tasks to the executor
futures = {executor.submit(upload_file_to_myshell, full_path): filename for filename, full_path in mapping_dict.items()} futures = {executor.submit(upload_file_to_myshell, source_path, target_path, is_abs): filename for filename, (source_path, target_path, is_abs) in mapping_dict.items()}
logging.info("submit done") logging.info("submit done")
# Collect the results as they complete # Collect the results as they complete
for future in as_completed(futures): for future in as_completed(futures):
@@ -95,7 +104,8 @@ def process_local_file_path_async(mapping_dict, max_workers=10):
result = future.result() result = future.result()
mapping_dict[filename] = result mapping_dict[filename] = result
except Exception as e: except Exception as e:
print(f"Error processing {filename}: {e}") del mapping_dict[filename]
raise NotImplementedError(f"Error processing {filename}: {e}")
end_time = time.time() end_time = time.time()
logging.info(f"upload end, elapsed time: {end_time - start_time}") logging.info(f"upload end, elapsed time: {end_time - start_time}")
return return
+5
View File
@@ -0,0 +1,5 @@
{
"comfyui-ollama": {
"reason": "this node requires installing an extra software on linux, which is currently unsupported"
}
}
+44 -18
View File
@@ -1,19 +1,45 @@
{ {
"ComfyUI-Easy-Use": [ "ComfyUI-Easy-Use": [
{ {
"name": "ComfyUI-Inspire-Pack", "name": "ComfyUI-Inspire-Pack",
"repo": "https://github.com/ltdrdata/ComfyUI-Inspire-Pack.git", "repo": "https://github.com/ltdrdata/ComfyUI-Inspire-Pack.git",
"commit": "" "commit": ""
}, },
{ {
"name": "ComfyUI-Advanced-ControlNet", "name": "ComfyUI-Advanced-ControlNet",
"repo": "https://github.com/Kosinkadink/ComfyUI-Advanced-ControlNet.git", "repo": "https://github.com/Kosinkadink/ComfyUI-Advanced-ControlNet.git",
"commit": "" "commit": ""
}, },
{ {
"name": "ComfyUI_smZNodes", "name": "ComfyUI_smZNodes",
"repo": "https://github.com/shiimizu/ComfyUI_smZNodes.git", "repo": "https://github.com/shiimizu/ComfyUI_smZNodes.git",
"commit": "" "commit": ""
} },
] {
} "name": "ComfyUI_IPAdapter_plus",
"repo": "https://github.com/cubiq/ComfyUI_IPAdapter_plus.git",
"commit": ""
}
],
"efficiency-nodes-comfyui": [
{
"name": "comfyui_controlnet_aux",
"repo": "https://github.com/Fannovel16/comfyui_controlnet_aux.git",
"commit": ""
}
],
"ComfyUI-Anyline": [
{
"name": "comfyui_controlnet_aux",
"repo": "https://github.com/Fannovel16/comfyui_controlnet_aux.git",
"commit": ""
}
],
"ComfyUI-Impact-Pack": [
{
"name": "ComfyUI-Impact-Subpack",
"repo": "https://github.com/ltdrdata/ComfyUI-Impact-Subpack.git",
"commit": ""
}
]
}
+3
View File
@@ -0,0 +1,3 @@
[
"BizyAir"
]
+3 -1
View File
@@ -3,4 +3,6 @@ pydantic
opencv-python opencv-python
imageio-ffmpeg imageio-ffmpeg
brotli brotli
# logfire pillow_heif
easydict
# logfire
+1197
View File
File diff suppressed because it is too large Load Diff
+16 -1
View File
@@ -1,6 +1,8 @@
import hashlib import hashlib
import time import time
from pathlib import PurePosixPath, Path, PureWindowsPath from pathlib import PurePosixPath, Path, PureWindowsPath
import base64
import re
def windows_to_linux_path(windows_path): def windows_to_linux_path(windows_path):
return PureWindowsPath(windows_path).as_posix() return PureWindowsPath(windows_path).as_posix()
@@ -17,4 +19,17 @@ def compute_sha256(file_path, chunk_size=1024 ** 2):
sha256.update(chunk) sha256.update(chunk)
print("finish compute sha256 for", file_path, f"time: {time.time() - start}") print("finish compute sha256 for", file_path, f"time: {time.time() - start}")
# Return the hexadecimal digest of the hash # Return the hexadecimal digest of the hash
return sha256.hexdigest() return sha256.hexdigest()
def get_alphanumeric_hash(input_string: str) -> str:
# Generate a SHA-256 hash of the input string
sha256_hash = hashlib.sha256(input_string.encode()).digest()
# Encode the hash in base64 to get a string with [A-Za-z0-9+/=]
base64_hash = base64.b64encode(sha256_hash).decode('ascii')
# Remove any non-alphanumeric characters (+, /, =)
alphanumeric_hash = re.sub(r'[^a-zA-Z0-9]', '', base64_hash)
return alphanumeric_hash
+124
View File
@@ -1,6 +1,9 @@
import { app } from "../../scripts/app.js"; import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js"; import { api } from "../../scripts/api.js";
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
app.registerExtension({ app.registerExtension({
name: "Shellagent.extension", name: "Shellagent.extension",
async setup() { async setup() {
@@ -173,6 +176,18 @@ app.registerExtension({
} }
} }
if (nodeData.name === "ShellAgentPluginInputAudio") {
if (
nodeData?.input?.required?.default_value?.[1]?.audio_upload === true
) {
nodeData.input.required.audioUI = ["AUDIO_UI"];
nodeData.input.required.upload = [
"SHELLAGENT_AUDIOUPLOAD",
{ widget: "default_value" },
];
}
}
if (nodeData.name === "ShellAgentPluginInputVideo") { if (nodeData.name === "ShellAgentPluginInputVideo") {
addUploadWidget(nodeType, nodeData, "default_value"); addUploadWidget(nodeType, nodeData, "default_value");
chainCallback(nodeType.prototype, "onNodeCreated", function () { chainCallback(nodeType.prototype, "onNodeCreated", function () {
@@ -354,23 +369,81 @@ app.registerExtension({
afterConfigureGraph(missingNodeTypes, app) { afterConfigureGraph(missingNodeTypes, app) {
function addIn(type, nodeId) { function addIn(type, nodeId) {
if(LiteGraph.slot_types_default_in[type] == null) {
LiteGraph.slot_types_default_in[type] = []
}
if (LiteGraph.slot_types_default_in[type].indexOf(nodeId) === -1) { if (LiteGraph.slot_types_default_in[type].indexOf(nodeId) === -1) {
LiteGraph.slot_types_default_in[type].unshift(nodeId) LiteGraph.slot_types_default_in[type].unshift(nodeId)
} }
} }
function addOut(type, nodeId) { function addOut(type, nodeId) {
if(LiteGraph.slot_types_default_out[type] == null) {
LiteGraph.slot_types_default_out[type] = []
}
if (LiteGraph.slot_types_default_out[type].indexOf(nodeId) === -1) { if (LiteGraph.slot_types_default_out[type].indexOf(nodeId) === -1) {
LiteGraph.slot_types_default_out[type].unshift(nodeId) LiteGraph.slot_types_default_out[type].unshift(nodeId)
} }
} }
addIn('IMAGE', 'ShellAgentPluginInputImage') addIn('IMAGE', 'ShellAgentPluginInputImage')
addIn('AUDIO', 'ShellAgentPluginInputAudio')
addOut('IMAGE', 'ShellAgentPluginSaveImage') addOut('IMAGE', 'ShellAgentPluginSaveImage')
addOut('IMAGE', 'ShellAgentPluginSaveImages') addOut('IMAGE', 'ShellAgentPluginSaveImages')
addOut('AUDIO', 'ShellAgentPluginSaveAudios')
addOut('AUDIO', 'ShellAgentPluginSaveAudio')
addOut('STRING', 'ShellAgentPluginOutputInteger') addOut('STRING', 'ShellAgentPluginOutputInteger')
addOut('STRING', 'ShellAgentPluginOutputFloat') addOut('STRING', 'ShellAgentPluginOutputFloat')
addOut('STRING', 'ShellAgentPluginOutputText') addOut('STRING', 'ShellAgentPluginOutputText')
},
getCustomWidgets() {
return {
SHELLAGENT_AUDIOUPLOAD(node, inputName) {
const audioWidget = node.widgets.find(
(w) => w.name === "default_value"
);
const audioUIWidget = node.widgets.find(
(w) => w.name === "audioUI"
);
const onAudioWidgetUpdate = /* @__PURE__ */ __name(() => {
audioUIWidget.element.src = api.apiURL(
getResourceURL(...splitFilePath(audioWidget.value))
);
}, "onAudioWidgetUpdate");
if (audioWidget.value) {
onAudioWidgetUpdate();
}
audioWidget.callback = onAudioWidgetUpdate;
const onGraphConfigured = node.onGraphConfigured;
node.onGraphConfigured = function() {
onGraphConfigured?.apply(this, arguments);
if (audioWidget.value) {
onAudioWidgetUpdate();
}
};
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = "audio/*";
fileInput.style.display = "none";
fileInput.onchange = () => {
if (fileInput.files.length) {
uploadFileAudio(audioWidget, audioUIWidget, fileInput.files[0], true);
}
};
const uploadWidget = node.addWidget(
"button",
inputName,
/* value=*/
"",
() => {
fileInput.click();
},
{ serialize: false }
);
uploadWidget.label = "choose file to upload";
return { widget: uploadWidget };
}
};
} }
}); });
@@ -744,4 +817,55 @@ function addLoadVideoCommon(nodeType, nodeData) {
} }
}); });
}); });
}
function getResourceURL(subfolder, filename, type = "input") {
const params = [
"filename=" + encodeURIComponent(filename),
"type=" + type,
"subfolder=" + subfolder,
app.getRandParam().substring(1)
].join("&");
return `/view?${params}`;
}
function splitFilePath(path) {
const folder_separator = path.lastIndexOf("/");
if (folder_separator === -1) {
return ["", path];
}
return [
path.substring(0, folder_separator),
path.substring(folder_separator + 1)
];
}
async function uploadFileAudio(audioWidget, audioUIWidget, file2, updateNode, pasted = false) {
try {
const body = new FormData();
body.append("image", file2);
if (pasted) body.append("subfolder", "pasted");
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body
});
if (resp.status === 200) {
const data = await resp.json();
let path = data.name;
if (data.subfolder) path = data.subfolder + "/" + path;
if (!audioWidget.options.values.includes(path)) {
audioWidget.options.values.push(path);
}
if (updateNode) {
audioUIWidget.element.src = api.apiURL(
getResourceURL(...splitFilePath(path))
);
audioWidget.value = path;
}
} else {
window.alert(resp.status + " - " + resp.statusText);
}
} catch (error) {
window.alert(error);
}
} }