PERF 重构项目,优化命名和结构

FIX 补齐requirements
FIX 剔除硬编码
This commit is contained in:
2025-07-10 15:32:26 +08:00
parent fe30757baf
commit 1f3c9db743
43 changed files with 1522 additions and 3099 deletions

View File

@@ -0,0 +1,67 @@
# 文件名 comfyui_v2.py
import subprocess
import modal
image = (
modal.Image.debian_slim(
python_version="3.10"
)
.apt_install("git", "gcc", "libportaudio2", "ffmpeg")
.pip_install("comfy_cli==0.0.0",
index_url="https://packages-1747622887395:0ee15474ccd7b27b57ca63a9306327678e6c2631@g-ldyi2063-pypi.pkg.coding.net/dev/packages/simple")
.run_commands(
"comfy --skip-prompt install --fast-deps --nvidia --version 0.3.40"
)
.pip_install_from_pyproject("./pyproject.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")
.run_commands("cp -f /root/comfy/ComfyUI/custom_nodes/ComfyUI-CustomNode/ext/nodes_bfl.py /root/comfy/ComfyUI/comfy_api_nodes/nodes_bfl.py")
)
app = modal.App(image=image)
custom_secret = modal.Secret.from_name("comfyui-custom-secret", environment_name="dev")
@app.function(
max_containers=1,
secrets=[custom_secret]
)
@modal.concurrent(
max_inputs=10
)
@modal.web_server(8000, startup_timeout=60)
def ui_1():
subprocess.Popen("comfy launch -- --cpu --listen 0.0.0.0 --port 8000", shell=True)
@app.function(
max_containers=1,
secrets=[custom_secret]
)
@modal.concurrent(
max_inputs=10
)
@modal.web_server(8000, startup_timeout=60)
def ui_2():
subprocess.Popen("comfy launch -- --cpu --listen 0.0.0.0 --port 8000", shell=True)
@app.function(
max_containers=1,
secrets=[custom_secret]
)
@modal.concurrent(
max_inputs=10
)
@modal.web_server(8000, startup_timeout=60)
def ui_3():
subprocess.Popen("comfy launch -- --cpu --listen 0.0.0.0 --port 8000", shell=True)
@app.function(
max_containers=1,
secrets=[custom_secret]
)
@modal.concurrent(
max_inputs=10
)
@modal.web_server(8000, startup_timeout=60)
def ui_4():
subprocess.Popen("comfy launch -- --cpu --listen 0.0.0.0 --port 8000", shell=True)

View File

@@ -1,98 +0,0 @@
# -*- 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

@@ -1,71 +0,0 @@
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": "保存图片路径"
}

View File

@@ -1,62 +0,0 @@
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", # 节点显示名称
}

View File

@@ -1,3 +1,4 @@
# 用于替换comfy内置api节点
import io
from inspect import cleandoc
from typing import Union, Optional
@@ -1071,7 +1072,6 @@ class FluxProDepthNode(ComfyNodeABC):
# NOTE: names should be globally unique
NODE_CLASS_MAPPINGS = {
"FluxProUltraImageNode": FluxProUltraImageNode,
# "FluxProImageNode": FluxProImageNode,
"FluxKontextProImageNode": FluxKontextProImageNode,
"FluxKontextMaxImageNode": FluxKontextMaxImageNode,
"FluxProExpandNode": FluxProExpandNode,
@@ -1083,7 +1083,6 @@ NODE_CLASS_MAPPINGS = {
# A dictionary that contains the friendly/humanly readable titles for the nodes
NODE_DISPLAY_NAME_MAPPINGS = {
"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",

View File

@@ -1,40 +0,0 @@
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生成器"
}

30
ext/pyproject.toml Normal file
View File

@@ -0,0 +1,30 @@
[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",
"pydantic",
"pydantic_settings",
"conformer==0.3.2",
"einops>0.6.1",
"tqdm",
"retry",
"pyYAML",
"boto3"
]

View File

@@ -1,77 +0,0 @@
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

View File

@@ -1,187 +0,0 @@
# -*- 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": "保存图片(带输出)"
}