11 Commits
12 changed files with 1083 additions and 227 deletions
+3
View File
@@ -18,6 +18,9 @@
- `Batch Image Loop Open SunxAI`:读取图像批次,按索引逐张送出
- `Batch Image Loop Close SunxAI`:接收每次循环结果,自动拼接最终结果
![视频转动漫工作流](./example_workflows/Sun_sample_workflow.png)
📌 **特点:**
- 🚀 **逐帧处理**:每一张图像会被完整处理后,才进入下一张循环(不是在采样时同时执行多帧)
-100
View File
@@ -1,100 +0,0 @@
# ☀️ 如何创建自定义 ComfyUI 节点模块 (`nodes/`)
## 🧩 文件结构约定
每一个节点模块放在 `custom_nodes/comfyui_sun_nodes/nodes/` 目录下,每个 `.py` 文件建议只放一组相关节点。
## 📄 新建节点文件
例如,你想写一个名为 `mynode_node.py` 的节点:
```bash
touch nodes/mynode_node.py
```
## ✨ 在 `mynode_node.py` 中写入如下格式:
```python
class MyNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"default": "Hello"})
}
}
RETURN_TYPES = ("STRING",)
FUNCTION = "run"
CATEGORY = "SunX🌞"
def run(self, text):
return (f"你输入的是: {text}",)
# 固定后缀写法(前缀随意)
mynode_CLASS_MAPPINGS = {
"MyNode": MyNode,
}
mynode_DISPLAY_NAME_MAPPINGS = {
"MyNode": "🌞 My Custom Node",
}
```
## 🧠 命名规范建议
| 内容 | 建议写法 |
| ------------ | ------------------------------------------------------------ |
| 类名 | 使用大驼峰,例如 `MyNode` |
| 内部节点名 | 不含空格、驼峰或下划线 |
| 显示节点名 | 可加 Emoji,中文描述更友好 |
| MAPPINGS 变量名 | `xxx_CLASS_MAPPINGS` 和 `xxx_DISPLAY_NAME_MAPPINGS`,前缀随意,后缀固定 |
---
## 🧩 在 `__init__.py` 中自动导入所有节点
路径:`custom_nodes/comfyui_sun_nodes/__init__.py`
```python
import os
import importlib
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
nodes_path = os.path.join(os.path.dirname(__file__), "nodes")
for filename in os.listdir(nodes_path):
if filename.endswith(".py") and filename != "__init__.py":
module_name = f"{__name__}.nodes.{filename[:-3]}"
module = importlib.import_module(module_name)
for attr in dir(module):
if attr.endswith("_CLASS_MAPPINGS"):
NODE_CLASS_MAPPINGS.update(getattr(module, attr))
elif attr.endswith("_DISPLAY_NAME_MAPPINGS"):
NODE_DISPLAY_NAME_MAPPINGS.update(getattr(module, attr))
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
```
---
## 🧪 示例效果
你创建的节点文件如下:
```bash
custom_nodes/
└── comfyui_sun_nodes/
├── __init__.py
└── nodes/
├── mynode_node.py ✅
└── another_node.py ✅
```
ComfyUI 启动后会自动加载并注册这些节点,不需手动添加字典。
+9 -16
View File
@@ -1,20 +1,13 @@
import os
import importlib
from .nodes.loop_images import BatchImageLoopOpenSun, BatchImageLoopCloseSun
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
NODE_CLASS_MAPPINGS = {
"SunxAI_BatchImageLoopOpenChen": BatchImageLoopOpenSun,
"SunxAI_BatchImageLoopCloseChen": BatchImageLoopCloseSun,
}
nodes_path = os.path.join(os.path.dirname(__file__), "nodes")
for filename in os.listdir(nodes_path):
if filename.endswith(".py") and filename != "__init__.py":
module_name = f"{__name__}.nodes.{filename[:-3]}"
module = importlib.import_module(module_name)
for attr in dir(module):
if attr.endswith("_CLASS_MAPPINGS"):
NODE_CLASS_MAPPINGS.update(getattr(module, attr))
elif attr.endswith("_DISPLAY_NAME_MAPPINGS"):
NODE_DISPLAY_NAME_MAPPINGS.update(getattr(module, attr))
NODE_DISPLAY_NAME_MAPPINGS = {
"SunxAI_BatchImageLoopOpenChen": "Batch Image Loop Open SunxAI",
"SunxAI_BatchImageLoopCloseChen": "Batch Image Loop Close SunxAI",
}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+998
View File
@@ -0,0 +1,998 @@
{
"id": "7cbcec68-7fa6-47bb-a38a-da689949a001",
"revision": 0,
"last_node_id": 301,
"last_link_id": 519,
"nodes": [
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
13.008308410644531,
63.21992492675781
],
"size": [
280.2037048339844,
122.77008056640625
],
"flags": {
"collapsed": false
},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 59
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
237,
514
]
}
],
"title": "CLIP Text Encode (Positive Prompt)",
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.38",
"Node name for S&R": "CLIPTextEncode",
"widget_ue_connectable": {}
},
"widgets_values": [
"Change to anime style while maintaining the original composition"
],
"color": "#232",
"bgcolor": "#353"
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
1239.2454833984375,
68.62554931640625
],
"size": [
190,
46
],
"flags": {
"collapsed": false
},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 52
},
{
"name": "vae",
"type": "VAE",
"link": 61
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
513
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.38",
"Node name for S&R": "VAEDecode",
"widget_ue_connectable": {}
},
"widgets_values": []
},
{
"id": 31,
"type": "KSampler",
"pos": [
833.25634765625,
43.657920837402344
],
"size": [
304.3830871582031,
474
],
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 300
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 464
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 515
},
{
"name": "latent_image",
"type": "LATENT",
"link": 519
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
52
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.38",
"Node name for S&R": "KSampler",
"widget_ue_connectable": {}
},
"widgets_values": [
323524225475238,
"randomize",
20,
1,
"euler",
"simple",
1
]
},
{
"id": 35,
"type": "FluxGuidance",
"pos": [
505.7422790527344,
65.77576446533203
],
"size": [
210,
58
],
"flags": {
"collapsed": false
},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "conditioning",
"type": "CONDITIONING",
"link": 292
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
464
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.38",
"Node name for S&R": "FluxGuidance",
"widget_ue_connectable": {}
},
"widgets_values": [
2.5
]
},
{
"id": 37,
"type": "UNETLoader",
"pos": [
-525.5791015625,
114.7538070678711
],
"size": [
337.76861572265625,
82
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
300
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.38",
"Node name for S&R": "UNETLoader",
"models": [
{
"name": "flux1-dev-kontext_fp8_scaled.safetensors",
"url": "https://huggingface.co/Comfy-Org/flux1-kontext-dev_ComfyUI/resolve/main/split_files/diffusion_models/flux1-dev-kontext_fp8_scaled.safetensors",
"directory": "diffusion_models"
}
],
"widget_ue_connectable": {}
},
"widgets_values": [
"flux1-kontext-dev.safetensors",
"fp8_e4m3fn"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 38,
"type": "DualCLIPLoader",
"pos": [
-525.5791015625,
244.75389099121094
],
"size": [
337.76861572265625,
130
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "CLIP",
"type": "CLIP",
"links": [
59
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.38",
"Node name for S&R": "DualCLIPLoader",
"models": [
{
"name": "clip_l.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors",
"directory": "text_encoders"
},
{
"name": "t5xxl_fp8_e4m3fn_scaled.safetensors",
"url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp8_e4m3fn_scaled.safetensors",
"directory": "text_encoders"
}
],
"widget_ue_connectable": {}
},
"widgets_values": [
"clip_l.safetensors",
"t5xxl_fp8_e4m3fn.safetensors",
"flux",
"default"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 39,
"type": "VAELoader",
"pos": [
-525.5791015625,
424.7539367675781
],
"size": [
337.76861572265625,
58
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "VAE",
"type": "VAE",
"links": [
61,
517
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.38",
"Node name for S&R": "VAELoader",
"models": [
{
"name": "ae.safetensors",
"url": "https://huggingface.co/Comfy-Org/Lumina_Image_2.0_Repackaged/resolve/main/split_files/vae/ae.safetensors",
"directory": "vae"
}
],
"widget_ue_connectable": {}
},
"widgets_values": [
"FLUX/flux_vae.safetensors"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 135,
"type": "ConditioningZeroOut",
"pos": [
543.3994750976562,
167.55950927734375
],
"size": [
240,
26
],
"flags": {
"collapsed": true
},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "conditioning",
"type": "CONDITIONING",
"link": 237
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
515
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.39",
"Node name for S&R": "ConditioningZeroOut",
"widget_ue_connectable": {}
},
"widgets_values": []
},
{
"id": 177,
"type": "ReferenceLatent",
"pos": [
333.2911376953125,
70.3294906616211
],
"size": [
143.41659545898438,
46
],
"flags": {
"collapsed": false
},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "conditioning",
"type": "CONDITIONING",
"link": 514
},
{
"name": "latent",
"shape": 7,
"type": "LATENT",
"link": 518
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
292
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.41",
"Node name for S&R": "ReferenceLatent",
"widget_ue_connectable": {}
},
"widgets_values": []
},
{
"id": 184,
"type": "MarkdownNote",
"pos": [
-1111.9537353515625,
66.36994934082031
],
"size": [
510.0960998535156,
116.33981323242188
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [],
"outputs": [],
"title": "Model links",
"properties": {
"widget_ue_connectable": {}
},
"widgets_values": [
"A单图处理,会将单图处理完毕再进行下一张,不会将多张都卡在采样器,\n\n循环你可以加载视频或者图片序列都行。"
],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 210,
"type": "VHS_LoadVideo",
"pos": [
-550.1968994140625,
655.0445556640625
],
"size": [
252.056640625,
672.0849609375
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "meta_batch",
"shape": 7,
"type": "VHS_BatchManager",
"link": null
},
{
"name": "vae",
"shape": 7,
"type": "VAE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
511
]
},
{
"name": "frame_count",
"type": "INT",
"links": null
},
{
"name": "audio",
"type": "AUDIO",
"links": null
},
{
"name": "video_info",
"type": "VHS_VIDEOINFO",
"links": null
}
],
"properties": {
"aux_id": "upseem/comfyui_sun_nodes",
"ver": "972c87da577b47211c4e9aeed30dc38c7bae607f",
"Node name for S&R": "VHS_LoadVideo",
"cnr_id": "comfyui-videohelpersuite",
"widget_ue_connectable": {}
},
"widgets_values": {
"video": "1.mp4",
"force_rate": 0,
"custom_width": 0,
"custom_height": 0,
"frame_load_cap": 0,
"skip_first_frames": 0,
"select_every_nth": 1,
"format": "AnimateDiff",
"choose video to upload": "image",
"videopreview": {
"hidden": false,
"paused": false,
"params": {
"filename": "1.mp4",
"type": "input",
"format": "video/mp4",
"force_rate": 0,
"custom_width": 0,
"custom_height": 0,
"frame_load_cap": 0,
"skip_first_frames": 0,
"select_every_nth": 1
}
}
}
},
{
"id": 275,
"type": "VHS_VideoCombine",
"pos": [
1217.2215576171875,
634.74658203125
],
"size": [
219.3603515625,
647.04052734375
],
"flags": {},
"order": 14,
"mode": 0,
"inputs": [
{
"label": "images",
"name": "images",
"type": "IMAGE",
"link": 507
},
{
"label": "audio",
"name": "audio",
"shape": 7,
"type": "AUDIO",
"link": null
},
{
"label": "meta_batch",
"name": "meta_batch",
"shape": 7,
"type": "VHS_BatchManager",
"link": null
},
{
"label": "vae",
"name": "vae",
"shape": 7,
"type": "VAE",
"link": null
}
],
"outputs": [
{
"label": "Filenames",
"name": "Filenames",
"type": "VHS_FILENAMES",
"slot_index": 0,
"links": null
}
],
"properties": {
"aux_id": "upseem/comfyui_sun_nodes",
"ver": "1.5.8",
"Node name for S&R": "VHS_VideoCombine",
"cnr_id": "comfyui-videohelpersuite",
"widget_ue_connectable": {}
},
"widgets_values": {
"frame_rate": 30,
"loop_count": 0,
"filename_prefix": "mp4/test",
"format": "video/h264-mp4",
"pix_fmt": "yuv420p",
"crf": 18,
"save_metadata": true,
"trim_to_audio": false,
"pingpong": false,
"save_output": true,
"videopreview": {
"hidden": false,
"paused": false,
"params": {
"filename": "aa_00101.mp4",
"subfolder": "mp4",
"type": "output",
"format": "video/h264-mp4",
"frame_rate": 3,
"workflow": "aa_00101.png",
"fullpath": "/root/autodl-tmp/ComfyUI/output/mp4/aa_00101.mp4"
}
}
}
},
{
"id": 298,
"type": "SunxAI_BatchImageLoopOpenChen",
"pos": [
-136.82192993164062,
654.65673828125
],
"size": [
249.73828125,
86
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "segmented_images",
"type": "IMAGE",
"link": 511
}
],
"outputs": [
{
"name": "FLOW_CONTROL",
"type": "FLOW_CONTROL",
"links": [
503
]
},
{
"name": "current_image",
"type": "IMAGE",
"links": [
516
]
},
{
"name": "max_iterations",
"type": "INT",
"links": [
504
]
},
{
"name": "iteration_count",
"type": "INT",
"links": []
}
],
"properties": {
"aux_id": "upseem/comfyui_sun_nodes",
"ver": "107ce434ed1cebb9dd9d881fd5b2460d4d74db7c",
"Node name for S&R": "SunxAI_BatchImageLoopOpenChen",
"widget_ue_connectable": {}
},
"widgets_values": []
},
{
"id": 299,
"type": "SunxAI_BatchImageLoopCloseChen",
"pos": [
847.4674072265625,
638.669921875
],
"size": [
302.2437438964844,
98
],
"flags": {},
"order": 13,
"mode": 0,
"inputs": [
{
"name": "flow_control",
"type": "FLOW_CONTROL",
"link": 503
},
{
"name": "current_image",
"type": "IMAGE",
"link": 513
},
{
"name": "max_iterations",
"type": "INT",
"link": 504
}
],
"outputs": [
{
"name": "result_images",
"type": "IMAGE",
"links": [
507
]
}
],
"properties": {
"aux_id": "upseem/comfyui_sun_nodes",
"ver": "107ce434ed1cebb9dd9d881fd5b2460d4d74db7c",
"Node name for S&R": "SunxAI_BatchImageLoopCloseChen",
"widget_ue_connectable": {}
},
"widgets_values": [
false
]
},
{
"id": 300,
"type": "VAEEncode",
"pos": [
119.16875457763672,
266.8257141113281
],
"size": [
140,
46
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "pixels",
"type": "IMAGE",
"link": 516
},
{
"name": "vae",
"type": "VAE",
"link": 517
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
518,
519
]
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.3.42",
"Node name for S&R": "VAEEncode",
"widget_ue_connectable": {}
},
"widgets_values": []
}
],
"links": [
[
52,
31,
0,
8,
0,
"LATENT"
],
[
59,
38,
0,
6,
0,
"CLIP"
],
[
61,
39,
0,
8,
1,
"VAE"
],
[
237,
6,
0,
135,
0,
"CONDITIONING"
],
[
292,
177,
0,
35,
0,
"CONDITIONING"
],
[
300,
37,
0,
31,
0,
"MODEL"
],
[
464,
35,
0,
31,
1,
"CONDITIONING"
],
[
503,
298,
0,
299,
0,
"FLOW_CONTROL"
],
[
504,
298,
2,
299,
2,
"INT"
],
[
507,
299,
0,
275,
0,
"IMAGE"
],
[
511,
210,
0,
298,
0,
"IMAGE"
],
[
513,
8,
0,
299,
1,
"IMAGE"
],
[
514,
6,
0,
177,
0,
"CONDITIONING"
],
[
515,
135,
0,
31,
2,
"CONDITIONING"
],
[
516,
298,
1,
300,
0,
"IMAGE"
],
[
517,
39,
0,
300,
1,
"VAE"
],
[
518,
300,
0,
177,
1,
"LATENT"
],
[
519,
300,
0,
31,
3,
"LATENT"
]
],
"groups": [
{
"id": 1,
"title": "Step 1- Load models",
"bounding": [
-535.5791015625,
44.753883361816406,
360,
450
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 3,
"title": "循环执行",
"bounding": [
-588.2969970703125,
564.1963500976562,
2152.915283203125,
792.60986328125
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 6,
"title": "A:处理单张图片流程",
"bounding": [
-578.388916015625,
-9.562874794006348,
2136.165283203125,
532.2862548828125
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 7,
"title": "✅SUNXAI 循环节点",
"bounding": [
-565.919921875,
-272.7617492675781,
2125.043701171875,
224.5164031982422
],
"color": "#a1309b",
"font_size": 160,
"flags": {}
}
],
"config": {},
"extra": {
"ds": {
"scale": 0.45000000000000245,
"offset": [
1211.9537353515625,
56.342079162597656
]
},
"frontendVersion": "1.23.4",
"groupNodes": {},
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true,
"ue_links": [],
"links_added_by_ue": []
},
"version": 0.4
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

+4
View File
@@ -0,0 +1,4 @@
from .loop_images import BatchImageLoopOpenSun, BatchImageLoopCloseSun
__all__ = ["BatchImageLoopOpenSun", "BatchImageLoopCloseSun"]
+58 -44
View File
@@ -1,5 +1,11 @@
import os
import uuid
from PIL import Image
import numpy as np
from comfy_execution.graph_utils import GraphBuilder, is_link
import folder_paths
from ..tools.tools import VariantSupport
import torch.nn.functional as F
import torch
@@ -16,17 +22,19 @@ class BatchImageLoopOpenSun:
inputs = {
"required": {
"segmented_images": ("IMAGE", {"forceInput": True}),
"output_dir": ("STRING", {"default": ""}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"iteration_count": ("INT", {"default": 0}),
"previous_image": ("IMAGE",),
"batch_id": ("STRING",),
}
}
return inputs
RETURN_TYPES = ("FLOW_CONTROL", "IMAGE", "INT", "INT")
RETURN_NAMES = ("FLOW_CONTROL", "current_image", "max_iterations", "iteration_count")
RETURN_TYPES = ("FLOW_CONTROL", "IMAGE", "INT", "INT", "STRING")
RETURN_NAMES = ("FLOW_CONTROL", "current_image", "max_iterations", "iteration_count", "batch_path")
FUNCTION = "while_loop_open"
CATEGORY = "CyberEveLoop🐰·Chen定制"
@@ -47,12 +55,25 @@ class BatchImageLoopOpenSun:
image = image.permute(0, 2, 3, 1)
return image
def while_loop_open(self, segmented_images, unique_id=None, iteration_count=0, previous_image=None):
def while_loop_open(self, segmented_images, output_dir="", unique_id=None, iteration_count=0, previous_image=None, batch_id=None):
print(f"[chen] Loop iteration: {iteration_count}")
images = self.standardize_images(segmented_images)
max_iterations = images.shape[0]
if batch_id is None:
batch_id = uuid.uuid4().hex[:8]
base_output = folder_paths.get_output_directory()
if output_dir:
batch_path = os.path.join(base_output, output_dir, batch_id)
else:
batch_path = os.path.join(base_output, "loop_batch", batch_id)
if iteration_count == 0:
os.makedirs(batch_path, exist_ok=True)
print(f"[chen] Created batch directory: {batch_path}")
if iteration_count >= max_iterations:
raise ValueError(f"[chen] Iteration {iteration_count} exceeds max {max_iterations}")
@@ -62,7 +83,7 @@ class BatchImageLoopOpenSun:
images[idx:idx+1] = previous_image
current_image = images[iteration_count:iteration_count+1]
return ("stub", current_image, max_iterations, iteration_count)
return ("stub", current_image, max_iterations, iteration_count, batch_path)
@VariantSupport()
@@ -77,6 +98,8 @@ class BatchImageLoopCloseSun:
"flow_control": ("FLOW_CONTROL", {"rawLink": True}),
"current_image": ("IMAGE",),
"max_iterations": ("INT", {"forceInput": True}),
"iteration_count": ("INT", {"forceInput": True}),
"batch_path": ("STRING", {"forceInput": True}),
},
"optional": {
"pass_back": ("BOOLEAN", {"default": False}),
@@ -84,13 +107,11 @@ class BatchImageLoopCloseSun:
"hidden": {
"dynprompt": "DYNPROMPT",
"unique_id": "UNIQUE_ID",
"result_images": ("IMAGE",),
"iteration_count": ("INT", {"default": 0}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("result_images",)
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("result_images", "output_path")
FUNCTION = "while_loop_close"
CATEGORY = "CyberEveLoop🐰·Chen定制"
@@ -100,37 +121,44 @@ class BatchImageLoopCloseSun:
assert len(image.shape) == 4, f"Image must be 4D [B,H,W,C], got {image.shape}"
return image
def initialize_results(self, max_iterations, current_image):
assert len(current_image.shape) == 4
return torch.zeros(
(max_iterations, *current_image.shape[1:]),
dtype=current_image.dtype,
device=current_image.device
)
def save_image(self, image, batch_path, index):
filepath = os.path.join(batch_path, f"{index:05d}.png")
if os.path.exists(filepath):
print(f"[chen] Skip (exists): {filepath}")
return
img_np = (image.squeeze(0).cpu().numpy() * 255).clip(0, 255).astype(np.uint8)
img_pil = Image.fromarray(img_np)
img_pil.save(filepath)
print(f"[chen] Saved: {filepath}")
def while_loop_close(self, flow_control, current_image, max_iterations,
pass_back=False, iteration_count=0,
result_images=None, dynprompt=None, unique_id=None):
def load_all_images(self, batch_path, max_iterations, device):
results = []
for i in range(max_iterations):
filepath = os.path.join(batch_path, f"{i:05d}.png")
img_pil = Image.open(filepath).convert("RGB")
img_np = np.array(img_pil).astype(np.float32) / 255.0
img_tensor = torch.from_numpy(img_np).unsqueeze(0)
results.append(img_tensor)
return torch.cat(results, dim=0).to(device)
def while_loop_close(self, flow_control, current_image, max_iterations, batch_path,
iteration_count, pass_back=False,
dynprompt=None, unique_id=None):
print(f"[chen] Iteration {iteration_count} / {max_iterations}")
current_image = self.standardize_image(current_image)
device = current_image.device
if iteration_count >= max_iterations:
raise ValueError(f"[chen] Iteration {iteration_count} exceeds max {max_iterations}")
if result_images is None:
result_images = self.initialize_results(max_iterations, current_image)
else:
assert result_images.shape[0] == max_iterations
result_images[iteration_count:iteration_count+1] = current_image
self.save_image(current_image, batch_path, iteration_count)
if iteration_count == max_iterations - 1:
print(f"[chen] Loop finished")
return (result_images,)
print(f"[chen] Loop finished, loading results from {batch_path}")
result_images = self.load_all_images(batch_path, max_iterations, device)
return (result_images, batch_path)
# 构建图用于下一轮迭代
this_node = dynprompt.get_node(unique_id)
open_node = flow_control[0]
upstream = {}
@@ -175,15 +203,15 @@ class BatchImageLoopCloseSun:
my_clone = graph.lookup_node("Recurse")
my_clone.set_input("iteration_count", iteration_count + 1)
my_clone.set_input("result_images", result_images)
new_open = graph.lookup_node(open_node)
new_open.set_input("iteration_count", iteration_count + 1)
new_open.set_input("batch_id", os.path.basename(batch_path))
if pass_back:
new_open.set_input("previous_image", current_image)
return {
"result": (my_clone.out(0),),
"result": (my_clone.out(0), my_clone.out(1)),
"expand": graph.finalize()
}
@@ -222,17 +250,3 @@ class BatchImageLoopCloseSun:
if child_id not in contained:
contained[child_id] = True
self.collect_contained(child_id, upstream, contained)
Loop_CLASS_MAPPINGS = {
"SunxAI_BatchImageLoopOpenChen": BatchImageLoopOpenSun,
"SunxAI_BatchImageLoopCloseChen": BatchImageLoopCloseSun,
}
Loop_DISPLAY_NAME_MAPPINGS = {
"SunxAI_BatchImageLoopOpenChen": "Batch Image Loop Open SunxAI",
"SunxAI_BatchImageLoopCloseChen": "Batch Image Loop Close SunxAI",
}
+11 -35
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "comfyui_sun_nodes"
version = "0.0.4"
version = "0.1.6"
description = "Custom ComfyUI nodes by SunX.AI"
authors = [
{name = "SunX AI", email = "sunx.ai@hotmail.com"}
@@ -12,70 +12,46 @@ authors = [
readme = "README.md"
license = {text = "MIT license"}
classifiers = []
dependencies = [
]
dependencies = []
[project.optional-dependencies]
dev = [
"bump-my-version",
"coverage", # testing
"mypy", # linting
"pre-commit", # runs linting on commit
"pytest", # testing
"ruff", # linting
"mypy",
"pre-commit",
"ruff",
]
[project.urls]
bugs = "https://github.com/upseem/comfyui_sun_nodes/issues"
homepage = "https://github.com/upseem/comfyui_sun_nodes"
Repository = "https://github.com/upseem/comfyui_sun_nodes"
[tool.comfy]
PublisherId = "sunxai"
DisplayName = "comfyui_Sun_nodes"
DisplayName = "ComfyUI Sun Nodes"
Icon = "https://avatars.githubusercontent.com/u/124853686"
[tool.setuptools.package-data]
"*" = ["*.*"]
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = [
"tests",
]
[tool.mypy]
files = "."
# Use strict defaults
strict = true
warn_unreachable = true
warn_no_return = true
[[tool.mypy.overrides]]
# Don't require test functions to include types
module = "tests.*"
allow_untyped_defs = true
disable_error_code = "attr-defined"
[tool.ruff]
# extend-exclude = ["static", "ci/templates"]
line-length = 140
src = ["src", "tests"]
src = ["nodes", "tools"]
target-version = "py39"
# Add rules to ban exec/eval
[tool.ruff.lint]
select = [
"S102", # exec-builtin
"S307", # eval-used
"S102",
"S307",
"W293",
"F", # The "F" series in Ruff stands for "Pyflakes" rules, which catch various Python syntax errors and undefined names.
# See all rules here: https://docs.astral.sh/ruff/rules/#pyflakes-f
"F",
]
[tool.ruff.lint.flake8-quotes]
-1
View File
@@ -1 +0,0 @@
"""Unit test package for comfyui_sun_nodes."""
-6
View File
@@ -1,6 +0,0 @@
import os
import sys
# Add the project root directory to Python path
# This allows the tests to import the project
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
-4
View File
@@ -1,4 +0,0 @@
[pytest]
testpaths = . # Run tests in the current directory
python_files = test_*.py # Run tests in files that start with "test_"
norecursedirs = .. # Don't run tests in the parent directory
-21
View File
@@ -1,21 +0,0 @@
#!/usr/bin/env python
"""Tests for `comfyui_sun_nodes` package."""
import pytest
from src.comfyui_sun_nodes.nodes import Example
@pytest.fixture
def example_node():
"""Fixture to create an Example node instance."""
return Example()
def test_example_node_initialization(example_node):
"""Test that the node can be instantiated."""
assert isinstance(example_node, Example)
def test_return_types():
"""Test the node's metadata."""
assert Example.RETURN_TYPES == ("IMAGE",)
assert Example.FUNCTION == "test"
assert Example.CATEGORY == "Example"