Compare commits

..
Author SHA1 Message Date
Chengwei Ouyang ced3e0de2f remove image 2025-03-12 15:11:54 +08:00
shanexi 0599875a79 fix: input image error in new comfyui version 2025-03-12 14:55:39 +08:00
Chengwei Ouyang 33899c19dc Merge pull request #10 from myshell-ai/fix_input_image
[Fix] Adapt to the new version comfyui
2025-03-10 23:28:05 +08:00
Chengwei Ouyang 06163a06d4 i[date 2025-03-10 23:21:36 +08:00
shanexi 087571346c Merge pull request #9 from myshell-ai/support-input-video
feat: support input audio
2025-02-13 17:44:28 +08:00
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
shanexi f28c2c6b31 Replace with default value assigned 2024-10-28 11:20:03 +08:00
wl-zhao 344a886792 add pypi version info 2024-10-28 11:19:19 +08:00
shanexi fffc29fc2b Add missing convert output 2024-10-28 11:15:39 +08:00
shanexi 20fcbb2e48 Fix duplicated drag menu item 2024-10-28 11:14:05 +08:00
shanexi f9a3ce43f5 Replace and remove 2024-10-28 11:09:29 +08:00
shanexi cbd714d9ea Replace Load Image with ShellAgent Input Image 2024-10-28 11:02:18 +08:00
shanexi b825b62a96 Drag to connect output text float integer 2024-10-28 10:09:38 +08:00
shanexi ae8ed60767 Save Image(s) on output connect pop menu 2024-10-28 09:52:28 +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
shanexi 710104d709 Image input 2024-10-27 16:02:48 +08:00
wl-zhao 752a0de95d add gguf 2024-10-27 15:39:16 +08:00
shanexi 9417b64458 No need to connect image combo 2024-10-27 15:23:13 +08:00
shanexi 4fb799112a Convert to save video 2024-10-27 09:42:31 +08:00
shanexi d2cfd99a33 Ouput convert 2024-10-27 09:33:06 +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
shanexi 15c7d7f60c Optimize number input convert 2024-10-24 21:41:17 +08:00
Xumin Yu 26e57ef44f Update dependency_checker.py 2024-10-23 16:32:40 +08:00
wl-zhao 00fb0b91cd handle model_searcher fail 2024-10-23 10:46:48 +08:00
wl-zhao fc1f9afbcd add new output nodes 2024-10-22 19:47:04 +08:00
wl-zhao b3042fdf6f fix bug 2024-10-22 15:33:04 +08:00
wl-zhao bf8f352347 skip when .git is not found 2024-10-21 23:47:43 +08:00
wl-zhao 2eea258bc4 Merge branch 'main' of github.com:myshell-ai/ComfyUI-ShellAgent-Plugin into main 2024-10-21 23:12:21 +08:00
wl-zhao be2725c2cb add dependency checker 2024-10-21 23:12:12 +08:00
shanexi 3f742ed72f Merge pull request #5 from myshell-ai/Convert-to-ShellAgent-with-default-value-assigned
Convert to ShellAgent with default value assigned
2024-10-21 17:49:07 +08:00
shanexi 6a27517db6 Convert to ShellAgent with default value assigned 2024-10-21 17:33:59 +08:00
wl-zhao 68e5dad893 update file path 2024-10-20 11:23:02 +08:00
wl-zhao 9a606f550d update file_upload 2024-10-20 10:43:40 +08:00
wl-zhao 1b5fd5e7d5 update file_upload 2024-10-20 10:41:12 +08:00
wl-zhao 44ac4cd0f8 update relpath of models 2024-10-20 10:19:22 +08:00
wl-zhao 83e1caa5fa update relpath of models 2024-10-20 10:04:06 +08:00
wl-zhao 58200be879 fix folder_path bugs 2024-10-20 09:37:32 +08:00
wl-zhao 895fc30c37 fix folder_path bugs 2024-10-20 09:33:56 +08:00
wl-zhao 62ab265a80 update catch git error 2024-10-20 09:21:54 +08:00
wl-zhao f1bf4485c1 update validate inputs 2024-10-18 14:05:58 +08:00
wl-zhao b8292f45ba update commit of node deps 2024-10-18 13:54:52 +08:00
wl-zhao 166fb12aba update node_deps_info 2024-10-18 13:37:07 +08:00
wl-zhao 3f6f36cd81 update node_deps_info 2024-10-18 12:25:48 +08:00
wl-zhao 0982abfcf8 update node_deps_info 2024-10-18 12:18:52 +08:00
wl-zhao e06fb3d5d3 update model suffix 2024-10-18 10:25:54 +08:00
wl-zhao 6afc9ce442 use glob to traverse 2024-10-18 10:16:30 +08:00
wl-zhao 0d9bd35d84 Merge branch 'main' of github.com:myshell-ai/ComfyUI-ShellAgent-Plugin into main 2024-10-18 10:05:45 +08:00
wl-zhao f49589ee82 add printed logs 2024-10-18 10:05:36 +08:00
tiancheng c33e93af11 README and license 2024-10-18 01:58:27 +03:00
wl-zhao 60d68d99ef fix input image bug 2024-10-17 23:40:42 +08:00
wl-zhao b8b6657801 fix input image bug 2024-10-17 23:38:46 +08:00
wl-zhao ad8715f3a2 fix input image bug 2024-10-17 23:21:31 +08:00
wl-zhao 92ab0995e4 fix input image bug 2024-10-17 23:12:36 +08:00
Wenliang Zhao e146e2512f Merge pull request #2 from myshell-ai/1-optimize-custom-node
1 optimize custom node
2024-10-17 16:36:50 +08:00
wl-zhao 49fb34f250 update error message 2024-10-17 16:26:45 +08:00
wl-zhao f2926099fe update error message 2024-10-17 16:22:49 +08:00
shanexi fb4e608fc6 Fix bugs 2024-10-17 16:22:11 +08:00
shanexi f6b746b213 Support number 2024-10-17 15:55:18 +08:00
wl-zhao f78f07961e update LoRA Stacker 2024-10-17 15:22:02 +08:00
wl-zhao 8311254009 update error message when missing nodes 2024-10-17 15:17:40 +08:00
wl-zhao aa185a6ff3 update 2024-10-17 14:42:36 +08:00
wl-zhao 83146c06f4 update model dependency loader 2024-10-17 14:28:12 +08:00
wl-zhao 6422a94ed4 update model dependency loader 2024-10-17 14:24:22 +08:00
wl-zhao 2a59a8a01e fix integer type 2024-10-17 11:47:44 +08:00
shanexi 370975a4ac Refactor connect 2024-10-17 11:20:20 +08:00
shanexi aa984eb533 ShellAgentPluginInputInteger add manage choices and client-side form validate 2024-10-17 10:58:21 +08:00
wl-zhao cf86159484 update requirements.txt 2024-10-17 00:31:51 +08:00
wl-zhao 1ec16a1662 update 2024-10-17 00:31:40 +08:00
shanexi 46e6b3ff91 Fix choices 2024-10-16 12:08:51 +08:00
shanexi 4e929fdc1b Remove console.log 2024-10-16 11:18:12 +08:00
shanexi cc4f0fdd79 Choices 2024-10-16 11:16:33 +08:00
shanexi d6f8d9156a Revert forceInput, comfyUI state is related somehow 2024-10-16 10:57:40 +08:00
shanexi 6905252b4d Fix api 2024-10-15 17:31:13 +08:00
shanexi da20ddb4d5 Choices UI 2024-10-15 16:55:10 +08:00
shanexi 8187880d77 Fix api 2024-10-15 16:08:22 +08:00
shanexi 1e3663f6c6 Input video 2024-10-15 15:34:26 +08:00
shanexi f52789d932 Refactor rename ext 2024-10-15 12:12:06 +08:00
shanexi d5ba41de7e Convert and connect 2024-10-15 12:06:29 +08:00
shanexi 9845c02811 Add convert to shellagent widget context menu 2024-10-15 11:41:18 +08:00
shanexi e9199161ea Force input 2024-10-15 10:18:14 +08:00
shanexi 261620ea3a Update image input 2024-10-14 17:32:47 +08:00
19 changed files with 3118 additions and 167 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)",
}
+108 -18
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
@@ -17,22 +45,22 @@ class ShellAgentPluginInputImage:
"required": { "required": {
"input_name": ( "input_name": (
"STRING", "STRING",
{"multiline": False, "default": "input_image"}, {"multiline": False, "default": "input_image", "forceInput": False},
), ),
"default_value": ( "default_value": (
"STRING", {"image_upload": True, "default": files[0] if len(files) else ""}, sorted(files), {"image_upload": True, "forceInput": False}
), ),
}, },
"optional": { "optional": {
"description": ( "description": (
"STRING", "STRING",
{"multiline": True, "default": ""}, {"multiline": False, "default": "", "forceInput": False},
), ),
} }
} }
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("image",) # RETURN_NAMES = ("image",)
FUNCTION = "run" FUNCTION = "run"
@@ -44,21 +72,82 @@ 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
@classmethod
def VALIDATE_INPUTS(s, input_name, default_value, image=None, description=""):
# check default_value first
image_to_check = default_value if default_value else image
if image_to_check.startswith("http"):
return True
if image_to_check == "":
return "Invalid image file: please check if the image is empty or invalid"
if os.path.isfile(image_to_check):
return True
if not folder_paths.exists_annotated_filepath(image_to_check):
return "Invalid image file: {}".format(image_to_check)
def run(self, input_name, default_value=None, display_name=None, description=None): 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, image=None, display_name=None, description=None):
# use default_value if it exists, otherwise use image
image_path = default_value if default_value else image
input_dir = folder_paths.get_input_directory() input_dir = folder_paths.get_input_directory()
image_path = default_value
try: try:
if image_path.startswith('http'): if image_path.startswith('http'):
import requests import requests
from io import BytesIO from io import BytesIO
print("Fetching image from url: ", image) print("Fetching image from url: ", image_path)
response = requests.get(image) 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
@@ -70,13 +159,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
+54 -6
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"])
@@ -146,12 +146,16 @@ class ShellAgentPluginInputInteger:
"description": ( "description": (
"STRING", "STRING",
{"multiline": True, "default": ""}, {"multiline": True, "default": ""},
) ),
"choices": (
"STRING",
{"multiline": False, "default": ""},
),
} }
} }
RETURN_TYPES = ("FLOAT",) RETURN_TYPES = ("INT",)
RETURN_NAMES = ("float",) RETURN_NAMES = ("int",)
FUNCTION = "run" FUNCTION = "run"
@@ -180,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)",
} }
+16 -3
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:
@@ -88,8 +88,12 @@ class ShellAgentPluginInputVideo:
{"multiline": False, "default": "input_video"}, {"multiline": False, "default": "input_video"},
), ),
"default_value": ( "default_value": (
"STRING", {"video_upload": True, "default": files[0] if len(files) else ""}, sorted(files),
{ "video_upload": True }
), ),
# "default_value": (
# "STRING", {"video_upload": True, "default": files[0] if len(files) else ""},
# ),
}, },
"optional": { "optional": {
"description": ( "description": (
@@ -116,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()
@@ -162,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 -5
View File
@@ -75,12 +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]}}
print(results)
return results return results
+88
View File
@@ -0,0 +1,88 @@
json_type_mapipng = {
"text": "string",
"float": "number",
"integer": "integer",
"boolean": "boolean",
}
class ShellAgentOutputText:
TYPE_STR = "text"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
s.TYPE_STR: ("STRING", {"tooltip": f"The {s.TYPE_STR} to output."}),
"output_name": ("STRING", {"multiline": False, "default": f"output_{s.TYPE_STR}"},),
},
}
RETURN_TYPES = ()
FUNCTION = "output_var"
OUTPUT_NODE = True
CATEGORY = "shellagent"
DESCRIPTION = "output the text"
@classmethod
def validate(cls, **kwargs):
schema = {
"title": kwargs["output_name"],
"type": json_type_mapipng[cls.TYPE_STR]
}
return schema
def output_var(self, **kwargs):
results = {"ui": {"output": [kwargs[self.TYPE_STR]]}}
return results
class ShellAgentOutputFloat(ShellAgentOutputText):
TYPE_STR = "float"
DESCRIPTION = "output the float"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
s.TYPE_STR: ("FLOAT", {"tooltip": f"The {s.TYPE_STR} to output."}),
"output_name": ("STRING", {"multiline": False, "default": f"output_{s.TYPE_STR}"},),
},
}
class ShellAgentOutputInteger(ShellAgentOutputText):
TYPE_STR = "integer"
DESCRIPTION = "output the integer"
@classmethod
def INPUT_TYPES(s):
return {
"required": {
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}"},),
},
}
NODE_CLASS_MAPPINGS = {
"ShellAgentPluginOutputText": ShellAgentOutputText,
"ShellAgentPluginOutputFloat": ShellAgentOutputFloat,
"ShellAgentPluginOutputInteger": ShellAgentOutputInteger,
"ShellAgentPluginOutputBoolean": ShellAgentOutputBoolean,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ShellAgentPluginOutputText": "Output Text (ShellAgent Plugin)",
"ShellAgentPluginOutputFloat": "Output Float (ShellAgent Plugin)",
"ShellAgentPluginOutputInteger": "Output Integer (ShellAgent Plugin)",
}
+68 -8
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
@@ -55,9 +66,11 @@ def schema_validator(prompt):
"outputs": {} "outputs": {}
} }
for node_id, node_info in prompt.items(): for node_id, node_info in prompt.items():
node_class_type = node_info["class_type"] node_class_type = node_info.get("class_type")
if node_class_type is None:
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]
if hasattr(node_cls, "RELATIVE_PYTHON_MODULE") and node_cls.RELATIVE_PYTHON_MODULE == "custom_nodes.ComfyUI-ShellAgent-Plugin": if hasattr(node_cls, "RELATIVE_PYTHON_MODULE") and node_cls.RELATIVE_PYTHON_MODULE.startswith("custom_nodes.ComfyUI-ShellAgent-Plugin"):
schema = {} schema = {}
if "input_name" in node_info["inputs"]: if "input_name" in node_info["inputs"]:
mode = "inputs" mode = "inputs"
@@ -82,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
@@ -122,6 +138,10 @@ async def shellagent_get_file(request):
async def shellagent_export(request): async def shellagent_export(request):
data = await request.json() data = await request.json()
prompt = data["prompt"] prompt = data["prompt"]
custom_dependencies = data.get("custom_dependencies", {
"models": {},
"custom_nodes": {}
})
# extra_data = data["extra_data"] # extra_data = data["extra_data"]
workflow_id = str(uuid.uuid4()) workflow_id = str(uuid.uuid4())
@@ -137,7 +157,7 @@ async def shellagent_export(request):
try: try:
schemas = schema_validator(prompt) schemas = schema_validator(prompt)
# custom_node.json # custom_node.json
dependency_results = resolve_dependencies(prompt) dependency_results = resolve_dependencies(prompt, custom_dependencies)
# save_root = os.path.join(WORKFLOW_ROOT, workflow_id) # save_root = os.path.join(WORKFLOW_ROOT, workflow_id)
# os.makedirs(save_root, exist_ok=True) # os.makedirs(save_root, exist_ok=True)
@@ -152,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.print_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)
+255 -73
View File
@@ -3,68 +3,50 @@ import subprocess
import json import json
import logging import logging
from functools import partial from functools import partial
import re
import glob
import sys
from folder_paths import models_dir as MODELS_DIR
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
ComfyUIModelLoaders = {
'VAELoader': (["vae_name"], "vae"),
'CheckpointLoader': (["ckpt_name"], "checkpoints"),
'CheckpointLoaderSimple': (["ckpt_name"], "checkpoints"),
'DiffusersLoader': (["model_path"], "diffusers"),
'unCLIPCheckpointLoader': (["ckpt_name"], "checkpoints"),
'LoraLoader': (["lora_name"], "loras"),
'LoraLoaderModelOnly': (["lora_name"], "loras"),
'ControlNetLoader': (["control_net_name"], "controlnet"),
'DiffControlNetLoader': (["control_net_name"], "controlnet"),
'UNETLoader': (["unet_name"], "unet"),
'CLIPLoader': (["clip_name"], "clip"),
'DualCLIPLoader': (["clip_name1", "clip_name2"], "clip"),
'CLIPVisionLoader': (["clip_name"], "clip_vision"),
'StyleModelLoader': (["style_model_name"], "style_models"),
'GLIGENLoader': (["gligen_name"], "gligen"),
'ImageOnlyCheckpointLoader': (["ckpt_name"], "checkpoints"),
"UpscaleModelLoader": (["model_name"], "upscale_models"),
"TripleCLIPLoader": (["clip_name1", "clip_name2", "clip_name3"], "clip"),
"HypernetworkLoader": (["hypernetwork_name"], "hypernetworks")
}
# ComfyUIFileLoaders = {
# 'VAELoader': (["vae_name"], "vae"),
# 'CheckpointLoader': (["ckpt_name"], "checkpoints"),
# 'CheckpointLoaderSimple': (["ckpt_name"], "checkpoints"),
# 'DiffusersLoader': (["model_path"], "diffusers"),
# 'unCLIPCheckpointLoader': (["ckpt_name"], "checkpoints"),
# 'LoraLoader': (["lora_name"], "loras"),
# 'LoraLoaderModelOnly': (["lora_name"], "loras"),
# 'ControlNetLoader': (["control_net_name"], "controlnet"),
# 'DiffControlNetLoader': (["control_net_name"], "controlnet"),
# 'UNETLoader': (["unet_name"], "unet"),
# 'CLIPLoader': (["clip_name"], "clip"),
# 'DualCLIPLoader': (["clip_name1", "clip_name2"], "clip"),
# 'CLIPVisionLoader': (["clip_name"], "clip_vision"),
# 'StyleModelLoader': (["style_model_name"], "style_models"),
# 'GLIGENLoader': (["gligen_name"], "gligen"),
# }
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")))
def handle_model_info(ckpt_path): 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_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"]
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, 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 = dirname.split('/', 1)[1]
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))
model_id = metadata["id"] model_id = metadata["id"]
else: else:
logging.info(f"computing sha256 of {ckpt_path}") logging.info(f"computing sha256 of {ckpt_path}")
if not os.path.isfile(ckpt_path):
raise NotImplementedError(f"please install {ckpt_path} first!")
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"))
@@ -74,8 +56,8 @@ def handle_model_info(ckpt_path):
urls = [] urls = []
item = { item = {
"filename": filename, "filename": windows_to_linux_path(filename),
"save_path": save_path, "save_path": windows_to_linux_path(rel_save_path),
"urls": urls, "urls": urls,
} }
return model_id, item return model_id, item
@@ -88,13 +70,17 @@ def inspect_repo_version(module_path):
"repo": "", "repo": "",
"commit": "" "commit": ""
} }
if not os.path.isdir(os.path.join(module_path, ".git")):
return result
# Get the remote repository URL # Get the remote repository URL
try: try:
remote_url = subprocess.check_output( remote_url = subprocess.check_output(
['git', 'config', '--get', 'remote.origin.url'], ['git', 'config', '--get', 'remote.origin.url'],
cwd=module_path cwd=module_path
).strip().decode() ).strip().decode()
except subprocess.CalledProcessError: except Exception:
return result return result
# Get the latest commit hash # Get the latest commit hash
@@ -103,7 +89,7 @@ def inspect_repo_version(module_path):
['git', 'rev-parse', 'HEAD'], ['git', 'rev-parse', 'HEAD'],
cwd=module_path cwd=module_path
).strip().decode() ).strip().decode()
except subprocess.CalledProcessError: except Exception:
return result return result
# Create and return the JSON result # Create and return the JSON result
@@ -114,57 +100,253 @@ def inspect_repo_version(module_path):
} }
return result return result
def fetch_model_searcher_results(model_ids):
import requests
url = "https://models-searcher.myshell.life/search_urls"
headers = {
"Content-Type": "application/json"
}
data = {
"sha256": model_ids
}
def resolve_dependencies(prompt): # resolve custom nodes and models at the same time response = requests.post(url, headers=headers, json=data)
if response.status_code == 200:
results = [item[:10] for item in response.json()]
else:
results = None
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
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["class_type"] node_class_type = node_info.get("class_type")
if node_class_type is None:
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]
if hasattr(node_cls, "RELATIVE_PYTHON_MODULE"):
skip_model_check = False
custom_node_path = None
if hasattr(node_cls, "RELATIVE_PYTHON_MODULE") and node_cls.RELATIVE_PYTHON_MODULE.startswith("custom_nodes."):
print(node_cls.RELATIVE_PYTHON_MODULE)
custom_nodes.append(node_cls.RELATIVE_PYTHON_MODULE) custom_nodes.append(node_cls.RELATIVE_PYTHON_MODULE)
if node_class_type in ComfyUIModelLoaders: custom_node_path = os.path.join(BASE_PATH, node_cls.RELATIVE_PYTHON_MODULE.replace(".", "/"))
input_names, save_path = ComfyUIModelLoaders[node_class_type] if node_cls.RELATIVE_PYTHON_MODULE[len("custom_nodes."):] in node_remote_skip_models:
for input_name in input_names: skip_model_check = True
ckpt_path = os.path.join("models", save_path, node_info["inputs"][input_name]) print(f"skip model check for {node_class_type}")
ckpt_paths.append(ckpt_path)
if node_class_type in model_loaders_info:
for field_name, filename in node_info["inputs"].items():
if type(filename) != str:
continue
for item in model_loaders_info[node_class_type]:
pattern = item["field_name"]
if re.match(f"^{pattern}$", field_name) and any([filename.endswith(possible_suffix) for possible_suffix in model_suffix]):
ckpt_path = get_full_path_or_raise(item["save_path"], filename)
if hasattr(folder_paths, "map_legacy"):
save_folder = folder_paths.map_legacy(item["save_path"])
else:
save_folder = item["save_path"]
rel_save_path = os.path.relpath(folder_paths.folder_names_and_paths[save_folder][0][0], folder_paths.models_dir)
ckpt_paths[ckpt_path] = {
"filename": filename,
"rel_save_path": rel_save_path
}
elif not skip_model_check:
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)
custom_nodes = list(set(custom_nodes)) custom_nodes = list(set(custom_nodes))
# step 0: comfyui version # step 0: comfyui version
comfyui_version = inspect_repo_version("./") repo_info = inspect_repo_version(BASE_PATH)
if repo_info["repo"] == "":
repo_info["require_recheck"] = True
if repo_info["name"] in custom_dependencies["custom_nodes"]:
repo_info["repo"] = custom_dependencies["custom_nodes"][repo_info["name"]].get("repo", "")
repo_info["commit"] = custom_dependencies["custom_nodes"][repo_info["name"]].get("commit", "")
comfyui_version = repo_info
# step 1: custom nodes # step 1: custom nodes
custom_nodes_list = [] custom_nodes_list = []
custom_nodes_names = []
requirements_lines = []
for custom_node in custom_nodes: for custom_node in custom_nodes:
try: try:
repo_info = inspect_repo_version(custom_node.replace(".", "/")) repo_info = inspect_repo_version(os.path.join(BASE_PATH, custom_node.replace(".", "/")))
custom_nodes_list.append(repo_info) custom_nodes_list.append(repo_info)
if repo_info["repo"] == "":
repo_info["require_recheck"] = True
if repo_info["name"] in custom_dependencies["custom_nodes"]:
repo_info["repo"] = custom_dependencies["custom_nodes"][repo_info["name"]].get("repo", "")
repo_info["commit"] = custom_dependencies["custom_nodes"][repo_info["name"]].get("commit", "")
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:
if repo_name in node_deps_info:
for deps_node in node_deps_info[repo_name]:
if deps_node["name"] not in custom_nodes_names:
repo_info = inspect_repo_version(os.path.join(BASE_PATH, "custom_nodes", deps_node["name"]))
deps_node["commit"] = repo_info["commit"]
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 = {}
for ckpt_path in ckpt_paths: missing_model_ids = []
model_id, item = handle_model_info(ckpt_path) for ckpt_path, ckpt_info in ckpt_paths.items():
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:
item["require_recheck"] = True
if model_id in custom_dependencies["models"]:
item["urls"] = custom_dependencies["models"][model_id].get("urls", [])
missing_model_ids.append(model_id)
# try to fetch from myshell model searcher
missing_model_results_myshell = fetch_model_searcher_results(missing_model_ids)
if missing_model_results_myshell is not None:
for missing_model_id, missing_model_urls in zip(missing_model_ids, missing_model_results_myshell):
if len(missing_model_urls) > 0:
models_dict[missing_model_id]["require_recheck"] = False
models_dict[missing_model_id]["urls"] = missing_model_urls
print("successfully fetch results from myshell", models_dict[missing_model_id])
# 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": v[2], "urls": [v[1]]} for v in file_mapping_dict.values()} files_dict = {
dependencies = { v[0]: {
"models": models_dict, "filename": windows_to_linux_path(os.path.relpath(v[2], BASE_PATH)) if not v[3] else v[2],
"files": files_dict "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
+27 -13
View File
@@ -3,8 +3,9 @@ import os
import requests import requests
import time import time
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
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
@@ -26,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")
@@ -45,13 +46,13 @@ def upload_file_to_myshell(local_file: str) -> str:
start_time = time.time() start_time = time.time()
ext = os.path.splitext(local_file)[1] ext = os.path.splitext(local_file)[1]
files = [ files = [
('file', (os.path.basename(local_file), open(local_file, 'rb'), ext_to_type[ext])), ('file', (os.path.basename(local_file), open(local_file, 'rb'), ext_to_type[ext.lower()])),
] ]
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"
@@ -59,19 +60,31 @@ def upload_file_to_myshell(local_file: str) -> str:
def collect_local_file(item, mapping_dict={}): def collect_local_file(item, mapping_dict={}):
input_dir = folder_paths.get_input_directory()
if not isinstance(item, str): if not isinstance(item, str):
return return
abspath = os.path.abspath(item)
input_abspath = os.path.join(input_dir, item)
# required file type # required file type
if os.path.isfile(item): is_abs = False
fpath = item if os.path.isfile(abspath):
elif os.path.isfile(f"input/{item}"): fpath = abspath
fpath = f"input/{item}" is_abs = True
elif os.path.isfile(input_abspath):
fpath = input_abspath
else: else:
fpath = None fpath = None
if fpath is not None: if fpath is not None:
ext = os.path.splitext(fpath)[1] ext = os.path.splitext(fpath)[1]
if ext 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
@@ -82,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):
@@ -91,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
+148
View File
@@ -0,0 +1,148 @@
{
"VAELoader": [
{
"field_name": "vae_name",
"save_path": "vae"
}
],
"CheckpointLoader": [
{
"field_name": "ckpt_name",
"save_path": "checkpoints"
}
],
"CheckpointLoaderSimple": [
{
"field_name": "ckpt_name",
"save_path": "checkpoints"
}
],
"DiffusersLoader": [
{
"field_name": "model_path",
"save_path": "diffusers"
}
],
"unCLIPCheckpointLoader": [
{
"field_name": "ckpt_name",
"save_path": "checkpoints"
}
],
"LoraLoader": [
{
"field_name": "lora_name",
"save_path": "loras"
}
],
"LoraLoaderModelOnly": [
{
"field_name": "lora_name",
"save_path": "loras"
}
],
"ControlNetLoader": [
{
"field_name": "control_net_name",
"save_path": "controlnet"
}
],
"DiffControlNetLoader": [
{
"field_name": "control_net_name",
"save_path": "controlnet"
}
],
"UNETLoader": [
{
"field_name": "unet_name",
"save_path": "unet"
}
],
"CLIPLoader": [
{
"field_name": "clip_name",
"save_path": "clip"
}
],
"DualCLIPLoader": [
{
"field_name": "clip_name[1-2]",
"save_path": "clip"
}
],
"CLIPVisionLoader": [
{
"field_name": "clip_name",
"save_path": "clip_vision"
}
],
"StyleModelLoader": [
{
"field_name": "style_model_name",
"save_path": "style_models"
}
],
"GLIGENLoader": [
{
"field_name": "gligen_name",
"save_path": "gligen"
}
],
"ImageOnlyCheckpointLoader": [
{
"field_name": "ckpt_name",
"save_path": "checkpoints"
}
],
"UpscaleModelLoader": [
{
"field_name": "model_name",
"save_path": "upscale_models"
}
],
"TripleCLIPLoader": [
{
"field_name": "clip_name[1-3]",
"save_path": "clip"
}
],
"HypernetworkLoader": [
{
"field_name": "hypernetwork_name",
"save_path": "hypernetworks"
}
],
"SUPIR_model_loader_v2": [
{
"field_name": "supir_model",
"save_path": "checkpoints"
}
],
"SUPIR_model_loader_v2_clip": [
{
"field_name": "supir_model",
"save_path": "checkpoints"
}
],
"Efficient Loader": [
{
"field_name": "ckpt_name",
"save_path": "checkpoints"
},
{
"field_name": "vae_name",
"save_path": "vae"
},
{
"field_name": "lora_name",
"save_path": "loras"
}
],
"LoRA Stacker": [
{
"field_name": "lora_name_([1-9]|[1-4][0-9]|50)",
"save_path": "loras"
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"comfyui-ollama": {
"reason": "this node requires installing an extra software on linux, which is currently unsupported"
}
}
+45
View File
@@ -0,0 +1,45 @@
{
"ComfyUI-Easy-Use": [
{
"name": "ComfyUI-Inspire-Pack",
"repo": "https://github.com/ltdrdata/ComfyUI-Inspire-Pack.git",
"commit": ""
},
{
"name": "ComfyUI-Advanced-ControlNet",
"repo": "https://github.com/Kosinkadink/ComfyUI-Advanced-ControlNet.git",
"commit": ""
},
{
"name": "ComfyUI_smZNodes",
"repo": "https://github.com/shiimizu/ComfyUI_smZNodes.git",
"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"
]
+8
View File
@@ -0,0 +1,8 @@
aiofiles
pydantic
opencv-python
imageio-ffmpeg
brotli
pillow_heif
easydict
# logfire
-20
View File
@@ -1,20 +0,0 @@
import hashlib
import time
from pathlib import PurePosixPath, Path
def windows_to_linux_path(windows_path):
return str(PurePosixPath(Path(windows_path)))
def compute_sha256(file_path, chunk_size=1024 ** 2):
# Create a new sha256 hash object
start = time.time()
sha256 = hashlib.sha256()
print("start compute sha256 for", file_path)
# Open the file in binary mode
with open(file_path, 'rb') as file:
# Read the file in chunks to handle large files efficiently
while chunk := file.read(chunk_size):
sha256.update(chunk)
print("finish compute sha256 for", file_path, f"time: {time.time() - start}")
# Return the hexadecimal digest of the hash
return sha256.hexdigest()
+1197
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
import hashlib
import time
from pathlib import PurePosixPath, Path, PureWindowsPath
import base64
import re
def windows_to_linux_path(windows_path):
return PureWindowsPath(windows_path).as_posix()
def compute_sha256(file_path, chunk_size=1024 ** 2):
# Create a new sha256 hash object
start = time.time()
sha256 = hashlib.sha256()
print("start compute sha256 for", file_path)
# Open the file in binary mode
with open(file_path, 'rb') as file:
# Read the file in chunks to handle large files efficiently
while chunk := file.read(chunk_size):
sha256.update(chunk)
print("finish compute sha256 for", file_path, f"time: {time.time() - start}")
# Return the hexadecimal digest of the hash
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
+854 -21
View File
@@ -1,11 +1,15 @@
import { app } from "../../scripts/app.js"; import { app } from "../../scripts/app.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() {
window.parent.postMessage({ window.parent.postMessage({
type: 'loaded' type: 'loaded'
}, '*'); }, '*');
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {
if (event.data.type === 'save') { if (event.data.type === 'save') {
app.graphToPrompt().then(data => { app.graphToPrompt().then(data => {
@@ -16,23 +20,852 @@ app.registerExtension({
}, "*"); }, "*");
}); });
} }
if (event.data.type === 'load') { if (event.data.type === 'load') {
app.loadGraphData(event.data.data, true, false); app.loadGraphData(event.data.data, true, false);
} }
if (event.data.type === 'load_default') { if (event.data.type === 'load_default') {
// 使用FileReader读取JSON文件 // 使用FileReader读取JSON文件
fetch('extensions/ComfyUI-ShellAgent-Plugin/shellagent_default.json') fetch('extensions/ComfyUI-ShellAgent-Plugin/shellagent_default.json')
.then(response => response.blob()) .then(response => response.blob())
.then(blob => { .then(blob => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = function(e) { reader.onload = function (e) {
const json = JSON.parse(e.target.result); const json = JSON.parse(e.target.result);
app.loadGraphData(json, true, false); app.loadGraphData(json, true, false);
}; };
reader.readAsText(blob); reader.readAsText(blob);
}) })
.catch(error => console.error('加载默认JSON文件时出错:', error)); .catch(error => console.error('加载默认JSON文件时出错:', error));
} }
}); });
}, },
}); async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (["ShellAgentPluginOutputText", "ShellAgentPluginOutputFloat", "ShellAgentPluginOutputInteger"].indexOf(nodeData.name) > -1) {
chainCallback(nodeType.prototype, "onNodeCreated", function () {
this.convertWidgetToInput(this.widgets[0])
})
}
if (["ShellAgentPluginInputText", "ShellAgentPluginInputFloat", "ShellAgentPluginInputInteger"].indexOf(nodeData.name) > -1) {
chainCallback(nodeType.prototype, "onNodeCreated", function () {
const widget = this.widgets.find(w => w.name === 'choices')
this.addWidget('button', 'manage choices', null, () => {
const container = document.createElement("div");
Object.assign(container.style, {
display: "grid",
gridTemplateColumns: "1fr 1fr",
gap: "10px",
});
const addNew = document.createElement("button");
addNew.textContent = "Add New";
addNew.classList.add("pysssss-presettext-addnew");
Object.assign(addNew.style, {
fontSize: "13px",
gridColumn: "1 / 3",
color: "dodgerblue",
width: "auto",
textAlign: "center",
});
addNew.onclick = () => {
addRow("");
};
container.append(addNew);
function addRow(p) {
const value = document.createElement("input");
if (["ShellAgentPluginInputFloat", "ShellAgentPluginInputInteger"].indexOf(nodeData.name) > -1) {
value.type = 'number';
}
const valueLbl = document.createElement("label");
value.value = p;
Object.assign(value.style, {
width: "250px",
});
valueLbl.textContent = "Value:";
valueLbl.append(value);
Object.assign(valueLbl.style, {
gridColumn: "1 / 3",
width: "auto",
});
addNew.before(valueLbl);
}
let arr = []
if (typeof widget.value === 'string') {
try {
arr = JSON.parse(widget.value)
} catch { }
} else if (Array.isArray(widget.value)) {
arr = widget.value
}
for (const a of arr) {
addRow(a);
}
const help = document.createElement("span");
help.textContent = "To remove a item set the value to blank";
help.style.gridColumn = "1 / 3";
container.append(help);
dialog.show("");
dialog.textElement.append(container);
})
const dialog = new app.ui.dialog.constructor();
dialog.element.classList.add("comfy-settings");
const closeButton = dialog.element.querySelector("button");
closeButton.textContent = "CANCEL";
const saveButton = document.createElement("button");
saveButton.textContent = "SAVE";
saveButton.onclick = function () {
const inputs = dialog.element.querySelectorAll("input");
const p = [];
for (let i = 0; i < inputs.length; i += 1) {
const v = inputs[i];
if (!v.value.trim()) {
continue;
}
p.push(v.value);
}
widget.value = p;
dialog.close();
};
closeButton.before(saveButton);
})
}
if (['LoadImage', 'LoadImageMask'].indexOf(nodeData.name) > -1) {
addMenuHandler(nodeType, function (_, options) {
options.unshift({
content: "Replace with ShellAgent Input Image",
callback: () => {
const node = addNode("ShellAgentPluginInputImage", this, { before: true });
const dvn = node.widgets.find(w => w.name === 'default_value')
dvn.value = this.widgets.find(w => w.name === 'image')?.value
app.graph.links.filter(l => l != null)
.forEach(l => {
const tn = app.graph._nodes_by_id[l.target_id]
node.connect(0, tn, 0)
})
app.graph.remove(this);
}
})
})
}
if (nodeData.name === "ShellAgentPluginInputImage") {
if (
nodeData?.input?.required?.default_value?.[1]?.image_upload === true
) {
nodeData.input.required.upload = [
"IMAGEUPLOAD",
{ widget: "default_value", imageInputName: "default_value", image_upload: true },
];
}
}
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") {
addUploadWidget(nodeType, nodeData, "default_value");
chainCallback(nodeType.prototype, "onNodeCreated", function () {
const pathWidget = this.widgets.find((w) => w.name === "default_value");
chainCallback(pathWidget, "callback", (value) => {
if (!value) {
return;
}
let parts = ["input", value];
let extension_index = parts[1].lastIndexOf(".");
let extension = parts[1].slice(extension_index + 1);
let format = "video"
if (["gif", "webp", "avif"].includes(extension)) {
format = "image"
}
format += "/" + extension;
let params = { filename: parts[1], type: parts[0], format: format };
this.updateParameters(params, true);
});
});
addLoadVideoCommon(nodeType, nodeData);
}
if (nodeData.name.indexOf('ShellAgentPlugin') === -1) {
addMenuHandler(nodeType, function (_, options) {
if (this.widgets) {
let toInput = [];
for (const w of this.widgets) {
if (["customtext"].indexOf(w.type) > -1) {
toInput.push({
content: w.name,
submenu: {
options: [
{
content: 'Input Text',
callback: () => {
this.convertWidgetToInput(w);
const node = addNode("ShellAgentPluginInputText", this, { before: true });
const dvn = node.widgets.find(w => w.name === 'default_value')
dvn.value = w.value;
node.connect(0, this, this.inputs.length - 1);
}
}
]
},
})
}
if (["number"].indexOf(w.type) > -1) {
toInput.push({
content: w.name,
submenu: {
options: [
{
content: 'Input Interger',
callback: () => {
this.convertWidgetToInput(w);
const node = addNode("ShellAgentPluginInputInteger", this, { before: true });
const dvn = node.widgets.find(w => w.name === 'default_value')
dvn.value = w.value;
node.connect(0, this, this.inputs.length - 1);
}
},
{
content: 'Input Float',
callback: () => {
this.convertWidgetToInput(w);
const node = addNode("ShellAgentPluginInputFloat", this, { before: true });
const dvn = node.widgets.find(w => w.name === 'default_value')
dvn.value = w.value;
node.connect(0, this, this.inputs.length - 1);
}
}
]
}
})
}
}
if (toInput.length) {
options.unshift({
content: "Convert to ShellAgent (Input)",
submenu: {
options: toInput
}
})
}
}
if (this.outputs) {
let toOutput = [];
for (const o of this.outputs) {
if (o.type === 'IMAGE') {
toOutput.push({
content: o.name,
submenu: {
options: [
{
content: 'Save Image',
callback: () => {
const node = addNode("ShellAgentPluginSaveImage", this);
this.connect(0, node, 0);
}
},
{
content: 'Save Images',
callback: () => {
const node = addNode("ShellAgentPluginSaveImages", this);
this.connect(0, node, 0);
}
}
]
}
})
}
if (o.type === 'STRING') {
toOutput.push({
content: o.name,
submenu: {
options: [
{
content: `Output Text`,
callback: () => {
const node = addNode("ShellAgentPluginOutputText", this);
this.connect(0, node, 0);
}
},
{
content: `Output Float`,
callback: () => {
const node = addNode("ShellAgentPluginOutputFloat", this);
this.connect(0, node, 0);
}
},
{
content: `Output Integer`,
callback: () => {
const node = addNode("ShellAgentPluginOutputInteger", this);
this.connect(0, node, 0);
}
}
]
}
})
}
if (o.type === "VHS_FILENAMES") {
toOutput.push({
content: o.name,
submenu: {
options: [
{
content: `Save Video - VHS`,
callback: () => {
const node = addNode("ShellAgentPluginSaveVideoVHS", this);
this.connect(0, node, 0);
}
}
]
}
})
}
}
if (toOutput.length) {
options.unshift({
content: "Connect to ShellAgent (Output)",
submenu: {
options: toOutput
}
})
}
}
})
}
},
afterConfigureGraph(missingNodeTypes, app) {
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) {
LiteGraph.slot_types_default_in[type].unshift(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) {
LiteGraph.slot_types_default_out[type].unshift(nodeId)
}
}
addIn('IMAGE', 'ShellAgentPluginInputImage')
addIn('AUDIO', 'ShellAgentPluginInputAudio')
addOut('IMAGE', 'ShellAgentPluginSaveImage')
addOut('IMAGE', 'ShellAgentPluginSaveImages')
addOut('AUDIO', 'ShellAgentPluginSaveAudios')
addOut('AUDIO', 'ShellAgentPluginSaveAudio')
addOut('STRING', 'ShellAgentPluginOutputInteger')
addOut('STRING', 'ShellAgentPluginOutputFloat')
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 };
}
};
}
});
function addMenuHandler(nodeType, cb) {
const getOpts = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function () {
const r = getOpts.apply(this, arguments);
cb.apply(this, arguments);
return r;
};
}
function fitHeight(node) {
node.setSize([node.size[0], node.computeSize([node.size[0], node.size[1]])[1]])
node?.graph?.setDirtyCanvas(true);
}
function addNode(name, nextTo, options) {
options = { select: true, shiftY: 0, before: false, ...(options || {}) };
const node = LiteGraph.createNode(name);
app.graph.add(node);
node.pos = [
options.before ? nextTo.pos[0] - node.size[0] - 30 : nextTo.pos[0] + nextTo.size[0] + 30,
nextTo.pos[1] + options.shiftY,
];
if (options.select) {
app.canvas.selectNode(node, false);
}
return node;
}
function chainCallback(object, property, callback) {
if (object == undefined) {
//This should not happen.
console.error("Tried to add callback to non-existant object")
return;
}
if (property in object && object[property]) {
const callback_orig = object[property]
object[property] = function () {
const r = callback_orig.apply(this, arguments);
callback.apply(this, arguments);
return r
};
} else {
object[property] = callback;
}
}
async function uploadFile(file) {
//TODO: Add uploaded file to cache with Cache.put()?
try {
// Wrap file in formdata so it includes filename
const body = new FormData();
const i = file.webkitRelativePath.lastIndexOf('/');
const subfolder = file.webkitRelativePath.slice(0, i + 1)
const new_file = new File([file], file.name, {
type: file.type,
lastModified: file.lastModified,
});
body.append("image", new_file);
if (i > 0) {
body.append("subfolder", subfolder);
}
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body,
});
if (resp.status === 200) {
return resp
} else {
alert(resp.status + " - " + resp.statusText);
}
} catch (error) {
alert(error);
}
}
function addVideoPreview(nodeType) {
chainCallback(nodeType.prototype, "onNodeCreated", function () {
var element = document.createElement("div");
const previewNode = this;
var previewWidget = this.addDOMWidget("videopreview", "preview", element, {
serialize: false,
hideOnZoom: false,
getValue() {
return element.value;
},
setValue(v) {
element.value = v;
},
});
previewWidget.computeSize = function (width) {
if (this.aspectRatio && !this.parentEl.hidden) {
let height = (previewNode.size[0] - 20) / this.aspectRatio + 10;
if (!(height > 0)) {
height = 0;
}
this.computedHeight = height + 10;
return [width, height];
}
return [width, -4];//no loaded src, widget should not display
}
element.addEventListener('contextmenu', (e) => {
e.preventDefault()
return app.canvas._mousedown_callback(e)
}, true);
element.addEventListener('pointerdown', (e) => {
e.preventDefault()
return app.canvas._mousedown_callback(e)
}, true);
element.addEventListener('mousewheel', (e) => {
e.preventDefault()
return app.canvas._mousewheel_callback(e)
}, true);
previewWidget.value = {
hidden: false, paused: false, params: {},
muted: app.ui.settings.getSettingValue("VHS.DefaultMute", false)
}
previewWidget.parentEl = document.createElement("div");
previewWidget.parentEl.className = "vhs_preview";
previewWidget.parentEl.style['width'] = "100%"
element.appendChild(previewWidget.parentEl);
previewWidget.videoEl = document.createElement("video");
previewWidget.videoEl.controls = false;
previewWidget.videoEl.loop = true;
previewWidget.videoEl.muted = true;
previewWidget.videoEl.style['width'] = "100%"
previewWidget.videoEl.addEventListener("loadedmetadata", () => {
previewWidget.aspectRatio = previewWidget.videoEl.videoWidth / previewWidget.videoEl.videoHeight;
fitHeight(this);
});
previewWidget.videoEl.addEventListener("error", () => {
//TODO: consider a way to properly notify the user why a preview isn't shown.
previewWidget.parentEl.hidden = true;
fitHeight(this);
});
previewWidget.videoEl.onmouseenter = () => {
previewWidget.videoEl.muted = previewWidget.value.muted
};
previewWidget.videoEl.onmouseleave = () => {
previewWidget.videoEl.muted = true;
};
previewWidget.imgEl = document.createElement("img");
previewWidget.imgEl.style['width'] = "100%"
previewWidget.imgEl.hidden = true;
previewWidget.imgEl.onload = () => {
previewWidget.aspectRatio = previewWidget.imgEl.naturalWidth / previewWidget.imgEl.naturalHeight;
fitHeight(this);
};
var timeout = null;
this.updateParameters = (params, force_update) => {
if (!previewWidget.value.params) {
if (typeof (previewWidget.value != 'object')) {
previewWidget.value = { hidden: false, paused: false }
}
previewWidget.value.params = {}
}
Object.assign(previewWidget.value.params, params)
if (!force_update &&
!app.ui.settings.getSettingValue("VHS.AdvancedPreviews", false)) {
return;
}
if (timeout) {
clearTimeout(timeout);
}
if (force_update) {
previewWidget.updateSource();
} else {
timeout = setTimeout(() => previewWidget.updateSource(), 100);
}
};
previewWidget.updateSource = function () {
if (this.value.params == undefined) {
return;
}
let params = {}
Object.assign(params, this.value.params);//shallow copy
this.parentEl.hidden = this.value.hidden;
if (params.format?.split('/')[0] == 'video' ||
app.ui.settings.getSettingValue("VHS.AdvancedPreviews", false) &&
(params.format?.split('/')[1] == 'gif') || params.format == 'folder') {
this.videoEl.autoplay = !this.value.paused && !this.value.hidden;
let target_width = 256
if (element.style?.width) {
//overscale to allow scrolling. Endpoint won't return higher than native
target_width = element.style.width.slice(0, -2) * 2;
}
if (!params.force_size || params.force_size.includes("?") || params.force_size == "Disabled") {
params.force_size = target_width + "x?"
} else {
let size = params.force_size.split("x")
let ar = parseInt(size[0]) / parseInt(size[1])
params.force_size = target_width + "x" + (target_width / ar)
}
if (app.ui.settings.getSettingValue("VHS.AdvancedPreviews", false)) {
this.videoEl.src = api.apiURL('/viewvideo?' + new URLSearchParams(params));
} else {
previewWidget.videoEl.src = api.apiURL('/view?' + new URLSearchParams(params));
}
this.videoEl.hidden = false;
this.imgEl.hidden = true;
} else if (params.format?.split('/')[0] == 'image') {
//Is animated image
this.imgEl.src = api.apiURL('/view?' + new URLSearchParams(params));
this.videoEl.hidden = true;
this.imgEl.hidden = false;
}
}
previewWidget.parentEl.appendChild(previewWidget.videoEl)
previewWidget.parentEl.appendChild(previewWidget.imgEl)
});
}
function addUploadWidget(nodeType, nodeData, widgetName, type = "video") {
chainCallback(nodeType.prototype, "onNodeCreated", function () {
const pathWidget = this.widgets.find((w) => w.name === widgetName);
const fileInput = document.createElement("input");
chainCallback(this, "onRemoved", () => {
fileInput?.remove();
});
if (type == "video") {
Object.assign(fileInput, {
type: "file",
accept: "video/webm,video/mp4,video/mkv,image/gif",
style: "display: none",
onchange: async () => {
if (fileInput.files.length) {
let resp = await uploadFile(fileInput.files[0])
if (resp.status != 200) {
//upload failed and file can not be added to options
return;
}
const filename = (await resp.json()).name;
pathWidget.options.values.push(filename);
pathWidget.value = filename;
if (pathWidget.callback) {
pathWidget.callback(filename)
}
}
},
});
} else {
throw "Unknown upload type"
}
document.body.append(fileInput);
let uploadWidget = this.addWidget("button", "choose " + type + " to upload", "image", () => {
//clear the active click event
app.canvas.node_widget = null
fileInput.click();
});
uploadWidget.options.serialize = false;
});
}
function addPreviewOptions(nodeType) {
chainCallback(nodeType.prototype, "getExtraMenuOptions", function (_, options) {
// The intended way of appending options is returning a list of extra options,
// but this isn't used in widgetInputs.js and would require
// less generalization of chainCallback
let optNew = []
const previewWidget = this.widgets.find((w) => w.name === "videopreview");
let url = null
if (previewWidget.videoEl?.hidden == false && previewWidget.videoEl.src) {
//Use full quality video
url = api.apiURL('/view?' + new URLSearchParams(previewWidget.value.params));
//Workaround for 16bit png: Just do first frame
url = url.replace('%2503d', '001')
} else if (previewWidget.imgEl?.hidden == false && previewWidget.imgEl.src) {
url = previewWidget.imgEl.src;
url = new URL(url);
}
if (url) {
optNew.push(
{
content: "Open preview",
callback: () => {
window.open(url, "_blank")
},
},
{
content: "Save preview",
callback: () => {
const a = document.createElement("a");
a.href = url;
a.setAttribute("download", new URLSearchParams(previewWidget.value.params).get("filename"));
document.body.append(a);
a.click();
requestAnimationFrame(() => a.remove());
},
}
);
}
const PauseDesc = (previewWidget.value.paused ? "Resume" : "Pause") + " preview";
if (previewWidget.videoEl.hidden == false) {
optNew.push({
content: PauseDesc, callback: () => {
//animated images can't be paused and are more likely to cause performance issues.
//changing src to a single keyframe is possible,
//For now, the option is disabled if an animated image is being displayed
if (previewWidget.value.paused) {
previewWidget.videoEl?.play();
} else {
previewWidget.videoEl?.pause();
}
previewWidget.value.paused = !previewWidget.value.paused;
}
});
}
//TODO: Consider hiding elements if no video preview is available yet.
//It would reduce confusion at the cost of functionality
//(if a video preview lags the computer, the user should be able to hide in advance)
const visDesc = (previewWidget.value.hidden ? "Show" : "Hide") + " preview";
optNew.push({
content: visDesc, callback: () => {
if (!previewWidget.videoEl.hidden && !previewWidget.value.hidden) {
previewWidget.videoEl.pause();
} else if (previewWidget.value.hidden && !previewWidget.videoEl.hidden && !previewWidget.value.paused) {
previewWidget.videoEl.play();
}
previewWidget.value.hidden = !previewWidget.value.hidden;
previewWidget.parentEl.hidden = previewWidget.value.hidden;
fitHeight(this);
}
});
optNew.push({
content: "Sync preview", callback: () => {
//TODO: address case where videos have varying length
//Consider a system of sync groups which are opt-in?
for (let p of document.getElementsByClassName("vhs_preview")) {
for (let child of p.children) {
if (child.tagName == "VIDEO") {
child.currentTime = 0;
} else if (child.tagName == "IMG") {
child.src = child.src;
}
}
}
}
});
const muteDesc = (previewWidget.value.muted ? "Unmute" : "Mute") + " Preview"
optNew.push({
content: muteDesc, callback: () => {
previewWidget.value.muted = !previewWidget.value.muted
}
})
if (options.length > 0 && options[0] != null && optNew.length > 0) {
optNew.push(null);
}
options.unshift(...optNew);
});
}
function addLoadVideoCommon(nodeType, nodeData) {
addVideoPreview(nodeType);
addPreviewOptions(nodeType);
chainCallback(nodeType.prototype, "onNodeCreated", function () {
// const pathWidget = this.widgets.find((w) => w.name === "video");
const pathWidget = this.widgets.find((w) => w.name === "default_value");
//do first load
requestAnimationFrame(() => {
for (let w of [pathWidget]) {
w.callback(w.value, null, this);
}
});
});
}
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);
}
}