将自定义节点放置到custom目录下

This commit is contained in:
2025-06-27 11:29:23 +08:00
parent 5a220bc39a
commit c9b0b3db82
18 changed files with 3340 additions and 12 deletions

View File

@@ -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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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
View 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上传"
}

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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
View 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
View 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"
]