input output nodes v1

This commit is contained in:
wl-zhao
2024-09-25 09:49:23 +08:00
parent 6627185e28
commit 89e275252d
7 changed files with 548 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
__pycache__
Executable
+54
View File
@@ -0,0 +1,54 @@
"""
@author: MyShell
@title: comfyui-shellagent-plugin
@description:
"""
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__)))
import inspect
import sys
import importlib
import subprocess
import requests
import folder_paths
from folder_paths import add_model_folder_path, get_filename_list, get_folder_paths
from tqdm import tqdm
# from . import custom_routes
# import routes
ag_path = os.path.join(os.path.dirname(__file__))
def get_python_files(path):
return [f[:-3] for f in os.listdir(path) if f.endswith(".py")]
def append_to_sys_path(path):
if path not in sys.path:
sys.path.append(path)
paths = ["comfy-nodes"]
files = []
for path in paths:
full_path = os.path.join(ag_path, path)
append_to_sys_path(full_path)
files.extend(get_python_files(full_path))
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
# Import all the modules and append their mappings
for file in files:
module = importlib.import_module(file)
if hasattr(module, "NODE_CLASS_MAPPINGS"):
NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS"):
NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
WEB_DIRECTORY = "web-plugin"
print(NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS)
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+151
View File
@@ -0,0 +1,151 @@
import folder_paths
from PIL import Image, ImageOps
import numpy as np
import torch
import os
import uuid
import tqdm
class ShellAgentPluginInputImage:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
files = sorted(files)
return {
"required": {
"input_name": (
"STRING",
{"multiline": False, "default": "input_image"},
),
"default_value": (
"STRING", {"image_upload": True, "default": files[0] if len(files) else ""},
),
},
"optional": {
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "run"
CATEGORY = "shellagent"
def run(self, input_name, default_value=None, display_name=None, description=None):
input_dir = folder_paths.get_input_directory()
image_path = default_value
try:
if image_path.startswith('http'):
import requests
from io import BytesIO
print("Fetching image from url: ", image)
response = requests.get(image)
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,'):
import base64
from io import BytesIO
print("Decoding base64 image")
base64_image = image_path[image_path.find(",")+1:]
decoded_image = base64.b64decode(base64_image)
image = Image.open(BytesIO(decoded_image))
else:
# local path
image_path = os.path.join(input_dir, image_path)
image = Image.open(image_path).convert("RGB")
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
video_extensions = ["webm", "mp4", "mkv", "gif"]
class ShellAgentPluginInputVideo:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = []
for f in os.listdir(input_dir):
if os.path.isfile(os.path.join(input_dir, f)):
file_parts = f.split(".")
if len(file_parts) > 1 and (file_parts[-1] in video_extensions):
files.append(f)
return {
"required": {
"input_name": (
"STRING",
{"multiline": False, "default": "input_video"},
),
"default_value": (
"STRING", {"video_upload": True, "default": files[0] if len(files) else ""},
),
},
"optional": {
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video",)
FUNCTION = "run"
CATEGORY = "shellagent"
def run(self, input_name, default_value=None, description=None):
input_dir = folder_paths.get_input_directory()
if default_value.startswith("http"):
import requests
print("Fetching video from URL: ", default_value)
response = requests.get(default_value, stream=True)
file_size = int(response.headers.get("Content-Length", 0))
file_extension = default_value.split(".")[-1].split("?")[
0
] # Extract extension and handle URLs with parameters
if file_extension not in video_extensions:
file_extension = ".mp4"
unique_filename = str(uuid.uuid4()) + "." + file_extension
video_path = os.path.join(input_dir, unique_filename)
chunk_size = 1024 # 1 Kibibyte
num_bars = int(file_size / chunk_size)
with open(video_path, "wb") as out_file:
for chunk in tqdm(
response.iter_content(chunk_size=chunk_size),
total=num_bars,
unit="KB",
desc="Downloading",
leave=True,
):
out_file.write(chunk)
else:
video_path = os.path.abspath(os.path.join(input_dir, default_value))
return (video_path,)
NODE_CLASS_MAPPINGS = {
"ShellAgentPluginInputImage": ShellAgentPluginInputImage,
# "ShellAgentPluginInputVideo": ShellAgentPluginInputVideo,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ShellAgentPluginInputImage": "Input Image (ShellAgent Plugin)",
# "ShellAgentPluginInputVideo": "Input Video (ShellAgent Plugin)"
}
+44
View File
@@ -0,0 +1,44 @@
import folder_paths
from PIL import Image, ImageOps
import numpy as np
import torch
class ShellAgentPluginInputText:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_name": (
"STRING",
{"multiline": False, "default": "input_text"},
),
},
"optional": {
"default_value": (
"STRING",
{"multiline": True, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
"choices": (
"STRING",
{"multiline": False, "default": ""},
),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "run"
CATEGORY = "shellagent"
def run(self, input_name, default_value=None, display_name=None, description=None, choices=None):
return [default_value]
NODE_CLASS_MAPPINGS = {"ShellAgentPluginInputText": ShellAgentPluginInputText}
NODE_DISPLAY_NAME_MAPPINGS = {"ShellAgentPluginInputText": "Input Text (ShellAgent Plugin)"}
+151
View File
@@ -0,0 +1,151 @@
import folder_paths
from PIL import Image, ImageOps
import numpy as np
import torch
import os
import uuid
import tqdm
class ShellAgentPluginInputImage:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
files = sorted(files)
return {
"required": {
"input_name": (
"STRING",
{"multiline": False, "default": "input_image"},
),
"default_value": (
"STRING", {"image_upload": True, "default": files[0] if len(files) else ""},
),
},
"optional": {
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "run"
CATEGORY = "shellagent"
def run(self, input_name, default_value=None, display_name=None, description=None):
input_dir = folder_paths.get_input_directory()
image_path = default_value
try:
if image_path.startswith('http'):
import requests
from io import BytesIO
print("Fetching image from url: ", image)
response = requests.get(image)
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,'):
import base64
from io import BytesIO
print("Decoding base64 image")
base64_image = image_path[image_path.find(",")+1:]
decoded_image = base64.b64decode(base64_image)
image = Image.open(BytesIO(decoded_image))
else:
# local path
image_path = os.path.join(input_dir, image_path)
image = Image.open(image_path).convert("RGB")
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
video_extensions = ["webm", "mp4", "mkv", "gif"]
class ShellAgentPluginInputVideo:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = []
for f in os.listdir(input_dir):
if os.path.isfile(os.path.join(input_dir, f)):
file_parts = f.split(".")
if len(file_parts) > 1 and (file_parts[-1] in video_extensions):
files.append(f)
return {
"required": {
"input_name": (
"STRING",
{"multiline": False, "default": "input_video"},
),
"default_value": (
"STRING", {"video_upload": True, "default": files[0] if len(files) else ""},
),
},
"optional": {
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video",)
FUNCTION = "run"
CATEGORY = "shellagent"
def run(self, input_name, default_value=None, description=None):
input_dir = folder_paths.get_input_directory()
if default_value.startswith("http"):
import requests
print("Fetching video from URL: ", default_value)
response = requests.get(default_value, stream=True)
file_size = int(response.headers.get("Content-Length", 0))
file_extension = default_value.split(".")[-1].split("?")[
0
] # Extract extension and handle URLs with parameters
if file_extension not in video_extensions:
file_extension = ".mp4"
unique_filename = str(uuid.uuid4()) + "." + file_extension
video_path = os.path.join(input_dir, unique_filename)
chunk_size = 1024 # 1 Kibibyte
num_bars = int(file_size / chunk_size)
with open(video_path, "wb") as out_file:
for chunk in tqdm(
response.iter_content(chunk_size=chunk_size),
total=num_bars,
unit="KB",
desc="Downloading",
leave=True,
):
out_file.write(chunk)
else:
video_path = os.path.abspath(os.path.join(input_dir, default_value))
return (video_path,)
NODE_CLASS_MAPPINGS = {
# "ShellAgentPluginInputImage": ShellAgentPluginInputImage,
"ShellAgentPluginInputVideo": ShellAgentPluginInputVideo,
}
NODE_DISPLAY_NAME_MAPPINGS = {
# "ShellAgentPluginInputImage": "Input Image (ShellAgent Plugin)",
"ShellAgentPluginInputVideo": "Input Video (ShellAgent Plugin)"
}
+29
View File
@@ -0,0 +1,29 @@
import folder_paths
from nodes import SaveImage
class ShellAgentSaveImage(SaveImage):
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE", {"tooltip": "The images 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"
},
}
def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None, **extra_kwargs):
results = super().save_images(images, filename_prefix, prompt, extra_pnginfo)
results["shellagent_kwargs"] = extra_kwargs
return results
NODE_CLASS_MAPPINGS = {
"ShellAgentPluginSaveImage": ShellAgentSaveImage,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ShellAgentPluginSaveImage": "Save Image (ShellAgent Plugin)"
}
Executable
+118
View File
@@ -0,0 +1,118 @@
import struct
from enum import Enum
import aiohttp
from typing import List, Union, Any, Optional
from PIL import Image, ImageOps
from io import BytesIO
from pydantic import BaseModel as PydanticBaseModel
class BaseModel(PydanticBaseModel):
class Config:
arbitrary_types_allowed = True
class Status(Enum):
NOT_STARTED = "not-started"
RUNNING = "running"
SUCCESS = "success"
FAILED = "failed"
UPLOADING = "uploading"
class StreamingPrompt(BaseModel):
workflow_api: Any
auth_token: str
inputs: dict[str, Union[str, bytes, Image.Image]]
running_prompt_ids: set[str] = set()
status_endpoint: Optional[str]
file_upload_endpoint: Optional[str]
class SimplePrompt(BaseModel):
status_endpoint: Optional[str]
file_upload_endpoint: Optional[str]
token: Optional[str]
workflow_api: dict
status: Status = Status.NOT_STARTED
progress: set = set()
last_updated_node: Optional[str] = None,
uploading_nodes: set = set()
done: bool = False
is_realtime: bool = False,
start_time: Optional[float] = None,
sockets = dict()
prompt_metadata: dict[str, SimplePrompt] = {}
streaming_prompt_metadata: dict[str, StreamingPrompt] = {}
class BinaryEventTypes:
PREVIEW_IMAGE = 1
UNENCODED_PREVIEW_IMAGE = 2
max_output_id_length = 24
async def send_image(image_data, sid=None, output_id:str = None):
max_length = max_output_id_length
output_id = output_id[:max_length]
padded_output_id = output_id.ljust(max_length, '\x00')
encoded_output_id = padded_output_id.encode('ascii', 'replace')
image_type = image_data[0]
image = image_data[1]
max_size = image_data[2]
quality = image_data[3]
if max_size is not None:
if hasattr(Image, 'Resampling'):
resampling = Image.Resampling.BILINEAR
else:
resampling = Image.ANTIALIAS
image = ImageOps.contain(image, (max_size, max_size), resampling)
type_num = 1
if image_type == "JPEG":
type_num = 1
elif image_type == "PNG":
type_num = 2
elif image_type == "WEBP":
type_num = 3
bytesIO = BytesIO()
header = struct.pack(">I", type_num)
# 4 bytes for the type
bytesIO.write(header)
# 10 bytes for the output_id
position_before = bytesIO.tell()
bytesIO.write(encoded_output_id)
position_after = bytesIO.tell()
bytes_written = position_after - position_before
print(f"Bytes written: {bytes_written}")
image.save(bytesIO, format=image_type, quality=quality, compress_level=1)
preview_bytes = bytesIO.getvalue()
await send_bytes(BinaryEventTypes.PREVIEW_IMAGE, preview_bytes, sid=sid)
async def send_socket_catch_exception(function, message):
try:
await function(message)
except (aiohttp.ClientError, aiohttp.ClientPayloadError, ConnectionResetError) as err:
print("send error:", err)
def encode_bytes(event, data):
if not isinstance(event, int):
raise RuntimeError(f"Binary event types must be integers, got {event}")
packed = struct.pack(">I", event)
message = bytearray(packed)
message.extend(data)
return message
async def send_bytes(event, data, sid=None):
message = encode_bytes(event, data)
print("sending image to ", event, sid)
if sid is None:
_sockets = list(sockets.values())
for ws in _sockets:
await send_socket_catch_exception(ws.send_bytes, message)
elif sid in sockets:
await send_socket_catch_exception(sockets[sid].send_bytes, message)