Compare commits
19
Commits
99e60fecb7
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dd0f05145 | ||
|
|
7358ddc2f9 | ||
|
|
e9f43126a9 | ||
|
|
4b891f103c | ||
|
|
405607f616 | ||
|
|
761f1bb03b | ||
|
|
7bfaa76190 | ||
|
|
f93b880d3f | ||
|
|
cbe1fc951d | ||
|
|
89dbe3b916 | ||
|
|
e004083989 | ||
|
|
a69e6a5017 | ||
|
|
77976deb2c | ||
|
|
f688712e27 | ||
|
|
023511075c | ||
|
|
4cd6809cea | ||
|
|
b46856148c | ||
|
|
014f3369c4 | ||
|
|
a08e57ada4 |
@@ -2,10 +2,13 @@
|
|||||||
ComfyUI-Easyai is a powerful extension for ComfyUI that enables users to share workflows and models to easyai.
|
ComfyUI-Easyai is a powerful extension for ComfyUI that enables users to share workflows and models to easyai.
|
||||||
|
|
||||||
## ChangeLog
|
## ChangeLog
|
||||||
|
- 20251102: add LoadImagesMulti,使用EasyAI平台多图上传(单任务)节点与该组件对接,可实现批量任务或者任意数量的多图上传和快速选择
|
||||||
|

