Compare commits

..
Author SHA1 Message Date
tiancheng cdc1e8a022 README and license 2024-10-17 10:52:53 +03:00
19 changed files with 165 additions and 3115 deletions
-4
View File
@@ -17,7 +17,6 @@ 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.
@@ -26,9 +25,6 @@ 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
@@ -1,195 +0,0 @@
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)",
}
+16 -105
View File
@@ -1,39 +1,11 @@
import folder_paths import folder_paths
import node_helpers from PIL import Image, ImageOps
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
@@ -45,23 +17,22 @@ class ShellAgentPluginInputImage:
"required": { "required": {
"input_name": ( "input_name": (
"STRING", "STRING",
{"multiline": False, "default": "input_image", "forceInput": False}, {"multiline": False, "default": "input_image"},
), ),
"default_value": ( "default_value": (
# "STRING", {"image_upload": True, "default": files[0] if len(files) else ""}, "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": "", "forceInput": False}, {"multiline": True, "default": ""},
), ),
} }
} }
RETURN_TYPES = ("IMAGE", "MASK") RETURN_TYPES = ("IMAGE",)
# RETURN_NAMES = ("image",) RETURN_NAMES = ("image",)
FUNCTION = "run" FUNCTION = "run"
@@ -73,69 +44,10 @@ class ShellAgentPluginInputImage:
"title": kwargs["input_name"], "title": kwargs["input_name"],
"type": "string", "type": "string",
"default": kwargs["default_value"], "default": kwargs["default_value"],
"description": kwargs.get("description", ""), "description": kwargs["description"],
"url_type": "image" "url_type": "image"
} }
return schema return schema
@classmethod
def VALIDATE_INPUTS(s, input_name, default_value, description=""):
image = default_value
if image.startswith("http"):
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):
return "Invalid image file: {}".format(image)
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()
@@ -144,9 +56,9 @@ class ShellAgentPluginInputImage:
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_path) print("Fetching image from url: ", image)
response = requests.get(image_path) response = requests.get(image)
image = safe_open_image(response.content) image = Image.open(BytesIO(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
@@ -158,14 +70,13 @@ 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 = node_helpers.pillow(Image.open, image_path) image = Image.open(image_path).convert("RGB")
return self.convert_image_mask(image) image = ImageOps.exif_transpose(image)
# image = ImageOps.exif_transpose(image) image = image.convert("RGB")
# image = image.convert("RGB") image = np.array(image).astype(np.float32) / 255.0
# image = np.array(image).astype(np.float32) / 255.0 image = torch.from_numpy(image)[None,]
# image = torch.from_numpy(image)[None,] return [image]
# return [image]
except Exception as e: except Exception as e:
raise e raise e
+6 -54
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.get("description", ""), "description": kwargs["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.get("description", ""), "description": kwargs["description"],
} }
if kwargs.get("choices", "") != "": if kwargs.get("choices", "") != "":
schema["enums"] = eval(kwargs["choices"]) schema["enums"] = eval(kwargs["choices"])
@@ -146,16 +146,12 @@ class ShellAgentPluginInputInteger:
"description": ( "description": (
"STRING", "STRING",
{"multiline": True, "default": ""}, {"multiline": True, "default": ""},
), )
"choices": (
"STRING",
{"multiline": False, "default": ""},
),
} }
} }
RETURN_TYPES = ("INT",) RETURN_TYPES = ("FLOAT",)
RETURN_NAMES = ("int",) RETURN_NAMES = ("float",)
FUNCTION = "run" FUNCTION = "run"
@@ -184,58 +180,14 @@ 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)",
} }
+3 -16
View File
@@ -4,7 +4,7 @@ import numpy as np
import torch import torch
import os import os
import uuid import uuid
from tqdm import tqdm import tqdm
# class ShellAgentPluginInputImage: # class ShellAgentPluginInputImage:
@@ -88,12 +88,8 @@ class ShellAgentPluginInputVideo:
{"multiline": False, "default": "input_video"}, {"multiline": False, "default": "input_video"},
), ),
"default_value": ( "default_value": (
sorted(files), "STRING", {"video_upload": True, "default": files[0] if len(files) else ""},
{ "video_upload": True }
), ),
# "default_value": (
# "STRING", {"video_upload": True, "default": files[0] if len(files) else ""},
# ),
}, },
"optional": { "optional": {
"description": ( "description": (
@@ -120,15 +116,6 @@ 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()
@@ -175,4 +162,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)"
} }
+5 -8
View File
@@ -75,15 +75,12 @@ class ShellAgentSaveVideoVHS:
return schema return schema
def save_video(self, filenames, **kwargs): def save_video(self, filenames, **kwargs):
status, output_files = filenames status, (preview_image, video_path) = 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, folder_paths.base_path) video_path = os.path.relpath(video_path)
results = {"ui": {"video": [video_path]}} results = {"ui": {"image": [preview_image], "video": [video_path]}}
print(results)
return results return results
-88
View File
@@ -1,88 +0,0 @@
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)",
}
+8 -68
View File
@@ -29,12 +29,9 @@ 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, inspect_repo_version from .dependency_checker import resolve_dependencies
from folder_paths import base_path as BASE_PATH
WORKFLOW_ROOT = "shellagent/comfy_workflow" WORKFLOW_ROOT = "shellagent/comfy_workflow"
@@ -48,14 +45,6 @@ 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
@@ -66,11 +55,9 @@ 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.get("class_type") node_class_type = node_info["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.startswith("custom_nodes.ComfyUI-ShellAgent-Plugin"): if hasattr(node_cls, "RELATIVE_PYTHON_MODULE") and node_cls.RELATIVE_PYTHON_MODULE == "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"
@@ -95,9 +82,6 @@ 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
@@ -138,10 +122,6 @@ 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())
@@ -157,7 +137,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, custom_dependencies) dependency_results = resolve_dependencies(prompt)
# 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)
@@ -172,56 +152,16 @@ 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"], "dependencies": dependency_results,
"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_detail": str(traceback.format_exc()), "message": str(traceback.print_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)
+73 -255
View File
@@ -3,50 +3,68 @@ 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")))
model_loaders_info = json.load(open(os.path.join(os.path.dirname(__file__), "model_loader_info.json"))) def handle_model_info(ckpt_path):
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": rel_save_path, "save_path": save_path,
"filename": filename, "filename": filename,
} }
json.dump(data, open(metadata_path, "w")) json.dump(data, open(metadata_path, "w"))
@@ -56,8 +74,8 @@ def handle_model_info(ckpt_path, filename, rel_save_path):
urls = [] urls = []
item = { item = {
"filename": windows_to_linux_path(filename), "filename": filename,
"save_path": windows_to_linux_path(rel_save_path), "save_path": save_path,
"urls": urls, "urls": urls,
} }
return model_id, item return model_id, item
@@ -70,17 +88,13 @@ 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 Exception: except subprocess.CalledProcessError:
return result return result
# Get the latest commit hash # Get the latest commit hash
@@ -89,7 +103,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 Exception: except subprocess.CalledProcessError:
return result return result
# Create and return the JSON result # Create and return the JSON result
@@ -100,253 +114,57 @@ 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
}
response = requests.post(url, headers=headers, json=data) def resolve_dependencies(prompt): # resolve custom nodes and models at the same time
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.get("class_type") node_class_type = node_info["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)
custom_node_path = os.path.join(BASE_PATH, node_cls.RELATIVE_PYTHON_MODULE.replace(".", "/")) if node_class_type in ComfyUIModelLoaders:
if node_cls.RELATIVE_PYTHON_MODULE[len("custom_nodes."):] in node_remote_skip_models: input_names, save_path = ComfyUIModelLoaders[node_class_type]
skip_model_check = True for input_name in input_names:
print(f"skip model check for {node_class_type}") ckpt_path = os.path.join("models", save_path, node_info["inputs"][input_name])
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()))
print("ckpt_paths:", ckpt_paths) ckpt_paths = list(set(ckpt_paths))
custom_nodes = list(set(custom_nodes)) custom_nodes = list(set(custom_nodes))
# step 0: comfyui version # step 0: comfyui version
repo_info = inspect_repo_version(BASE_PATH) comfyui_version = inspect_repo_version("./")
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(os.path.join(BASE_PATH, custom_node.replace(".", "/"))) repo_info = inspect_repo_version(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 = {}
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:
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 = { files_dict = {v[0]: {"filename": v[2], "urls": [v[1]]} for v in file_mapping_dict.values()}
v[0]: { dependencies = {
"filename": windows_to_linux_path(os.path.relpath(v[2], BASE_PATH)) if not v[3] else v[2], "models": models_dict,
"urls": [v[1]]} for v in file_mapping_dict.values()} "files": files_dict
}
depencencies = { results = {
"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
+13 -27
View File
@@ -3,9 +3,8 @@ 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.utils import compute_sha256, get_alphanumeric_hash from .utils import compute_sha256
ext_to_type = { ext_to_type = {
# image # image
@@ -27,7 +26,7 @@ ext_to_type = {
'.m4a': 'audio/mp4', '.m4a': 'audio/mp4',
} }
def upload_file_to_myshell(local_file: str, target_path: str, is_abs) -> str: def upload_file_to_myshell(local_file: str) -> 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")
@@ -46,13 +45,13 @@ def upload_file_to_myshell(local_file: str, target_path: str, is_abs) -> 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.lower()])), ('file', (os.path.basename(local_file), open(local_file, 'rb'), ext_to_type[ext])),
] ]
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}, will be saved to {target_path}") logging.info(f"{local_file} uploaded, time elapsed: {end_time - start_time}")
return [sha256sum, response.json()['url'], target_path, is_abs] return [sha256sum, response.json()['url'], local_file]
else: else:
raise Exception( raise Exception(
f"[HTTP ERROR] {response.status_code} - {response.text} \n" f"[HTTP ERROR] {response.status_code} - {response.text} \n"
@@ -60,31 +59,19 @@ def upload_file_to_myshell(local_file: str, target_path: str, is_abs) -> 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
is_abs = False if os.path.isfile(item):
if os.path.isfile(abspath): fpath = item
fpath = abspath elif os.path.isfile(f"input/{item}"):
is_abs = True fpath = f"input/{item}"
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.lower() in ext_to_type.keys(): if ext in ext_to_type.keys():
if is_abs: # if use abs path, replace it mapping_dict[item] = fpath
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
@@ -95,7 +82,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, source_path, target_path, is_abs): filename for filename, (source_path, target_path, is_abs) in mapping_dict.items()} futures = {executor.submit(upload_file_to_myshell, full_path): filename for filename, full_path 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):
@@ -104,8 +91,7 @@ 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:
del mapping_dict[filename] print(f"Error processing {filename}: {e}")
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
@@ -1,148 +0,0 @@
{
"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
@@ -1,5 +0,0 @@
{
"comfyui-ollama": {
"reason": "this node requires installing an extra software on linux, which is currently unsupported"
}
}
-45
View File
@@ -1,45 +0,0 @@
{
"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
@@ -1,3 +0,0 @@
[
"BizyAir"
]
-8
View File
@@ -1,8 +0,0 @@
aiofiles
pydantic
opencv-python
imageio-ffmpeg
brotli
pillow_heif
easydict
# logfire
+20
View File
@@ -0,0 +1,20 @@
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
@@ -1,35 +0,0 @@
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
+21 -854
View File
@@ -1,15 +1,11 @@
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 => {
@@ -20,852 +16,23 @@ 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" },
];
}
}
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);
}
}