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

|
||||
- 202401027: init
|
||||
|
||||
## Features
|
||||
- share workflows to easyai
|
||||
- LoadImagesMulti
|
||||
|
||||
## Installation
|
||||
### [method1] From Source
|
||||
@@ -21,8 +24,6 @@ If you have a comfy-cli, you can simply execute `comfy node registry-install com
|
||||
## Usage
|
||||

|
||||
- 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
|
||||
This project is licensed under the MIT License. See the LICENSE file for details.
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
WEB_DIRECTORY = "js"
|
||||
from .nodes import NODE_CLASS_MAPPINGS
|
||||
__all__ = ['NODE_CLASS_MAPPINGS']
|
||||
from .nodes import NODE_CLASS_MAPPINGS,NODE_DISPLAY_NAME_MAPPINGS
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
from aiohttp import ClientSession, web
|
||||
from server import PromptServer
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 196 KiB |
@@ -1,51 +1,126 @@
|
||||
import soundfile as sf
|
||||
import requests
|
||||
import io
|
||||
import os
|
||||
from PIL import Image, ImageOps, ImageSequence
|
||||
import numpy as np
|
||||
import torch
|
||||
import folder_paths
|
||||
import node_helpers
|
||||
import re
|
||||
|
||||
class AudioLoadPath:
|
||||
class LoadImagesMulti:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": { "path": ("STRING", {"default": "X://insert/path/here.mp4"}),
|
||||
"sample_rate": ("INT", {"default": 22050, "min": 6000, "max": 192000, "step": 1}),
|
||||
"offset": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1e6, "step": 0.001}),
|
||||
"duration": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1e6, "step": 0.001})}}
|
||||
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_TYPES = ("AUDIO", )
|
||||
CATEGORY = "Audio Reactor"
|
||||
FUNCTION = "load"
|
||||
return {
|
||||
"required": {
|
||||
"filenames": ("STRING", {
|
||||
"default": "filename1.png\nfilename2.png",
|
||||
"tooltip": "输入多个文件名,用逗号或者换行分隔,例如: 1.png, 2.jpg, dir/sub.png",
|
||||
"multiline": True # 多行文本域
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
def load(self, path: str, sample_rate: int, offset: float, duration: float|None):
|
||||
if duration == 0.0:
|
||||
duration = None
|
||||
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"
|
||||
|
||||
if path.startswith(('http://', 'https://')):
|
||||
# 对于网络路径,直接从内存加载
|
||||
try:
|
||||
response = requests.get(path)
|
||||
response.raise_for_status()
|
||||
audio_data = io.BytesIO(response.content)
|
||||
def load_images(self, filenames):
|
||||
# 解析用户输入的多个文件名
|
||||
|
||||
# 使用 soundfile 从内存中读取音频数据
|
||||
audio, file_sr = sf.read(audio_data)
|
||||
filenames = re.split(r'[, \r\n]+', filenames) # 按逗号、空格或任何换行符分割
|
||||
filenames = [f.strip() for f in filenames if f.strip()] # 去掉首尾空格和空字符串
|
||||
|
||||
# 如果需要重采样
|
||||
if file_sr != sample_rate:
|
||||
# 这里需要添加重采样逻辑
|
||||
# 可以使用 librosa.resample 或其他方法
|
||||
pass
|
||||
if len(filenames) == 0:
|
||||
raise ValueError("未提供有效的文件名,请至少输入一个文件名。")
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"加载网络音频失败: {str(e)}")
|
||||
else:
|
||||
# 本地文件使用原有的 librosa 方式加载
|
||||
audio, _ = librosa.load(path, sr=sample_rate, offset=offset, duration=duration)
|
||||
output_images = []
|
||||
output_masks = []
|
||||
output_paths = []
|
||||
|
||||
# 转换为 torch tensor 并调整维度
|
||||
audio = torch.from_numpy(audio)[None,:,None]
|
||||
return (audio,)
|
||||
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 = {
|
||||
"AudioLoadPath": AudioLoadPath,
|
||||
"LoadImagesMulti": LoadImagesMulti
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LoadImagesMulti": "Load Images(input filenames)"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
numpy
|
||||
librosa
|
||||
Reference in New Issue
Block a user