将自定义节点放置到custom目录下
This commit is contained in:
57
__init__.py
57
__init__.py
@@ -1,17 +1,31 @@
|
||||
from .nodes.image import SaveImagePath
|
||||
from .nodes.heygem import HeyGemF2F, HeyGemF2FFromFile
|
||||
from .nodes.s3 import S3Download, S3Upload, S3UploadURL
|
||||
from .nodes.text import *
|
||||
from .nodes.traverse_folder import TraverseFolder
|
||||
from .nodes.unload_all_models import UnloadAllModels
|
||||
from .nodes.string_empty_judgement import StringEmptyJudgement
|
||||
from .nodes.compute_video_point import VideoStartPointDurationCompute
|
||||
from .nodes.cos import COSUpload, COSDownload
|
||||
from .nodes.face_detect import FaceDetect
|
||||
from .nodes.face_extract import FaceExtract
|
||||
from .nodes.heygem import HeyGemF2F, HeyGemF2FFromFile
|
||||
from .nodes.image import SaveImagePath
|
||||
from .nodes.load_image_pro import LoadNetImg
|
||||
from .nodes.log2db import LogToDB
|
||||
from .nodes.nodes_bfl import (
|
||||
FluxProUltraImageNode,
|
||||
FluxKontextProImageNode,
|
||||
FluxKontextMaxImageNode,
|
||||
FluxProExpandNode,
|
||||
FluxProFillNode,
|
||||
FluxProCannyNode,
|
||||
FluxProDepthNode
|
||||
|
||||
)
|
||||
from .nodes.s3 import S3Download, S3Upload, S3UploadURL
|
||||
from .nodes.string_empty_judgement import StringEmptyJudgement
|
||||
from .nodes.text import *
|
||||
from .nodes.traverse_folder import TraverseFolder
|
||||
from .nodes.unload_all_models import UnloadAllModels
|
||||
from .nodes.video import VideoCut, VideoCutByFramePoint, VideoChangeFPS
|
||||
from .nodes.vod2local import VodToLocalNode
|
||||
from .nodes.prompt_id_generator import TaskIdGenerate
|
||||
from .nodes.random_line import RandomLineSelector
|
||||
from .nodes.webhook_forward import PlugAndPlayWebhook, SaveImageWithOutput
|
||||
|
||||
# A dictionary that contains all nodes you want to export with their names
|
||||
# NOTE: names should be globally unique
|
||||
@@ -37,6 +51,19 @@ NODE_CLASS_MAPPINGS = {
|
||||
"HeyGemF2F": HeyGemF2F,
|
||||
"HeyGemF2FFromFile": HeyGemF2FFromFile,
|
||||
"SaveImagePath": SaveImagePath,
|
||||
"LoadNetImg": LoadNetImg,
|
||||
"FluxProUltraImageNode": FluxProUltraImageNode,
|
||||
# "FluxProImageNode": FluxProImageNode,
|
||||
"FluxKontextProImageNode": FluxKontextProImageNode,
|
||||
"FluxKontextMaxImageNode": FluxKontextMaxImageNode,
|
||||
"FluxProExpandNode": FluxProExpandNode,
|
||||
"FluxProFillNode": FluxProFillNode,
|
||||
"FluxProCannyNode": FluxProCannyNode,
|
||||
"FluxProDepthNode": FluxProDepthNode,
|
||||
"TaskIdGenerate": TaskIdGenerate,
|
||||
"RandomLineSelector": RandomLineSelector,
|
||||
"PlugAndPlayWebhook": PlugAndPlayWebhook,
|
||||
"SaveImageWithOutput": SaveImageWithOutput
|
||||
}
|
||||
|
||||
# A dictionary that contains the friendly/humanly readable titles for the nodes
|
||||
@@ -61,5 +88,19 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LoadTextCustomOnline": "读取文本文件(线上)",
|
||||
"HeyGemF2F": "HeyGem口型同步(API, 传入文件Tensor)",
|
||||
"HeyGemF2FFromFile": "HeyGem口型同步(API, 传入文件路径)",
|
||||
"SaveImagePath": "保存图片"
|
||||
"SaveImagePath": "保存图片",
|
||||
"LoadNetImg": "load_net_image",
|
||||
"FluxProUltraImageNode": "Flux 1.1 [pro] Ultra Image",
|
||||
# "FluxProImageNode": "Flux 1.1 [pro] Image",
|
||||
"FluxKontextProImageNode": "Flux.1 Kontext [pro] Image",
|
||||
"FluxKontextMaxImageNode": "Flux.1 Kontext [max] Image",
|
||||
"FluxProExpandNode": "Flux.1 Expand Image",
|
||||
"FluxProFillNode": "Flux.1 Fill Image",
|
||||
"FluxProCannyNode": "Flux.1 Canny Control Image",
|
||||
"FluxProDepthNode": "Flux.1 Depth Control Image",
|
||||
"TaskIdGenerate": "TaskID生成器",
|
||||
"RandomLineSelector": "随机选择一行内容",
|
||||
"PlugAndPlayWebhook": "Webhook转发器",
|
||||
"SaveImageWithOutput": "保存图片(带输出)"
|
||||
|
||||
}
|
||||
|
||||
47
comfyui_v2.py
Normal file
47
comfyui_v2.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# 文件名 comfyui_v2.py
|
||||
import subprocess
|
||||
|
||||
import modal
|
||||
|
||||
image = ( # build up a Modal Image to run ComfyUI, step by step
|
||||
modal.Image.debian_slim( # start from basic Linux with Python
|
||||
python_version="3.10"
|
||||
)
|
||||
.apt_install("git", "gcc", "libportaudio2", "ffmpeg")
|
||||
.pip_install("fastapi[standard]==0.115.4") # install web dependencies
|
||||
.pip_install("comfy_cli==0.0.0",
|
||||
index_url="https://packages-1747622887395:0ee15474ccd7b27b57ca63a9306327678e6c2631@g-ldyi2063-pypi.pkg.coding.net/dev/packages/simple")
|
||||
.run_commands( # use comfy-cli to install ComfyUI and its dependencies
|
||||
"comfy --skip-prompt install --fast-deps --nvidia --version 0.3.40"
|
||||
)
|
||||
.pip_install_from_pyproject("./pyproject_comfyui.toml")
|
||||
.run_commands("comfy node install https://e.coding.net/g-ldyi2063/dev/ComfyUI-CustomNode.git", force_build=True)
|
||||
.run_commands("comfy node install https://github.com/yolain/ComfyUI-Easy-Use.git", force_build=True)
|
||||
# .add_local_file("ext/webhook_forward.py", "/root/comfy/ComfyUI/custom_nodes/webhook_forward.py", copy=True)
|
||||
# .add_local_file("ext/prompt_id_generator.py", "/root/comfy/ComfyUI/custom_nodes/prompt_id_generator.py", copy=True)
|
||||
# .add_local_file("ext/load_image_pro.py", "/root/comfy/ComfyUI/custom_nodes/load_image_pro.py", copy=True)
|
||||
# .add_local_file('ext/image.py', '/root/comfy/ComfyUI/custom_nodes/image.py', copy=True)
|
||||
# .add_local_file('ext/nodes_bfl.py', '/root/comfy/ComfyUI/comfy_api_nodes/nodes_bfl.py', copy=True)
|
||||
# # .add_local_file('ext/s3_utils.py', '/root/comfy/ComfyUI/custom_nodes/s3_utils.py', copy=True)
|
||||
# .add_local_file('ext/cos_utils.py', '/root/comfy/ComfyUI/custom_nodes/cos_utils.py', copy=True)
|
||||
# .add_local_file('ext/random_line.py', '/root/comfy/ComfyUI/custom_nodes/random_line.py', copy=True)
|
||||
)
|
||||
app = modal.App(name="test", image=image)
|
||||
vol = modal.Volume.from_name("test", environment_name="dev", create_if_missing=True)
|
||||
custom_secret = modal.Secret.from_name("comfyui-custom-secret", environment_name="dev")
|
||||
|
||||
|
||||
# modal deploy .\comfyui_v2.py --name dev
|
||||
@app.cls(
|
||||
max_containers=1, # limit interactive session to 1 container
|
||||
# gpu="L40S", # good starter GPU for inference
|
||||
volumes={"/cache": vol}, # mounts our cached models
|
||||
secrets=[custom_secret]
|
||||
)
|
||||
@modal.concurrent(
|
||||
max_inputs=10
|
||||
) # required for UI startup process which runs several API calls concurrently
|
||||
@modal.web_server(8000, startup_timeout=60)
|
||||
class ModalComfy():
|
||||
|
||||
subprocess.Popen("comfy launch -- --cpu --listen 0.0.0.0 --port 8000", shell=True)
|
||||
98
ext/cos_utils.py
Normal file
98
ext/cos_utils.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
"""
|
||||
File cos_utils.py
|
||||
Author silence
|
||||
Date 2025/6/11 20:37
|
||||
"""
|
||||
import mimetypes
|
||||
import os
|
||||
from loguru import logger
|
||||
from qcloud_cos import CosConfig
|
||||
from qcloud_cos import CosS3Client
|
||||
|
||||
# COS配置
|
||||
COS_BUCKET_NAME = 'sucai-1324682537'
|
||||
COS_SECRET_ID = 'AKIDsrihIyjZOBsjimt8TsN8yvv1AMh5dB44'
|
||||
COS_SECRET_KEY = 'CPZcxdk6W39Jd4cGY95wvupoyMd0YFqW'
|
||||
COS_REGION = 'ap-shanghai'
|
||||
|
||||
|
||||
class CosUploadNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"file_path": ("STRING", {"forceInput": True}),
|
||||
},
|
||||
"optional": {
|
||||
"remove_source": ("BOOLEAN", {"default": False}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("upload_url",)
|
||||
FUNCTION = "upload_to_cos"
|
||||
CATEGORY = "不忘科技-自定义节点🚩"
|
||||
|
||||
def upload_to_cos(self, file_path, remove_source=False):
|
||||
try:
|
||||
# 检查文件是否存在
|
||||
if not os.path.isfile(file_path):
|
||||
raise Exception(f"文件不存在: {file_path}")
|
||||
|
||||
# 获取文件MIME类型和分类
|
||||
mime_type, _ = mimetypes.guess_type(file_path)
|
||||
if mime_type:
|
||||
category = mime_type.split('/')[0]
|
||||
else:
|
||||
category = 'unknown'
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
object_key = f'tk/{category}/{file_name}'
|
||||
|
||||
# 配置COS客户端
|
||||
config = CosConfig(
|
||||
Region=COS_REGION,
|
||||
SecretId=COS_SECRET_ID,
|
||||
SecretKey=COS_SECRET_KEY
|
||||
)
|
||||
client = CosS3Client(config)
|
||||
|
||||
# 上传文件
|
||||
client.upload_file(
|
||||
Bucket=COS_BUCKET_NAME,
|
||||
Key=object_key,
|
||||
LocalFilePath=file_path,
|
||||
EnableMD5=False
|
||||
)
|
||||
|
||||
# 生成访问URL
|
||||
upload_url = f'https://{COS_BUCKET_NAME}.cos.{COS_REGION}.myqcloud.com/{object_key}'
|
||||
|
||||
logger.info(f'文件上传成功: {upload_url}')
|
||||
|
||||
# 如果需要删除源文件
|
||||
if remove_source and os.path.exists(file_path):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
logger.info(f'源文件已删除: {file_path}')
|
||||
except Exception as e:
|
||||
logger.warning(f'删除源文件失败: {e}')
|
||||
|
||||
return (upload_url,)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f'上传失败: {str(e)}'
|
||||
logger.error(error_msg)
|
||||
# 返回错误信息而不是抛出异常,这样ComfyUI不会中断流程
|
||||
return (f"ERROR: {error_msg}",)
|
||||
|
||||
|
||||
# 节点注册
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"CosUploadNode": CosUploadNode
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"CosUploadNode": "腾讯COS上传"
|
||||
}
|
||||
71
ext/image.py
Normal file
71
ext/image.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import os
|
||||
import uuid
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
|
||||
class SaveImagePath:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"image_path": ("IMAGE", {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "load"
|
||||
CATEGORY = "不忘科技-自定义节点🚩"
|
||||
|
||||
def load(self, image_path):
|
||||
# 如果是torch.Tensor类型,转换为numpy数组
|
||||
if isinstance(image_path, torch.Tensor):
|
||||
image_path = image_path.cpu().numpy()
|
||||
|
||||
# 去除多余的维度,如果形状是(1, 1, height, width, channels)或(1, height, width, channels)等情况
|
||||
while len(image_path.shape) > 3:
|
||||
image_path = image_path.squeeze(0)
|
||||
|
||||
# 如果是通道优先格式 (C, H, W),转换为通道最后格式 (H, W, C)
|
||||
if len(image_path.shape) == 3 and image_path.shape[0] <= 4:
|
||||
image_path = np.transpose(image_path, (1, 2, 0))
|
||||
|
||||
# 如果是单通道图像,转换为3通道
|
||||
if len(image_path.shape) == 2:
|
||||
image_path = np.stack([image_path] * 3, axis=-1)
|
||||
|
||||
# 数据范围和类型转换 - 这是关键修复
|
||||
if image_path.dtype == np.float32 or image_path.dtype == np.float64:
|
||||
# ComfyUI图像数据通常是0-1范围的浮点数
|
||||
if image_path.max() <= 1.0:
|
||||
# 从0-1范围转换到0-255范围
|
||||
image_path = (image_path * 255.0).astype(np.uint8)
|
||||
else:
|
||||
# 如果已经是0-255范围,直接转换类型
|
||||
image_path = np.clip(image_path, 0, 255).astype(np.uint8)
|
||||
elif image_path.dtype != np.uint8:
|
||||
# 其他数据类型,确保在0-255范围内
|
||||
image_path = np.clip(image_path, 0, 255).astype(np.uint8)
|
||||
|
||||
pil_image = Image.fromarray(image_path)
|
||||
|
||||
output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output")
|
||||
# base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# output_dir = '/root/comfy/ComfyUI/output'
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
file_name = "%s.png" % str(uuid.uuid4())
|
||||
p = os.path.join(output_dir, file_name)
|
||||
pil_image.save(p)
|
||||
|
||||
return (p,)
|
||||
|
||||
|
||||
# 节点类定义结束,以下是用于注册节点的字典结构(通常在实际使用中由ComfyUI等框架来解析和注册)
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SaveImagePath": SaveImagePath
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SaveImagePath": "保存图片路径"
|
||||
}
|
||||
62
ext/load_image_pro.py
Normal file
62
ext/load_image_pro.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from io import BytesIO
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
|
||||
# 定义节点类
|
||||
class LoadNetImg:
|
||||
# 定义节点输入类型
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image_url": ("STRING", {
|
||||
"default": "https://example.com/sample.jpg",
|
||||
"multiline": False
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
# 定义节点输出类型
|
||||
RETURN_TYPES = ("IMAGE",) # 返回图像数据
|
||||
RETURN_NAMES = ("image",) # 命名返回值
|
||||
FUNCTION = "load_image_task" # 函数标识符,方便注册多个节点功能
|
||||
OUTPUT_NODE = False # 不允许该节点直接作为最终输出节点
|
||||
CATEGORY = "image" # 节点所属类别(在 ComfyUI 界面中分类)
|
||||
|
||||
def load_image_task(self, image_url):
|
||||
try:
|
||||
if not image_url or not image_url.strip():
|
||||
raise ValueError("需要提供图片URL")
|
||||
|
||||
# 下载网络图片
|
||||
response = requests.get(image_url)
|
||||
response.raise_for_status() # 请求失败时抛出异常
|
||||
image = Image.open(BytesIO(response.content)).convert("RGB")
|
||||
|
||||
# 按照官方格式转换图像数据
|
||||
# Convert to numpy array and normalize to 0-1
|
||||
image_array = np.array(image).astype(np.float32) / 255.0
|
||||
# Convert to torch tensor and add batch dimension
|
||||
image_tensor = torch.from_numpy(image_array)[None,]
|
||||
|
||||
return (image_tensor,) # 返回torch张量
|
||||
except Exception as e:
|
||||
print(f"Error loading image: {e}")
|
||||
# 返回一个空的黑色图片作为错误处理
|
||||
empty_image = torch.zeros((1, 512, 512, 3), dtype=torch.float32)
|
||||
return (empty_image,)
|
||||
|
||||
|
||||
|
||||
|
||||
# 映射节点类和名称
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LoadNetImg": LoadNetImg, # 将类映射到节点名称
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LoadNetImg": "load_net_image", # 节点显示名称
|
||||
}
|
||||
1093
ext/nodes_bfl.py
Normal file
1093
ext/nodes_bfl.py
Normal file
File diff suppressed because it is too large
Load Diff
40
ext/prompt_id_generator.py
Normal file
40
ext/prompt_id_generator.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import uuid
|
||||
|
||||
|
||||
class TaskIdGenerate:
|
||||
"""TaskID生成器:用户可传入或自动生成TaskID"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"custom_task_id": ("STRING", {"default": "", "placeholder": "留空则自动生成"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("task_id",)
|
||||
FUNCTION = "generate_task_id"
|
||||
OUTPUT_NODE = False
|
||||
CATEGORY = "utils"
|
||||
|
||||
def generate_task_id(self, custom_task_id=""):
|
||||
if custom_task_id and custom_task_id.strip():
|
||||
# 用户输入了自定义ID
|
||||
task_id = custom_task_id.strip()
|
||||
print(f"📝 使用自定义TaskID: {task_id}")
|
||||
else:
|
||||
# 自动生成UUID
|
||||
task_id = str(uuid.uuid4())
|
||||
print(f"🎲 自动生成TaskID: {task_id}")
|
||||
|
||||
return (task_id,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"TaskIdGenerate": TaskIdGenerate
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"TaskIdGenerate": "TaskID生成器"
|
||||
}
|
||||
77
ext/random_line.py
Normal file
77
ext/random_line.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import random
|
||||
import time
|
||||
|
||||
|
||||
class RandomLineSelector:
|
||||
"""
|
||||
ComfyUI自定义节点:随机行选择器
|
||||
从输入的多行文本中随机选择一行输出
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": """
|
||||
保持人物样貌和衣服不变,改变人物的光影效果。更换背景: 图像呈现出一个温馨的维多利亚复古风格房间,墙面被多样化的艺术画框与照片装饰,展现出浓郁的怀旧氛围。中心的金框全身镜是视觉焦点,上面缠绕着精致花卉装饰,增强了空间的艺术感。左侧靠墙摆放花瓶与烛台,白色地板映衬着跳动的暖黄色光线,营造出柔和而梦幻的氛围。右侧床铺整洁柔软,与一旁的小抽屉柜和台灯形成和谐的组合。整体画面色调以暖黄色为主,搭配田园风格装饰元素,构图均衡,注重细节,体现了舒适浪漫的生活场景。人物重新打光融合到场景中。
|
||||
保持人物形象和衣服不变,改变人物的光影效果。更换背景: 衣帽间的开放式悬挂区搭配浅色木地板,衣杆上挂满中性色长裙,旁边搁板整齐叠放针织毛衣与围巾。
|
||||
保持人物形象和衣服不变,改变人物的光影效果。更换背景: 白色衣柜门打开后,分层挂满长裙和连体裤,底部抽屉收纳着色彩鲜艳的运动服与家居裤。
|
||||
保持人物形象和衣服不变,改变人物的光影效果。更换背景: 玻璃门的衣柜设计体现精致生活,内部悬挂精致衬衫和镂空裙摆,饰品抽屉随时为配件搭配提供轻便选择。
|
||||
"""
|
||||
}),
|
||||
"seed": ("INT", {
|
||||
"default": 0,
|
||||
"min": -1,
|
||||
"max": 0xffffffffffffffff
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("selected_line",)
|
||||
FUNCTION = "select_random_line"
|
||||
CATEGORY = "text"
|
||||
|
||||
def select_random_line(self, text, seed):
|
||||
"""
|
||||
从输入文本中随机选择一行
|
||||
|
||||
Args:
|
||||
text (str): 输入的多行文本
|
||||
seed (int): 随机种子
|
||||
|
||||
Returns:
|
||||
tuple: 包含选中行的元组
|
||||
"""
|
||||
# 设置随机种子(-1表示使用当前时间戳)
|
||||
if seed == -1:
|
||||
random.seed(int(time.time() * 1000000)) # 使用微秒级时间戳
|
||||
else:
|
||||
random.seed(seed)
|
||||
|
||||
# 过滤掉空行
|
||||
non_empty_lines = [line.strip() for line in text.split('\n') if line.strip()]
|
||||
|
||||
# 如果没有非空行,返回空字符串
|
||||
if not non_empty_lines:
|
||||
return ("",)
|
||||
|
||||
# 随机选择一行
|
||||
selected_line = random.choice(non_empty_lines)
|
||||
|
||||
return (selected_line,)
|
||||
|
||||
|
||||
# ComfyUI节点映射
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"RandomLineSelector": RandomLineSelector
|
||||
}
|
||||
|
||||
# 节点显示名称映射
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RandomLineSelector": "随机选择一行内容"
|
||||
}
|
||||
# https://bowongai--dev-ui.modal.run/
|
||||
# https://bowongai--dev-ui.modal.run
|
||||
187
ext/webhook_forward.py
Normal file
187
ext/webhook_forward.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
"""
|
||||
File webhook_forward.py
|
||||
Author silence
|
||||
Date 2025/6/13 13:42
|
||||
"""
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class PromptIDGenerator:
|
||||
"""PromptID生成器:用户可传入或自动生成PromptID"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"custom_prompt_id": ("STRING", {"default": "", "placeholder": "留空则自动生成"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("prompt_id",)
|
||||
FUNCTION = "generate_prompt_id"
|
||||
OUTPUT_NODE = False
|
||||
CATEGORY = "utils"
|
||||
|
||||
def generate_prompt_id(self, custom_prompt_id=""):
|
||||
if custom_prompt_id and custom_prompt_id.strip():
|
||||
# 用户输入了自定义ID
|
||||
prompt_id = custom_prompt_id.strip()
|
||||
print(f"📝 使用自定义PromptID: {prompt_id}")
|
||||
else:
|
||||
# 自动生成ID:时间戳 + 随机字符
|
||||
timestamp = str(int(time.time()))
|
||||
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||
prompt_id = f"prompt_{timestamp}_{random_str}"
|
||||
print(f"🎲 自动生成PromptID: {prompt_id}")
|
||||
|
||||
return (prompt_id,)
|
||||
|
||||
|
||||
class PlugAndPlayWebhook:
|
||||
"""即插即用Webhook节点:连上线就能转发数据"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"webhook_url": ("STRING", {"default": "http://127.0.0.1:8010/handler/webhook",
|
||||
"placeholder": "https://your-api.com/webhook"}),
|
||||
"image_url": ("STRING", {"default": "",
|
||||
"placeholder": "图片的url"}),
|
||||
},
|
||||
"optional": {
|
||||
"prompt_id": ("STRING", {"default": ""}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "send"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "utils"
|
||||
|
||||
def send(self, webhook_url, image_url, prompt_id="", prompt=None, extra_pnginfo=None, unique_id=None):
|
||||
if not webhook_url:
|
||||
raise ValueError("❌ 请填写Webhook URL!")
|
||||
|
||||
# 使用传入的prompt_id,如果没有则用unique_id
|
||||
final_prompt_id = prompt_id or unique_id or "unknown"
|
||||
|
||||
# 准备发送的数据
|
||||
data = {
|
||||
"img_base64": image_url,
|
||||
"format": "png",
|
||||
"image_url": image_url,
|
||||
"prompt_id": final_prompt_id,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
# 发送Webhook
|
||||
try:
|
||||
response = requests.post(webhook_url, json=data)
|
||||
response.raise_for_status()
|
||||
print(f'发送的数据:{data}')
|
||||
except Exception as e:
|
||||
print(f"❌ 发送失败: {str(e)}")
|
||||
|
||||
# 终端节点,无需返回
|
||||
return ()
|
||||
|
||||
|
||||
class SaveImageWithOutput:
|
||||
"""保存图片并输出的节点:既保存图片又能继续传递数据"""
|
||||
|
||||
def __init__(self):
|
||||
self.output_dir = "output"
|
||||
self.type = "output"
|
||||
self.prefix_append = ""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "STRING")
|
||||
RETURN_NAMES = ("images", "file_path")
|
||||
FUNCTION = "save_image"
|
||||
OUTPUT_NODE = False
|
||||
CATEGORY = "image"
|
||||
|
||||
def save_image(self, images, filename_prefix="ComfyUI"):
|
||||
filename_prefix += self.prefix_append
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = self.get_save_image_path(filename_prefix,
|
||||
self.output_dir)
|
||||
|
||||
file_paths = []
|
||||
|
||||
for i, image in enumerate(images):
|
||||
# 转换图片数据
|
||||
img_array = 255. * image.cpu().numpy()
|
||||
img = Image.fromarray(np.clip(img_array, 0, 255).astype(np.uint8))
|
||||
|
||||
# 生成文件名
|
||||
file = f"{filename_prefix}_{counter + i:05}_.png"
|
||||
file_path = os.path.join(full_output_folder, file)
|
||||
|
||||
# 保存图片
|
||||
img.save(file_path, compress_level=4)
|
||||
file_paths.append(file_path)
|
||||
|
||||
print(f"✅ 图片已保存到: {file_paths[0] if file_paths else '未知路径'}")
|
||||
|
||||
# 直接返回元组:(原始图片数据, 第一个文件路径)
|
||||
return (images, file_paths[0] if file_paths else "")
|
||||
|
||||
def get_save_image_path(self, filename_prefix, output_dir):
|
||||
def map_filename(filename):
|
||||
prefix_len = len(os.path.basename(filename_prefix))
|
||||
prefix = filename[:prefix_len + 1]
|
||||
try:
|
||||
digits = int(filename[prefix_len + 1:].split('_')[0])
|
||||
except:
|
||||
digits = 0
|
||||
return (digits, prefix)
|
||||
|
||||
subfolder = ""
|
||||
full_output_folder = os.path.join(output_dir, subfolder)
|
||||
|
||||
if os.path.commonpath((output_dir, os.path.abspath(full_output_folder))) != output_dir:
|
||||
print("Saving image outside the output folder is not allowed.")
|
||||
return {}
|
||||
|
||||
try:
|
||||
counter = max(filter(lambda a: a[1][:-1] == filename_prefix and a[1][-1] == "_",
|
||||
map(map_filename, os.listdir(full_output_folder))))[0] + 1
|
||||
except ValueError:
|
||||
counter = 1
|
||||
except FileNotFoundError:
|
||||
os.makedirs(full_output_folder, exist_ok=True)
|
||||
counter = 1
|
||||
|
||||
return full_output_folder, filename_prefix, counter, subfolder, filename_prefix
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"PlugAndPlayWebhook": PlugAndPlayWebhook,
|
||||
"SaveImageWithOutput": SaveImageWithOutput
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"PlugAndPlayWebhook": "Webhook转发器",
|
||||
"SaveImageWithOutput": "保存图片(带输出)"
|
||||
}
|
||||
98
nodes/cos_utils.py
Normal file
98
nodes/cos_utils.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
"""
|
||||
File cos_utils.py
|
||||
Author silence
|
||||
Date 2025/6/11 20:37
|
||||
"""
|
||||
import mimetypes
|
||||
import os
|
||||
from loguru import logger
|
||||
from qcloud_cos import CosConfig
|
||||
from qcloud_cos import CosS3Client
|
||||
|
||||
# COS配置
|
||||
COS_BUCKET_NAME = 'sucai-1324682537'
|
||||
COS_SECRET_ID = 'AKIDsrihIyjZOBsjimt8TsN8yvv1AMh5dB44'
|
||||
COS_SECRET_KEY = 'CPZcxdk6W39Jd4cGY95wvupoyMd0YFqW'
|
||||
COS_REGION = 'ap-shanghai'
|
||||
|
||||
|
||||
class CosUploadNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"file_path": ("STRING", {"forceInput": True}),
|
||||
},
|
||||
"optional": {
|
||||
"remove_source": ("BOOLEAN", {"default": False}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("upload_url",)
|
||||
FUNCTION = "upload_to_cos"
|
||||
CATEGORY = "不忘科技-自定义节点🚩"
|
||||
|
||||
def upload_to_cos(self, file_path, remove_source=False):
|
||||
try:
|
||||
# 检查文件是否存在
|
||||
if not os.path.isfile(file_path):
|
||||
raise Exception(f"文件不存在: {file_path}")
|
||||
|
||||
# 获取文件MIME类型和分类
|
||||
mime_type, _ = mimetypes.guess_type(file_path)
|
||||
if mime_type:
|
||||
category = mime_type.split('/')[0]
|
||||
else:
|
||||
category = 'unknown'
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
object_key = f'tk/{category}/{file_name}'
|
||||
|
||||
# 配置COS客户端
|
||||
config = CosConfig(
|
||||
Region=COS_REGION,
|
||||
SecretId=COS_SECRET_ID,
|
||||
SecretKey=COS_SECRET_KEY
|
||||
)
|
||||
client = CosS3Client(config)
|
||||
|
||||
# 上传文件
|
||||
client.upload_file(
|
||||
Bucket=COS_BUCKET_NAME,
|
||||
Key=object_key,
|
||||
LocalFilePath=file_path,
|
||||
EnableMD5=False
|
||||
)
|
||||
|
||||
# 生成访问URL
|
||||
upload_url = f'https://{COS_BUCKET_NAME}.cos.{COS_REGION}.myqcloud.com/{object_key}'
|
||||
|
||||
logger.info(f'文件上传成功: {upload_url}')
|
||||
|
||||
# 如果需要删除源文件
|
||||
if remove_source and os.path.exists(file_path):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
logger.info(f'源文件已删除: {file_path}')
|
||||
except Exception as e:
|
||||
logger.warning(f'删除源文件失败: {e}')
|
||||
|
||||
return (upload_url,)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f'上传失败: {str(e)}'
|
||||
logger.error(error_msg)
|
||||
# 返回错误信息而不是抛出异常,这样ComfyUI不会中断流程
|
||||
return (f"ERROR: {error_msg}",)
|
||||
|
||||
|
||||
# 节点注册
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"CosUploadNode": CosUploadNode
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"CosUploadNode": "腾讯COS上传"
|
||||
}
|
||||
@@ -13,7 +13,6 @@ class SaveImagePath:
|
||||
"image_path": ("IMAGE", {"forceInput": True}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "load"
|
||||
CATEGORY = "不忘科技-自定义节点🚩"
|
||||
@@ -50,13 +49,13 @@ class SaveImagePath:
|
||||
|
||||
pil_image = Image.fromarray(image_path)
|
||||
|
||||
# output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output")
|
||||
output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output")
|
||||
# base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
output_dir = '/root/comfy/ComfyUI/output'
|
||||
# output_dir = '/root/comfy/ComfyUI/output'
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
file_name = "%s.jpg" % str(uuid.uuid4())
|
||||
file_name = "%s.png" % str(uuid.uuid4())
|
||||
p = os.path.join(output_dir, file_name)
|
||||
pil_image.save(p)
|
||||
|
||||
|
||||
62
nodes/load_image_pro.py
Normal file
62
nodes/load_image_pro.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from io import BytesIO
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
|
||||
# 定义节点类
|
||||
class LoadNetImg:
|
||||
# 定义节点输入类型
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"image_url": ("STRING", {
|
||||
"default": "https://example.com/sample.jpg",
|
||||
"multiline": False
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
# 定义节点输出类型
|
||||
RETURN_TYPES = ("IMAGE",) # 返回图像数据
|
||||
RETURN_NAMES = ("image",) # 命名返回值
|
||||
FUNCTION = "load_image_task" # 函数标识符,方便注册多个节点功能
|
||||
OUTPUT_NODE = False # 不允许该节点直接作为最终输出节点
|
||||
CATEGORY = "image" # 节点所属类别(在 ComfyUI 界面中分类)
|
||||
|
||||
def load_image_task(self, image_url):
|
||||
try:
|
||||
if not image_url or not image_url.strip():
|
||||
raise ValueError("需要提供图片URL")
|
||||
|
||||
# 下载网络图片
|
||||
response = requests.get(image_url)
|
||||
response.raise_for_status() # 请求失败时抛出异常
|
||||
image = Image.open(BytesIO(response.content)).convert("RGB")
|
||||
|
||||
# 按照官方格式转换图像数据
|
||||
# Convert to numpy array and normalize to 0-1
|
||||
image_array = np.array(image).astype(np.float32) / 255.0
|
||||
# Convert to torch tensor and add batch dimension
|
||||
image_tensor = torch.from_numpy(image_array)[None,]
|
||||
|
||||
return (image_tensor,) # 返回torch张量
|
||||
except Exception as e:
|
||||
print(f"Error loading image: {e}")
|
||||
# 返回一个空的黑色图片作为错误处理
|
||||
empty_image = torch.zeros((1, 512, 512, 3), dtype=torch.float32)
|
||||
return (empty_image,)
|
||||
|
||||
|
||||
|
||||
|
||||
# 映射节点类和名称
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"LoadNetImg": LoadNetImg, # 将类映射到节点名称
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"LoadNetImg": "load_net_image", # 节点显示名称
|
||||
}
|
||||
1093
nodes/nodes_bfl.py
Normal file
1093
nodes/nodes_bfl.py
Normal file
File diff suppressed because it is too large
Load Diff
40
nodes/prompt_id_generator.py
Normal file
40
nodes/prompt_id_generator.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import uuid
|
||||
|
||||
|
||||
class TaskIdGenerate:
|
||||
"""TaskID生成器:用户可传入或自动生成TaskID"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"custom_task_id": ("STRING", {"default": "", "placeholder": "留空则自动生成"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("task_id",)
|
||||
FUNCTION = "generate_task_id"
|
||||
OUTPUT_NODE = False
|
||||
CATEGORY = "utils"
|
||||
|
||||
def generate_task_id(self, custom_task_id=""):
|
||||
if custom_task_id and custom_task_id.strip():
|
||||
# 用户输入了自定义ID
|
||||
task_id = custom_task_id.strip()
|
||||
print(f"📝 使用自定义TaskID: {task_id}")
|
||||
else:
|
||||
# 自动生成UUID
|
||||
task_id = str(uuid.uuid4())
|
||||
print(f"🎲 自动生成TaskID: {task_id}")
|
||||
|
||||
return (task_id,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"TaskIdGenerate": TaskIdGenerate
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"TaskIdGenerate": "TaskID生成器"
|
||||
}
|
||||
77
nodes/random_line.py
Normal file
77
nodes/random_line.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import random
|
||||
import time
|
||||
|
||||
|
||||
class RandomLineSelector:
|
||||
"""
|
||||
ComfyUI自定义节点:随机行选择器
|
||||
从输入的多行文本中随机选择一行输出
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"text": ("STRING", {
|
||||
"multiline": True,
|
||||
"default": """
|
||||
保持人物样貌和衣服不变,改变人物的光影效果。更换背景: 图像呈现出一个温馨的维多利亚复古风格房间,墙面被多样化的艺术画框与照片装饰,展现出浓郁的怀旧氛围。中心的金框全身镜是视觉焦点,上面缠绕着精致花卉装饰,增强了空间的艺术感。左侧靠墙摆放花瓶与烛台,白色地板映衬着跳动的暖黄色光线,营造出柔和而梦幻的氛围。右侧床铺整洁柔软,与一旁的小抽屉柜和台灯形成和谐的组合。整体画面色调以暖黄色为主,搭配田园风格装饰元素,构图均衡,注重细节,体现了舒适浪漫的生活场景。人物重新打光融合到场景中。
|
||||
保持人物形象和衣服不变,改变人物的光影效果。更换背景: 衣帽间的开放式悬挂区搭配浅色木地板,衣杆上挂满中性色长裙,旁边搁板整齐叠放针织毛衣与围巾。
|
||||
保持人物形象和衣服不变,改变人物的光影效果。更换背景: 白色衣柜门打开后,分层挂满长裙和连体裤,底部抽屉收纳着色彩鲜艳的运动服与家居裤。
|
||||
保持人物形象和衣服不变,改变人物的光影效果。更换背景: 玻璃门的衣柜设计体现精致生活,内部悬挂精致衬衫和镂空裙摆,饰品抽屉随时为配件搭配提供轻便选择。
|
||||
"""
|
||||
}),
|
||||
"seed": ("INT", {
|
||||
"default": 0,
|
||||
"min": -1,
|
||||
"max": 0xffffffffffffffff
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("selected_line",)
|
||||
FUNCTION = "select_random_line"
|
||||
CATEGORY = "text"
|
||||
|
||||
def select_random_line(self, text, seed):
|
||||
"""
|
||||
从输入文本中随机选择一行
|
||||
|
||||
Args:
|
||||
text (str): 输入的多行文本
|
||||
seed (int): 随机种子
|
||||
|
||||
Returns:
|
||||
tuple: 包含选中行的元组
|
||||
"""
|
||||
# 设置随机种子(-1表示使用当前时间戳)
|
||||
if seed == -1:
|
||||
random.seed(int(time.time() * 1000000)) # 使用微秒级时间戳
|
||||
else:
|
||||
random.seed(seed)
|
||||
|
||||
# 过滤掉空行
|
||||
non_empty_lines = [line.strip() for line in text.split('\n') if line.strip()]
|
||||
|
||||
# 如果没有非空行,返回空字符串
|
||||
if not non_empty_lines:
|
||||
return ("",)
|
||||
|
||||
# 随机选择一行
|
||||
selected_line = random.choice(non_empty_lines)
|
||||
|
||||
return (selected_line,)
|
||||
|
||||
|
||||
# ComfyUI节点映射
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"RandomLineSelector": RandomLineSelector
|
||||
}
|
||||
|
||||
# 节点显示名称映射
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RandomLineSelector": "随机选择一行内容"
|
||||
}
|
||||
# https://bowongai--dev-ui.modal.run/
|
||||
# https://bowongai--dev-ui.modal.run
|
||||
187
nodes/webhook_forward.py
Normal file
187
nodes/webhook_forward.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
"""
|
||||
File webhook_forward.py
|
||||
Author silence
|
||||
Date 2025/6/13 13:42
|
||||
"""
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class PromptIDGenerator:
|
||||
"""PromptID生成器:用户可传入或自动生成PromptID"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"custom_prompt_id": ("STRING", {"default": "", "placeholder": "留空则自动生成"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("prompt_id",)
|
||||
FUNCTION = "generate_prompt_id"
|
||||
OUTPUT_NODE = False
|
||||
CATEGORY = "utils"
|
||||
|
||||
def generate_prompt_id(self, custom_prompt_id=""):
|
||||
if custom_prompt_id and custom_prompt_id.strip():
|
||||
# 用户输入了自定义ID
|
||||
prompt_id = custom_prompt_id.strip()
|
||||
print(f"📝 使用自定义PromptID: {prompt_id}")
|
||||
else:
|
||||
# 自动生成ID:时间戳 + 随机字符
|
||||
timestamp = str(int(time.time()))
|
||||
random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
||||
prompt_id = f"prompt_{timestamp}_{random_str}"
|
||||
print(f"🎲 自动生成PromptID: {prompt_id}")
|
||||
|
||||
return (prompt_id,)
|
||||
|
||||
|
||||
class PlugAndPlayWebhook:
|
||||
"""即插即用Webhook节点:连上线就能转发数据"""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"webhook_url": ("STRING", {"default": "http://127.0.0.1:8010/handler/webhook",
|
||||
"placeholder": "https://your-api.com/webhook"}),
|
||||
"image_url": ("STRING", {"default": "",
|
||||
"placeholder": "图片的url"}),
|
||||
},
|
||||
"optional": {
|
||||
"prompt_id": ("STRING", {"default": ""}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "send"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "utils"
|
||||
|
||||
def send(self, webhook_url, image_url, prompt_id="", prompt=None, extra_pnginfo=None, unique_id=None):
|
||||
if not webhook_url:
|
||||
raise ValueError("❌ 请填写Webhook URL!")
|
||||
|
||||
# 使用传入的prompt_id,如果没有则用unique_id
|
||||
final_prompt_id = prompt_id or unique_id or "unknown"
|
||||
|
||||
# 准备发送的数据
|
||||
data = {
|
||||
"img_base64": image_url,
|
||||
"format": "png",
|
||||
"image_url": image_url,
|
||||
"prompt_id": final_prompt_id,
|
||||
"timestamp": time.time()
|
||||
}
|
||||
|
||||
# 发送Webhook
|
||||
try:
|
||||
response = requests.post(webhook_url, json=data)
|
||||
response.raise_for_status()
|
||||
print(f'发送的数据:{data}')
|
||||
except Exception as e:
|
||||
print(f"❌ 发送失败: {str(e)}")
|
||||
|
||||
# 终端节点,无需返回
|
||||
return ()
|
||||
|
||||
|
||||
class SaveImageWithOutput:
|
||||
"""保存图片并输出的节点:既保存图片又能继续传递数据"""
|
||||
|
||||
def __init__(self):
|
||||
self.output_dir = "output"
|
||||
self.type = "output"
|
||||
self.prefix_append = ""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "STRING")
|
||||
RETURN_NAMES = ("images", "file_path")
|
||||
FUNCTION = "save_image"
|
||||
OUTPUT_NODE = False
|
||||
CATEGORY = "image"
|
||||
|
||||
def save_image(self, images, filename_prefix="ComfyUI"):
|
||||
filename_prefix += self.prefix_append
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = self.get_save_image_path(filename_prefix,
|
||||
self.output_dir)
|
||||
|
||||
file_paths = []
|
||||
|
||||
for i, image in enumerate(images):
|
||||
# 转换图片数据
|
||||
img_array = 255. * image.cpu().numpy()
|
||||
img = Image.fromarray(np.clip(img_array, 0, 255).astype(np.uint8))
|
||||
|
||||
# 生成文件名
|
||||
file = f"{filename_prefix}_{counter + i:05}_.png"
|
||||
file_path = os.path.join(full_output_folder, file)
|
||||
|
||||
# 保存图片
|
||||
img.save(file_path, compress_level=4)
|
||||
file_paths.append(file_path)
|
||||
|
||||
print(f"✅ 图片已保存到: {file_paths[0] if file_paths else '未知路径'}")
|
||||
|
||||
# 直接返回元组:(原始图片数据, 第一个文件路径)
|
||||
return (images, file_paths[0] if file_paths else "")
|
||||
|
||||
def get_save_image_path(self, filename_prefix, output_dir):
|
||||
def map_filename(filename):
|
||||
prefix_len = len(os.path.basename(filename_prefix))
|
||||
prefix = filename[:prefix_len + 1]
|
||||
try:
|
||||
digits = int(filename[prefix_len + 1:].split('_')[0])
|
||||
except:
|
||||
digits = 0
|
||||
return (digits, prefix)
|
||||
|
||||
subfolder = ""
|
||||
full_output_folder = os.path.join(output_dir, subfolder)
|
||||
|
||||
if os.path.commonpath((output_dir, os.path.abspath(full_output_folder))) != output_dir:
|
||||
print("Saving image outside the output folder is not allowed.")
|
||||
return {}
|
||||
|
||||
try:
|
||||
counter = max(filter(lambda a: a[1][:-1] == filename_prefix and a[1][-1] == "_",
|
||||
map(map_filename, os.listdir(full_output_folder))))[0] + 1
|
||||
except ValueError:
|
||||
counter = 1
|
||||
except FileNotFoundError:
|
||||
os.makedirs(full_output_folder, exist_ok=True)
|
||||
counter = 1
|
||||
|
||||
return full_output_folder, filename_prefix, counter, subfolder, filename_prefix
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"PlugAndPlayWebhook": PlugAndPlayWebhook,
|
||||
"SaveImageWithOutput": SaveImageWithOutput
|
||||
}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"PlugAndPlayWebhook": "Webhook转发器",
|
||||
"SaveImageWithOutput": "保存图片(带输出)"
|
||||
}
|
||||
28
pyproject.toml
Normal file
28
pyproject.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "modal_comfyui"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"fastapi[standard]==0.115.4",
|
||||
"cos-python-sdk-v5",
|
||||
"sqlalchemy",
|
||||
"ultralytics",
|
||||
"tencentcloud-sdk-python",
|
||||
"pymysql",
|
||||
"Pillow",
|
||||
"ffmpy",
|
||||
"opencv-python",
|
||||
"av",
|
||||
"imageio",
|
||||
"loguru",
|
||||
"openai-whisper",
|
||||
"sentry-sdk",
|
||||
"pydantic",
|
||||
"pydantic_settings",
|
||||
"conformer==0.3.2",
|
||||
"einops>0.6.1"
|
||||
]
|
||||
28
pyproject_comfyui.toml
Normal file
28
pyproject_comfyui.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "modal_comfyui"
|
||||
version = "1.0.0"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"fastapi[standard]==0.115.4",
|
||||
"cos-python-sdk-v5",
|
||||
"sqlalchemy",
|
||||
"ultralytics",
|
||||
"tencentcloud-sdk-python",
|
||||
"pymysql",
|
||||
"Pillow",
|
||||
"ffmpy",
|
||||
"opencv-python",
|
||||
"av",
|
||||
"imageio",
|
||||
"loguru",
|
||||
"openai-whisper",
|
||||
"sentry-sdk",
|
||||
"pydantic",
|
||||
"pydantic_settings",
|
||||
"conformer==0.3.2",
|
||||
"einops>0.6.1"
|
||||
]
|
||||
Reference in New Issue
Block a user