|
||||||
- 202401027: init
|
- 202401027: init
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- share workflows to easyai
|
- share workflows to easyai
|
||||||
|
- LoadImagesMulti
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
### [method1] From Source
|
### [method1] From Source
|
||||||
@@ -19,9 +22,8 @@ If you have ComfyUI-Manager, you can simply search "Easyai for ComfyUI" from `Cu
|
|||||||
If you have a comfy-cli, you can simply execute `comfy node registry-install comfyui-easyai` in command line.
|
If you have a comfy-cli, you can simply execute `comfy node registry-install comfyui-easyai` in command line.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|

|
||||||
- After installation, you can use the Easyai nodes in your ComfyUI workflows.
|
- After installation, you can use the Easyai nodes in your ComfyUI workflows.
|
||||||
- For more detailed usage, please refer to the [金华岩石](https://jinhuayanshi.cn) or [三景AI](https://easyai.jinhuayanshi.cn) websites.
|
|
||||||
- Follow me: <img src="./docs/douyin.jpg" width="200" />
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
This project is licensed under the MIT License. See the LICENSE file for details.
|
This project is licensed under the MIT License. See the LICENSE file for details.
|
||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
WEB_DIRECTORY = "js"
|
WEB_DIRECTORY = "js"
|
||||||
NODE_CLASS_MAPPINGS = {}
|
from .nodes import NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS
|
||||||
__all__ = ['NODE_CLASS_MAPPINGS']
|
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||||
|
|
||||||
from aiohttp import ClientSession, web
|
from aiohttp import ClientSession, web
|
||||||
from server import PromptServer
|
from server import PromptServer
|
||||||
@@ -9,7 +9,7 @@ from server import PromptServer
|
|||||||
async def upload_workflow(request):
|
async def upload_workflow(request):
|
||||||
try:
|
try:
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
url = data["domain"] + "/api/v1/openapi/upload/workflow"
|
url = data["domain"] + "/v1/openapi/upload/workflow"
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-comfy-api-key': data["apiKey"]
|
'x-comfy-api-key': data["apiKey"]
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.9 MiB |
+24
-12
@@ -6,17 +6,8 @@ app.registerExtension({
|
|||||||
name: "ComfyUI-Easyai",
|
name: "ComfyUI-Easyai",
|
||||||
async setup() {
|
async setup() {
|
||||||
const menu = document.querySelector(".comfy-menu");
|
const menu = document.querySelector(".comfy-menu");
|
||||||
const separator = document.createElement("hr");
|
|
||||||
|
|
||||||
separator.style.margin = "20px 0";
|
const upload = () => {
|
||||||
separator.style.width = "100%";
|
|
||||||
menu.append(separator);
|
|
||||||
|
|
||||||
const uploadEasyaiButton = document.createElement("button");
|
|
||||||
uploadEasyaiButton.id = "uploadEasyaiButton";
|
|
||||||
uploadEasyaiButton.textContent = "上传Easyai";
|
|
||||||
// 点击按钮时弹出对话框
|
|
||||||
uploadEasyaiButton.onclick = () => {
|
|
||||||
// 创建遮罩层
|
// 创建遮罩层
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.style.position = 'fixed';
|
overlay.style.position = 'fixed';
|
||||||
@@ -50,7 +41,7 @@ app.registerExtension({
|
|||||||
// 添加域名输入框标签
|
// 添加域名输入框标签
|
||||||
const domainLabel = document.createElement('label');
|
const domainLabel = document.createElement('label');
|
||||||
domainLabel.setAttribute('for', 'domainInput');
|
domainLabel.setAttribute('for', 'domainInput');
|
||||||
domainLabel.innerText = 'Domain (域名):';
|
domainLabel.innerText = 'BaseURL (接口地址):';
|
||||||
domainLabel.style.color = 'white'; // 白色标签文字
|
domainLabel.style.color = 'white'; // 白色标签文字
|
||||||
dialog.appendChild(domainLabel);
|
dialog.appendChild(domainLabel);
|
||||||
|
|
||||||
@@ -165,7 +156,7 @@ app.registerExtension({
|
|||||||
localStorage.setItem('easyai-x-comfy-api-key', apiKey);
|
localStorage.setItem('easyai-x-comfy-api-key', apiKey);
|
||||||
localStorage.setItem('easyai-domain', domain);
|
localStorage.setItem('easyai-domain', domain);
|
||||||
app.graphToPrompt().then((p2) => {
|
app.graphToPrompt().then((p2) => {
|
||||||
const workflow = JSON.stringify(p2.output);
|
const workflow = JSON.stringify(p2.output, null, 2);
|
||||||
api.fetchApi('/easyai/upload_workflow', {
|
api.fetchApi('/easyai/upload_workflow', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -209,6 +200,27 @@ app.registerExtension({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
let cmGroup = new (await import("../../scripts/ui/components/buttonGroup.js")).ComfyButtonGroup(
|
||||||
|
new(await import("../../scripts/ui/components/button.js")).ComfyButton({
|
||||||
|
icon: "upload",
|
||||||
|
action: upload,
|
||||||
|
tooltip: "上传Easyai"
|
||||||
|
}).element
|
||||||
|
);
|
||||||
|
|
||||||
|
app.menu?.settingsGroup.element.after(cmGroup.element);
|
||||||
|
}
|
||||||
|
catch(exception) {
|
||||||
|
console.log('ComfyUI is outdated. New style menu based features are disabled.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadEasyaiButton = document.createElement("button");
|
||||||
|
uploadEasyaiButton.id = "uploadEasyaiButton";
|
||||||
|
uploadEasyaiButton.textContent = "上传Easyai";
|
||||||
|
// 点击按钮时弹出对话框
|
||||||
|
uploadEasyaiButton.onclick = upload;
|
||||||
|
|
||||||
uploadEasyaiButton.style.background = "linear-gradient(135deg, #8A00FF 0%, #00FFC6 100%)";
|
uploadEasyaiButton.style.background = "linear-gradient(135deg, #8A00FF 0%, #00FFC6 100%)";
|
||||||
uploadEasyaiButton.style.color = "black";
|
uploadEasyaiButton.style.color = "black";
|
||||||
uploadEasyaiButton.style.display = "inline-block";
|
uploadEasyaiButton.style.display = "inline-block";
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import os
|
||||||
|
from PIL import Image, ImageOps, ImageSequence
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import folder_paths
|
||||||
|
import node_helpers
|
||||||
|
import re
|
||||||
|
|
||||||
|
class LoadImagesMulti:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
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 = folder_paths.filter_files_content_types(files, ["image"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"filenames": ("STRING", {
|
||||||
|
"default": "filename1.png\nfilename2.png",
|
||||||
|
"tooltip": "输入多个文件名,用逗号或者换行分隔,例如: 1.png, 2.jpg, dir/sub.png",
|
||||||
|
"multiline": True # 多行文本域
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CATEGORY = "EasyAI"
|
||||||
|
RETURN_TYPES = ("IMAGE", "MASK", "STRING",
|
||||||
|
"IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE")
|
||||||
|
RETURN_NAMES = ("images", "masks", "filepaths",
|
||||||
|
"image1", "image2", "image3", "image4", "image5", "image6")
|
||||||
|
INPUT_IS_LIST = False
|
||||||
|
OUTPUT_IS_LIST = (True,True,False,
|
||||||
|
False,False,False,False,False,False)
|
||||||
|
FUNCTION = "load_images"
|
||||||
|
|
||||||
|
def load_images(self, filenames):
|
||||||
|
# 解析用户输入的多个文件名
|
||||||
|
|
||||||
|
filenames = re.split(r'[, \r\n]+', filenames) # 按逗号、空格或任何换行符分割
|
||||||
|
filenames = [f.strip() for f in filenames if f.strip()] # 去掉首尾空格和空字符串
|
||||||
|
|
||||||
|
if len(filenames) == 0:
|
||||||
|
raise ValueError("未提供有效的文件名,请至少输入一个文件名。")
|
||||||
|
|
||||||
|
output_images = []
|
||||||
|
output_masks = []
|
||||||
|
output_paths = []
|
||||||
|
|
||||||
|
excluded_formats = ["MPO"]
|
||||||
|
|
||||||
|
for fname in filenames:
|
||||||
|
# 支持子目录,如 "sub/my.png"
|
||||||
|
img_path = folder_paths.get_annotated_filepath(fname)
|
||||||
|
|
||||||
|
if not folder_paths.exists_annotated_filepath(fname):
|
||||||
|
raise FileNotFoundError(f"文件不存在: {fname}")
|
||||||
|
|
||||||
|
img = node_helpers.pillow(Image.open, img_path)
|
||||||
|
|
||||||
|
# frames_img = []
|
||||||
|
# frames_mask = []
|
||||||
|
|
||||||
|
w, h = None, None
|
||||||
|
|
||||||
|
for i in ImageSequence.Iterator(img):
|
||||||
|
i = node_helpers.pillow(ImageOps.exif_transpose, i)
|
||||||
|
|
||||||
|
if i.mode == "I":
|
||||||
|
i = i.point(lambda x: x * (1 / 255))
|
||||||
|
|
||||||
|
rgb = i.convert("RGB")
|
||||||
|
|
||||||
|
# 统一尺寸
|
||||||
|
if w is None:
|
||||||
|
w, h = rgb.size
|
||||||
|
elif rgb.size != (w, h):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 转 tensor
|
||||||
|
rgb_tensor = torch.from_numpy(
|
||||||
|
np.array(rgb).astype(np.float32) / 255.0
|
||||||
|
)[None,]
|
||||||
|
|
||||||
|
# Mask
|
||||||
|
if "A" in i.getbands():
|
||||||
|
alpha = i.getchannel("A")
|
||||||
|
mask_np = np.array(alpha).astype(np.float32) / 255.0
|
||||||
|
mask_tensor = 1. - torch.from_numpy(mask_np)
|
||||||
|
elif i.mode == "P" and "transparency" in i.info:
|
||||||
|
alpha = i.convert("RGBA").getchannel("A")
|
||||||
|
mask_np = np.array(alpha).astype(np.float32) / 255.0
|
||||||
|
mask_tensor = 1. - torch.from_numpy(mask_np)
|
||||||
|
else:
|
||||||
|
mask_tensor = torch.zeros((64, 64), dtype=torch.float32)
|
||||||
|
|
||||||
|
output_images.append(rgb_tensor)
|
||||||
|
output_masks.append(mask_tensor.unsqueeze(0))
|
||||||
|
|
||||||
|
# if len(frames_img) > 1 and img.format not in excluded_formats:
|
||||||
|
# image_tensor = torch.cat(frames_img, dim=0)
|
||||||
|
# mask_tensor = torch.cat(frames_mask, dim=0)
|
||||||
|
# else:
|
||||||
|
# image_tensor = frames_img[0]
|
||||||
|
# mask_tensor = frames_mask[0]
|
||||||
|
|
||||||
|
# output_images.append(frames_img)
|
||||||
|
# output_masks.append(frames_mask)
|
||||||
|
output_paths.append(img_path)
|
||||||
|
|
||||||
|
# 合并为 batch(N, H, W, C)
|
||||||
|
# batch_images = output_images # 保持 list,每个元素是不同尺寸的 tensor
|
||||||
|
# batch_masks = output_masks
|
||||||
|
# 前6张单图输出(如果不够就用None占位)
|
||||||
|
single_images = [output_images[i] if i < len(output_images) else None for i in range(6)]
|
||||||
|
|
||||||
|
return (output_images, output_masks, "\n".join(output_paths),
|
||||||
|
*single_images)
|
||||||
|
|
||||||
|
# 节点导出
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"LoadImagesMulti": LoadImagesMulti
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"LoadImagesMulti": "Load Images(input filenames)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
numpy
|
||||||
|
librosa
|
||||||
Reference in New Issue
Block a user