Delete custom_nodes
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- custom_nodes/Cheny_custom_nodes/yolov8_person_detect.py +0 -188
- custom_nodes/ComfyUI-AK-Pack/.gitignore +0 -18
- custom_nodes/ComfyUI-AK-Pack/README.md +0 -183
- custom_nodes/ComfyUI-AK-Pack/__init__.py +0 -109
- custom_nodes/ComfyUI-AK-Pack/icon.jpg +0 -0
- custom_nodes/ComfyUI-AK-Pack/nodes/AKBase.py +0 -248
- custom_nodes/ComfyUI-AK-Pack/nodes/AKCLIPEncodeMultiple.py +0 -243
- custom_nodes/ComfyUI-AK-Pack/nodes/AKContrastAndSaturateImage.py +0 -281
- custom_nodes/ComfyUI-AK-Pack/nodes/AKControlMultipleKSamplers.py +0 -46
- custom_nodes/ComfyUI-AK-Pack/nodes/AKIndexMultiple.py +0 -72
- custom_nodes/ComfyUI-AK-Pack/nodes/AKKSamplerSettings.py +0 -152
- custom_nodes/ComfyUI-AK-Pack/nodes/AKPipeLoop.py +0 -184
- custom_nodes/ComfyUI-AK-Pack/nodes/AKProjectSettingsOut.py +0 -178
- custom_nodes/ComfyUI-AK-Pack/nodes/AKReplaceAlphaWithColor.py +0 -201
- custom_nodes/ComfyUI-AK-Pack/nodes/AKReplaceColorWithAlpha.py +0 -153
- custom_nodes/ComfyUI-AK-Pack/nodes/CLIPTextEncodeAndCombineCached.py +0 -68
- custom_nodes/ComfyUI-AK-Pack/nodes/CLIPTextEncodeCached.py +0 -50
- custom_nodes/ComfyUI-AK-Pack/nodes/Getter.py +0 -38
- custom_nodes/ComfyUI-AK-Pack/nodes/IsOneOfGroupsActive.py +0 -43
- custom_nodes/ComfyUI-AK-Pack/nodes/PreviewRawText.py +0 -99
- custom_nodes/ComfyUI-AK-Pack/nodes/RepeatGroupState.py +0 -35
- custom_nodes/ComfyUI-AK-Pack/nodes/Setter.py +0 -136
- custom_nodes/ComfyUI-AK-Pack/pyproject.toml +0 -28
- custom_nodes/ComfyUI-Addoor/classes/AD_AnyFileList.py +0 -88
- custom_nodes/ComfyUI-Addoor/classes/AD_BatchImageLoadFromDir.py +0 -52
- custom_nodes/ComfyUI-Addoor/classes/AD_LoadImageAdvanced.py +0 -87
- custom_nodes/ComfyUI-Addoor/classes/AD_PromptReplace.py +0 -59
- custom_nodes/ComfyUI-Addoor/example_workflow/RunningHub API.json +0 -500
- custom_nodes/ComfyUI-Addoor/nodes/AddPaddingAdvanced.py +0 -435
- custom_nodes/ComfyUI-Addoor/nodes/AddPaddingBase.py +0 -99
- custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-FofrToolkit.py +0 -126
- custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-imageResize.py +0 -172
- custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-textAppend.py +0 -48
- custom_nodes/ComfyUI-Addoor/nodes/__init__.py +0 -62
- custom_nodes/ComfyUI-Addoor/nodes/imagecreatemask.py +0 -109
- custom_nodes/ComfyUI-Addoor/nodes/multiline_string.py +0 -42
- custom_nodes/ComfyUI-Addoor/rp/RunningHubFileSaver.py +0 -183
- custom_nodes/ComfyUI-Addoor/rp/RunningHubInit.py +0 -22
- custom_nodes/ComfyUI-Addoor/rp/RunningHubNodeInfo.py +0 -45
- custom_nodes/ComfyUI-Addoor/rp/RunningHubWorkflowExecutor.py +0 -151
- custom_nodes/ComfyUI-Addoor/rp/config.ini +0 -6
- custom_nodes/ComfyUI-Copilot/backend/agent_factory.py +0 -98
- custom_nodes/ComfyUI-Copilot/backend/controller/conversation_api.py +0 -1083
- custom_nodes/ComfyUI-Copilot/backend/controller/expert_api.py +0 -192
- custom_nodes/ComfyUI-Copilot/backend/core.py +0 -23
- custom_nodes/ComfyUI-Copilot/backend/dao/__init__.py +0 -0
- custom_nodes/ComfyUI-Copilot/backend/dao/workflow_table.py +0 -183
- custom_nodes/ComfyUI-Copilot/backend/logs/comfyui_copilot.log +0 -0
- custom_nodes/ComfyUI-Copilot/backend/service/__init__.py +0 -0
- custom_nodes/ComfyUI-Copilot/backend/service/link_agent_tools.py +0 -467
custom_nodes/Cheny_custom_nodes/yolov8_person_detect.py
DELETED
|
@@ -1,188 +0,0 @@
|
|
| 1 |
-
import copy
|
| 2 |
-
import os
|
| 3 |
-
|
| 4 |
-
import torch
|
| 5 |
-
from PIL import Image
|
| 6 |
-
import glob
|
| 7 |
-
import numpy as np
|
| 8 |
-
from ultralytics import YOLO
|
| 9 |
-
import folder_paths
|
| 10 |
-
|
| 11 |
-
models_dirs=os.path.join(folder_paths.models_dir,'yolo')
|
| 12 |
-
def get_files(models_dir,file_exp_list):
|
| 13 |
-
file_list=[]
|
| 14 |
-
for exp in file_exp_list:
|
| 15 |
-
# extend可将可迭代对象中的元素,逐个添加进当前列表
|
| 16 |
-
file_list.extend(glob.glob(os.path.join(models_dir,'*'+exp))) #glob就是模式匹配文件,支持通配符,每个文件返回一个列表,所以用extend
|
| 17 |
-
file_dict={}
|
| 18 |
-
for i in range(len(file_list)):
|
| 19 |
-
_,filename=os.path.split(file_list[i]) # 将文件路径拆成("C:/models/yolo", "model1.pt")
|
| 20 |
-
# 创建文件名到文件路径的映射
|
| 21 |
-
file_dict[filename]=file_list[i]
|
| 22 |
-
|
| 23 |
-
return file_dict
|
| 24 |
-
|
| 25 |
-
def tensor2pil(image):
|
| 26 |
-
if isinstance(image,Image.Image):
|
| 27 |
-
return image
|
| 28 |
-
else:
|
| 29 |
-
if len(image.shape)<3:
|
| 30 |
-
image=image.unsqueeze(0)
|
| 31 |
-
return Image.fromarray((image[0].cpu().numpy()*255).astype(np.uint8))
|
| 32 |
-
|
| 33 |
-
def pil2tensor(image):
|
| 34 |
-
new_image=image.convert('RGB')
|
| 35 |
-
image_array=np.array(new_image).astype(np.float32)/255.0
|
| 36 |
-
image_tensor=torch.tensor(image_array)
|
| 37 |
-
return image_tensor
|
| 38 |
-
|
| 39 |
-
def mask2tensor(mask):
|
| 40 |
-
mask=mask.convert('L')
|
| 41 |
-
mask_array=np.array(mask).astype(np.float32)/255.0
|
| 42 |
-
mask_tensor=torch.tensor(mask_array)
|
| 43 |
-
return mask_tensor
|
| 44 |
-
|
| 45 |
-
class Yolov8_person_detect:
|
| 46 |
-
CATEGORY="My Nodes/yolov8 person detect"
|
| 47 |
-
RETURN_TYPES=("MASK",)
|
| 48 |
-
RETURN_NAMES=("back_mask",)
|
| 49 |
-
FUNCTION="yolov8_person_detect"
|
| 50 |
-
|
| 51 |
-
@classmethod
|
| 52 |
-
def INPUT_TYPES(cls):
|
| 53 |
-
model_exp=["seg.pt"]
|
| 54 |
-
FILES_DICT=get_files(models_dirs,model_exp)
|
| 55 |
-
FILE_LIST=list(FILES_DICT.keys())
|
| 56 |
-
|
| 57 |
-
return{
|
| 58 |
-
"required":{
|
| 59 |
-
"back_image":("IMAGE",),
|
| 60 |
-
"mask":("MASK",),
|
| 61 |
-
"yolo_model":(FILE_LIST,),
|
| 62 |
-
},
|
| 63 |
-
"optional":{
|
| 64 |
-
"true_rate":("FLOAT",{
|
| 65 |
-
"default":0.85,
|
| 66 |
-
"min":0.01,
|
| 67 |
-
"max":1.0,
|
| 68 |
-
"step":0.01,
|
| 69 |
-
"display":"number"
|
| 70 |
-
}),
|
| 71 |
-
"img_ratio":("FLOAT",{
|
| 72 |
-
"default":float(2/3),
|
| 73 |
-
"min":0.01,
|
| 74 |
-
"max":1.0,
|
| 75 |
-
"step":0.01,
|
| 76 |
-
"display":"number"
|
| 77 |
-
}),
|
| 78 |
-
"x_ratio":("FLOAT",{
|
| 79 |
-
"default":float(0.5),
|
| 80 |
-
"min":0.01,
|
| 81 |
-
"max":1.0,
|
| 82 |
-
"step":0.01,
|
| 83 |
-
"display":"number"
|
| 84 |
-
}),
|
| 85 |
-
"y_ratio":("FLOAT",{
|
| 86 |
-
"default":float(1/10),
|
| 87 |
-
"min":0.01,
|
| 88 |
-
"max":1.0,
|
| 89 |
-
"step":0.01,
|
| 90 |
-
"display":"number"
|
| 91 |
-
})
|
| 92 |
-
}
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
def yolov8_person_detect(self,mask,back_image,yolo_model,true_rate,img_ratio,x_ratio,y_ratio):
|
| 96 |
-
back_image=tensor2pil(back_image)
|
| 97 |
-
|
| 98 |
-
yolo_model=YOLO(os.path.join(models_dirs,yolo_model))
|
| 99 |
-
result = yolo_model(
|
| 100 |
-
back_image,
|
| 101 |
-
retina_masks=True,
|
| 102 |
-
classes=[0],
|
| 103 |
-
conf=true_rate,
|
| 104 |
-
verbose=False
|
| 105 |
-
)[0]
|
| 106 |
-
|
| 107 |
-
if result.masks is not None and len(result.masks)>0:
|
| 108 |
-
if result.boxes is not None and len(result.boxes)>0:
|
| 109 |
-
if result.boxes.conf[0]>=true_rate:
|
| 110 |
-
masks_data=result.masks.data
|
| 111 |
-
n_mask=masks_data[0]
|
| 112 |
-
n_mask=n_mask.unsqueeze(0)
|
| 113 |
-
return(n_mask,)
|
| 114 |
-
|
| 115 |
-
# 直接生成mask,按背景 2/3
|
| 116 |
-
# bd_w,bd_h=back_image.size
|
| 117 |
-
# n_mask=Image.new('L',(bd_w,bd_h),"black")
|
| 118 |
-
# target=img_ratio*max(bd_h,bd_w)
|
| 119 |
-
|
| 120 |
-
# new_w,new_h=int(img_ratio*bd_w),int(img_ratio*bd_h)
|
| 121 |
-
|
| 122 |
-
# paste_mask=Image.new('L',(new_w,new_h),"white")
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
# x=int(bd_w-new_w)
|
| 126 |
-
# y=int(bd_h-new_h)
|
| 127 |
-
|
| 128 |
-
# x_trap_padding=int(x*x_ratio)
|
| 129 |
-
# x=x-x_trap_padding
|
| 130 |
-
|
| 131 |
-
# y_trap_padding=int(y*y_ratio)
|
| 132 |
-
# y=y-y_trap_padding
|
| 133 |
-
|
| 134 |
-
# n_mask.paste(paste_mask,(x,y))
|
| 135 |
-
# n_mask=mask2tensor(n_mask)
|
| 136 |
-
|
| 137 |
-
# if len(n_mask.shape)<3:
|
| 138 |
-
# n_mask=n_mask.unsqueeze(0)
|
| 139 |
-
# print("Yolov8_person_detect done")
|
| 140 |
-
# return (n_mask,)
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
# 利用原图人物mask,生成新mask
|
| 144 |
-
mask=tensor2pil(mask).convert('L')
|
| 145 |
-
|
| 146 |
-
bd_w,bd_h=back_image.size
|
| 147 |
-
|
| 148 |
-
n_mask=Image.new('L',(bd_w,bd_h),"black")
|
| 149 |
-
target=img_ratio*max(bd_h,bd_w)
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
m_bbox=mask.getbbox()
|
| 153 |
-
|
| 154 |
-
mask=mask.crop(m_bbox)
|
| 155 |
-
|
| 156 |
-
m_w,m_h=mask.size
|
| 157 |
-
ratio=target/max(m_w,m_h)
|
| 158 |
-
new_w,new_h=int(ratio*m_w),int(ratio*m_h)
|
| 159 |
-
mask=mask.resize((new_w,new_h),Image.LANCZOS)
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
if new_w>=bd_w or new_h>=bd_h:
|
| 163 |
-
raise ValueError(f'缩放图片的长宽超过背景图大小,请下调img_ratio值')
|
| 164 |
-
|
| 165 |
-
x=int(bd_w-new_w)
|
| 166 |
-
y=int(bd_h-new_h)
|
| 167 |
-
|
| 168 |
-
x_trap_padding=int(x*x_ratio)
|
| 169 |
-
x=x-x_trap_padding
|
| 170 |
-
|
| 171 |
-
y_trap_padding=int(y*y_ratio)
|
| 172 |
-
y=y-y_trap_padding
|
| 173 |
-
|
| 174 |
-
n_mask.paste(mask,(x,y))
|
| 175 |
-
n_mask=mask2tensor(n_mask)
|
| 176 |
-
|
| 177 |
-
if len(n_mask.shape)<3:
|
| 178 |
-
n_mask=n_mask.unsqueeze(0)
|
| 179 |
-
print("Yolov8_person_detect done")
|
| 180 |
-
return (n_mask,)
|
| 181 |
-
|
| 182 |
-
NODE_CLASS_MAPPINGS={
|
| 183 |
-
"Yolov8_person_detect":Yolov8_person_detect
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
NODE_DISPLAY_NAME_MAPPINGS={
|
| 187 |
-
"Yolov8_person_detect":"Yolov8_person_detect(My Node)"
|
| 188 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/.gitignore
DELETED
|
@@ -1,18 +0,0 @@
|
|
| 1 |
-
# Python bytecode
|
| 2 |
-
__pycache__/
|
| 3 |
-
*.pyc
|
| 4 |
-
*.pyo
|
| 5 |
-
*.pyd
|
| 6 |
-
|
| 7 |
-
# Virtual environment
|
| 8 |
-
.venv/
|
| 9 |
-
venv/
|
| 10 |
-
env/
|
| 11 |
-
|
| 12 |
-
# Editor/IDE specific files
|
| 13 |
-
.idea/
|
| 14 |
-
.vscode/
|
| 15 |
-
|
| 16 |
-
# Operating System files
|
| 17 |
-
.DS_Store
|
| 18 |
-
Thumbs.db
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/README.md
DELETED
|
@@ -1,183 +0,0 @@
|
|
| 1 |
-
#### Other My Nodes
|
| 2 |
-
- 📘 [ComfyUI-AK-Pack](https://github.com/akawana/ComfyUI-AK-Pack)
|
| 3 |
-
- 📘 [ComfyUI-AK-XZ-Axis](https://github.com/akawana/ComfyUI-AK-XZ-Axis)
|
| 4 |
-
- 📘 [ComfyUI RGBYP Mask Editor](https://github.com/akawana/ComfyUI-RGBYP-Mask-Editor)
|
| 5 |
-
- 📘 [ComfyUI Folded Prompts](https://github.com/akawana/ComfyUI-Folded-Prompts)
|
| 6 |
-
|
| 7 |
-
# ComfyUI AK Pack
|
| 8 |
-
This is a pack of useful ComfyUI nodes and UI extensions. It was created for **complex and large** workflows. The main goal of this pack is to unify and simplify working with my other packs: 📘 [ComfyUI-AK-XZ-Axis](https://github.com/akawana/ComfyUI-AK-XZ-Axis), 📘 [ComfyUI RGBYP Mask Editor](https://github.com/akawana/ComfyUI-RGBYP-Mask-Editor), 📘 [ComfyUI Folded Prompts](https://github.com/akawana/ComfyUI-Folded-Prompts).
|
| 9 |
-
|
| 10 |
-
The most interesting parts of this pack for most users are:
|
| 11 |
-
- Multiple Samplers Control
|
| 12 |
-
- Project Settings
|
| 13 |
-
- AK Base
|
| 14 |
-
|
| 15 |
-
The other nodes are also useful, but they are of secondary importance. You should be able to figure them out on your own.
|
| 16 |
-
|
| 17 |
-
I have also created example workflows:
|
| 18 |
-
|
| 19 |
-
---
|
| 20 |
-
#### UI: Multiple Samplers Control
|
| 21 |
-
|
| 22 |
-
This is not a node in the traditional sense, but a **ComfyUI UI extension**. The panel allows you to control multiple KSamplers or Detailers from a single place.
|
| 23 |
-
|
| 24 |
-
In my workflows, I often use 6–8 KSamplers and Detailers at the same time. I spent a long time looking for a fast and centralized way to control them, and this panel is the result of that effort.
|
| 25 |
-
|
| 26 |
-
The panel is integrated into the left toolbar of ComfyUI and can be disabled in Settings.
|
| 27 |
-
|
| 28 |
-
The panel includes a Settings tab where you configure, using comma-separated lists, which KSampler nodes you want to control. You can specify node names or node IDs in these lists.
|
| 29 |
-
|
| 30 |
-
For example: "KSampler, 12, 33, My Detailer, BlaBlaSampler" - will find all the samplers mentioned.
|
| 31 |
-
|
| 32 |
-
Once configured, the detected nodes will appear in a dropdown list in the Control tab, allowing you to adjust the settings of your KSamplers or Detailers from a single centralized interface.
|
| 33 |
-
|
| 34 |
-
| [](./img/MSamplers.jpg) | [](./img/MSamplersSettings.jpg) | [](./img/MSamplersChooser.jpg) |
|
| 35 |
-
|---|---|---|
|
| 36 |
-
|
| 37 |
-
---
|
| 38 |
-
#### UI: Project Settings
|
| 39 |
-
|
| 40 |
-
This is not a node in the strict sense. It is a **UI extension** — a panel that allows you to quickly enter the main parameters of your project.
|
| 41 |
-
The panel is embedded into the left toolbar of ComfyUI and can be disabled in the settings.
|
| 42 |
-
|
| 43 |
-
I work a lot with img2img and often use fairly large workflows. I got tired of constantly navigating to different parts of the workflow every time I needed to adjust input parameters, so I created this panel. Now I can load an image and enter all the required values in one place.
|
| 44 |
-
|
| 45 |
-
All panel data is stored directly in the workflow graph, except for the image itself. **The image** is copied to the **input/garbage/** folder. If you delete this folder later, nothing bad will happen — you will simply need to select the image again.
|
| 46 |
-
|
| 47 |
-
To retrieve the data from this panel, I created a separate node called **AKProjectSettingsOut**. Just place it anywhere you need in your workflow to access the panel values.
|
| 48 |
-
|
| 49 |
-
| [](./img/ProjectSettings.jpg) | [](./img/ProjectSettingsOptions.jpg) | [](./img/AKProjectSettingsOut.jpg) |
|
| 50 |
-
|---|---|---|
|
| 51 |
-
|
| 52 |
-
---
|
| 53 |
-
#### Node: AK Base
|
| 54 |
-
|
| 55 |
-
This node is designed as a central hub for working within a workflow, which is why it is called AK Base. At its core, it provides a preview-based comparison of two images. Similar nodes already exist, but this one adds support for a gallery of multiple images.
|
| 56 |
-
|
| 57 |
-
In addition, it includes:
|
| 58 |
-
- a button to copy a single image to the clipboard,
|
| 59 |
-
- a button to copy the entire gallery to the clipboard,
|
| 60 |
-
- a dialog that allows you to inspect the results while working in a different part of the workflow.
|
| 61 |
-
|
| 62 |
-
In the Properties panel, this node provides a setting called node_list.
|
| 63 |
-
This is a text field where you can specify a list of nodes, using either node names or node IDs.
|
| 64 |
-
|
| 65 |
-
Once configured, AK Base can automatically adjust the parameters seed, denoise, cfg, and xz_steps based on the image selected in the gallery.
|
| 66 |
-
This functionality was implemented specifically to support my 📘 [ComfyUI-AK-XZ-Axis](https://github.com/akawana/ComfyUI-AK-XZ-Axis) nodes.
|
| 67 |
-
|
| 68 |
-
| [](./img/AKBase.jpg) | [](./img/AKBaseGallery.jpg) | [](./img/AKBasePIP.jpg) |
|
| 69 |
-
|---|---|---|
|
| 70 |
-
|
| 71 |
-
---
|
| 72 |
-
#### Node: Setter & Getter
|
| 73 |
-
|
| 74 |
-
These nodes already exist in several other packs. My goal was to make them faster. In my implementation, the nodes do not use JavaScript to store or pass data. All data is passed only through Python and direct connections between nodes. Simply put, they hide links and hide outputs and inputs.
|
| 75 |
-
|
| 76 |
-
In my setup, JavaScript is responsible only for updating the list of variables and does not affect the Run process in any way. Based on my comparisons, in complex workflows with 20–30 Getter/Setter nodes, my nodes perform much faster.
|
| 77 |
-
|
| 78 |
-
---
|
| 79 |
-
#### Node: Repeat Group State
|
| 80 |
-
|
| 81 |
-
A connection-free interactive node that synchronizes the state of its own group with the state of other groups matching a given substring. This allows groups to depend on other groups without wires, similar to rgthree repeaters.
|
| 82 |
-
|
| 83 |
-
- Finds groups with names containing the target substring.
|
| 84 |
-
- Checks whether any of them are Active.
|
| 85 |
-
- If **all** matching groups are disabled -> it disables **its own group**.
|
| 86 |
-
- If **any** matching group is active -> it enables **its own group**.
|
| 87 |
-
|
| 88 |
-
<img src="./img/preview_repeater.jpg" height="200"/>
|
| 89 |
-
|
| 90 |
-
---
|
| 91 |
-
#### Node: IsOneOfGroupsActive
|
| 92 |
-
|
| 93 |
-
Checks the state of all groups whose names **contain a specified substring**.
|
| 94 |
-
|
| 95 |
-
- If **at least one** matching group is Active -> output is `true`.
|
| 96 |
-
- If **all** matching groups are Muted/Bypassed -> output is `false`.
|
| 97 |
-
|
| 98 |
-
<img src="./img/preview_is_group_active.jpg" height="200"/>
|
| 99 |
-
|
| 100 |
-
---
|
| 101 |
-
#### Node: AK Index Multiple
|
| 102 |
-
|
| 103 |
-
Extracts a specific range from any **List** (images, masks, latents, text etc..) and creates individual outputs for that range.
|
| 104 |
-
Optionally replaces missing values with a fallback (`if_none`).
|
| 105 |
-
|
| 106 |
-
<img src="./img/AKIndexMultiple.jpg" height="200">
|
| 107 |
-
|
| 108 |
-
---
|
| 109 |
-
#### Node: AK CLIP Encode Multiple
|
| 110 |
-
|
| 111 |
-
Extracts a specific range from any **List** of strings and CLIP encodes individual outputs for that range.
|
| 112 |
-
|
| 113 |
-
Same as **AK Index Multiple** but for CLIP encoding. Works faster than regular CLIP Encoders because does only one encoding for all NONE input strings. Caches the stings and does not encode them if no changes. Also outputs combined conditioning.
|
| 114 |
-
|
| 115 |
-
<img src="./img/AKCLIPEncodeMultiple.jpg" height="200">
|
| 116 |
-
|
| 117 |
-
---
|
| 118 |
-
#### Node: AK KSampler Settings & AK KSampler Settings Out
|
| 119 |
-
|
| 120 |
-
Allows you to define sampler settings anywhere in the workflow.
|
| 121 |
-
The Out node lets you retrieve these settings wherever they are needed.
|
| 122 |
-
|
| 123 |
-
- `Seed`, `Sampler`, `Scheduler`, `Steps`, `Cfg`, `Denoise`, `xz_steps`
|
| 124 |
-
|
| 125 |
-
> [!NOTE]
|
| 126 |
-
> This node includes an additional field called xz_steps.
|
| 127 |
-
> It is used to control the number of steps when working with my 📘 [ComfyUI-AK-XZ-Axis](https://github.com/akawana/ComfyUI-AK-XZ-Axis) nodes.
|
| 128 |
-
> This field is also convenient when used together with the **AK Base** node, because it can automatically set xz_steps = 1 when a single image is selected in the gallery.
|
| 129 |
-
|
| 130 |
-
---
|
| 131 |
-
#### Node: AK Replace Alpha With Color & AK Replace Color With Alpha
|
| 132 |
-
|
| 133 |
-
`AK Replace Alpha With Color` Replaces alpha with a color.
|
| 134 |
-
|
| 135 |
-
`AK Replace Color With Alpha` Performs the reverse operation: Replaces a color with alpha. You can specify the color manually, or use automatic modes that detect the color from pixels in different corners of the image. Image **cropping** is also available. This is useful because when rendering images on a flat background, the outermost pixels around the edges often have color artifacts and are better removed.
|
| 136 |
-
|
| 137 |
-
> [!NOTE]
|
| 138 |
-
> Both of these nodes are useful when working with images that have a transparent background. You can first replace alpha with a color, perform the generation, and then, before saving, remove that color and convert it back to alpha.
|
| 139 |
-
|
| 140 |
-
<img src="./img/AKReplaceAlphaWithColor.jpg" height="200">
|
| 141 |
-
|
| 142 |
-
---
|
| 143 |
-
#### Other nodes
|
| 144 |
-
|
| 145 |
-
##### Node: AK Pipe & AK Pipe Loop
|
| 146 |
-
|
| 147 |
-
AK Pipe is a zero-copy pipeline node that passes a structured pipe object through the graph and updates only explicitly connected inputs. It avoids unnecessary allocations by reusing the original pipe when no values change and creates a new container only on real object replacement. This design minimizes memory churn and Python overhead, making it significantly faster than traditional pipe merge nodes.
|
| 148 |
-
|
| 149 |
-
`AK Pipe Loop` Always outputs the most recently modified connected AK Pipe.
|
| 150 |
-
|
| 151 |
-
##### Node: CLIP Text Encode Cached
|
| 152 |
-
|
| 153 |
-
Simple node for encoding conditioning with a small caching experiment.
|
| 154 |
-
|
| 155 |
-
##### Node: CLIP Text Encode and Combine Cached
|
| 156 |
-
|
| 157 |
-
Can combine the input conditioning with the text entered in the node.
|
| 158 |
-
|
| 159 |
-
<img src="./img/CLIPTextEncodeAndCombineCached.jpg" height="200">
|
| 160 |
-
|
| 161 |
-
##### Node: AK Resize On Boolean
|
| 162 |
-
|
| 163 |
-
Resizes the image and optional mask based on a boolean parameter (enable / disable). Useful when working with the **Project Settings** panel.
|
| 164 |
-
|
| 165 |
-
<img src="./img/AKResizeOnBoolean.jpg" height="200">
|
| 166 |
-
|
| 167 |
-
##### Node: AK Contrast And Saturate Image
|
| 168 |
-
|
| 169 |
-
Node for adjusting image contrast and saturation.
|
| 170 |
-
|
| 171 |
-
##### Node: Preview Raw Text
|
| 172 |
-
|
| 173 |
-
Displays text and can format **JSON**. It also supports displaying a list of texts.
|
| 174 |
-
|
| 175 |
-
---
|
| 176 |
-
|
| 177 |
-
# Installation
|
| 178 |
-
|
| 179 |
-
From your ComfyUI root directory:
|
| 180 |
-
|
| 181 |
-
```bash
|
| 182 |
-
cd ComfyUI/custom_nodes
|
| 183 |
-
git clone https://github.com/akawana/ComfyUI-Utils-extra.git
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/__init__.py
DELETED
|
@@ -1,109 +0,0 @@
|
|
| 1 |
-
WEB_DIRECTORY = "./js"
|
| 2 |
-
|
| 3 |
-
from .nodes.AKIndexMultiple import NODE_CLASS_MAPPINGS as AKIndexMultiple_MAPPINGS
|
| 4 |
-
from .nodes.AKIndexMultiple import NODE_DISPLAY_NAME_MAPPINGS as AKIndexMultiple_DISPLAY
|
| 5 |
-
|
| 6 |
-
from .nodes.AKCLIPEncodeMultiple import NODE_CLASS_MAPPINGS as AKCLIPEncodeMultiple_MAPPINGS
|
| 7 |
-
from .nodes.AKCLIPEncodeMultiple import NODE_DISPLAY_NAME_MAPPINGS as AKCLIPEncodeMultiple_DISPLAY
|
| 8 |
-
|
| 9 |
-
from .nodes.IsOneOfGroupsActive import NODE_CLASS_MAPPINGS as GROUPCHK_MAPPINGS
|
| 10 |
-
from .nodes.IsOneOfGroupsActive import NODE_DISPLAY_NAME_MAPPINGS as GROUPCHK_DISPLAY
|
| 11 |
-
|
| 12 |
-
from .nodes.RepeatGroupState import NODE_CLASS_MAPPINGS as RPSTATE_MAPPINGS
|
| 13 |
-
from .nodes.RepeatGroupState import NODE_DISPLAY_NAME_MAPPINGS as RPSTATE_DISPLAY
|
| 14 |
-
|
| 15 |
-
from .nodes.PreviewRawText import NODE_CLASS_MAPPINGS as PRTSTATE_MAPPINGS
|
| 16 |
-
from .nodes.PreviewRawText import NODE_DISPLAY_NAME_MAPPINGS as PRTSTATE_DISPLAY
|
| 17 |
-
|
| 18 |
-
from .nodes.CLIPTextEncodeCached import NODE_CLASS_MAPPINGS as CLCSTATE_MAPPINGS
|
| 19 |
-
from .nodes.CLIPTextEncodeCached import NODE_DISPLAY_NAME_MAPPINGS as CLCSTATE_DISPLAY
|
| 20 |
-
|
| 21 |
-
from .nodes.CLIPTextEncodeAndCombineCached import NODE_CLASS_MAPPINGS as CLCOMBINE_STATE_MAPPINGS
|
| 22 |
-
from .nodes.CLIPTextEncodeAndCombineCached import NODE_DISPLAY_NAME_MAPPINGS as CLCOMBINE_STATE_DISPLAY
|
| 23 |
-
|
| 24 |
-
from .nodes.AKBase import NODE_CLASS_MAPPINGS as AKBSTATE_MAPPINGS
|
| 25 |
-
from .nodes.AKBase import NODE_DISPLAY_NAME_MAPPINGS as AKBSTATE_DISPLAY
|
| 26 |
-
|
| 27 |
-
from .nodes.AKPipe import NODE_CLASS_MAPPINGS as AKPIPESTATE_MAPPINGS
|
| 28 |
-
from .nodes.AKPipe import NODE_DISPLAY_NAME_MAPPINGS as AKPIPESTATE_DISPLAY
|
| 29 |
-
|
| 30 |
-
from .nodes.AKPipeLoop import NODE_CLASS_MAPPINGS as AKPIPEL_STATE_MAPPINGS
|
| 31 |
-
from .nodes.AKPipeLoop import NODE_DISPLAY_NAME_MAPPINGS as AKPIPEL_STATE_DISPLAY
|
| 32 |
-
|
| 33 |
-
from .nodes.Setter import NODE_CLASS_MAPPINGS as SETTERSTATE_MAPPINGS
|
| 34 |
-
from .nodes.Setter import NODE_DISPLAY_NAME_MAPPINGS as SETTERSTATE_DISPLAY
|
| 35 |
-
|
| 36 |
-
from .nodes.Getter import NODE_CLASS_MAPPINGS as GETTERSTATE_MAPPINGS
|
| 37 |
-
from .nodes.Getter import NODE_DISPLAY_NAME_MAPPINGS as GETTERSTATE_DISPLAY
|
| 38 |
-
|
| 39 |
-
from .nodes.AKResizeOnBoolean import NODE_CLASS_MAPPINGS as RESIZESTATE_MAPPINGS
|
| 40 |
-
from .nodes.AKResizeOnBoolean import NODE_DISPLAY_NAME_MAPPINGS as RESIZESTATE_DISPLAY
|
| 41 |
-
|
| 42 |
-
from .nodes.IsMaskEmpty import NODE_CLASS_MAPPINGS as ISMSTATE_MAPPINGS
|
| 43 |
-
from .nodes.IsMaskEmpty import NODE_DISPLAY_NAME_MAPPINGS as ISMSTATE_DISPLAY
|
| 44 |
-
|
| 45 |
-
from .nodes.AKContrastAndSaturateImage import NODE_CLASS_MAPPINGS as AKSAT_STATE_MAPPINGS
|
| 46 |
-
from .nodes.AKContrastAndSaturateImage import NODE_DISPLAY_NAME_MAPPINGS as AKSAT_STATE_DISPLAY
|
| 47 |
-
|
| 48 |
-
from .nodes.AKReplaceAlphaWithColor import NODE_CLASS_MAPPINGS as AKRALPHA_STATE_MAPPINGS
|
| 49 |
-
from .nodes.AKReplaceAlphaWithColor import NODE_DISPLAY_NAME_MAPPINGS as AKRALPHA_STATE_DISPLAY
|
| 50 |
-
|
| 51 |
-
from .nodes.AKReplaceColorWithAlpha import NODE_CLASS_MAPPINGS as AKRCOLOR_STATE_MAPPINGS
|
| 52 |
-
from .nodes.AKReplaceColorWithAlpha import NODE_DISPLAY_NAME_MAPPINGS as AKRCOLOR_STATE_DISPLAY
|
| 53 |
-
|
| 54 |
-
from .nodes.AKControlMultipleKSamplers import NODE_CLASS_MAPPINGS as AK_CONTROL_SAMPLERS_COLOR_STATE_MAPPINGS
|
| 55 |
-
from .nodes.AKControlMultipleKSamplers import NODE_DISPLAY_NAME_MAPPINGS as AK_CONTROL_SAMPLERS_COLOR_STATE_DISPLAY
|
| 56 |
-
|
| 57 |
-
from .nodes.AKKSamplerSettings import NODE_CLASS_MAPPINGS as AKKSamplerSettings_STATE_MAPPINGS
|
| 58 |
-
from .nodes.AKKSamplerSettings import NODE_DISPLAY_NAME_MAPPINGS as AKKSamplerSettings_STATE_DISPLAY
|
| 59 |
-
|
| 60 |
-
from .nodes.AKProjectSettingsOut import NODE_CLASS_MAPPINGS as AKProjectSettingsOut_STATE_MAPPINGS
|
| 61 |
-
from .nodes.AKProjectSettingsOut import NODE_DISPLAY_NAME_MAPPINGS as AKProjectSettingsOut_STATE_DISPLAY
|
| 62 |
-
|
| 63 |
-
NODE_CLASS_MAPPINGS = {
|
| 64 |
-
**AKIndexMultiple_MAPPINGS,
|
| 65 |
-
**AKCLIPEncodeMultiple_MAPPINGS,
|
| 66 |
-
**GROUPCHK_MAPPINGS,
|
| 67 |
-
**RPSTATE_MAPPINGS,
|
| 68 |
-
**PRTSTATE_MAPPINGS,
|
| 69 |
-
**CLCSTATE_MAPPINGS,
|
| 70 |
-
**CLCOMBINE_STATE_MAPPINGS,
|
| 71 |
-
**AKBSTATE_MAPPINGS,
|
| 72 |
-
**AKPIPESTATE_MAPPINGS,
|
| 73 |
-
**AKPIPEL_STATE_MAPPINGS,
|
| 74 |
-
**SETTERSTATE_MAPPINGS,
|
| 75 |
-
**GETTERSTATE_MAPPINGS,
|
| 76 |
-
**RESIZESTATE_MAPPINGS,
|
| 77 |
-
**ISMSTATE_MAPPINGS,
|
| 78 |
-
**AKSAT_STATE_MAPPINGS,
|
| 79 |
-
**AKRALPHA_STATE_MAPPINGS,
|
| 80 |
-
**AKRCOLOR_STATE_MAPPINGS,
|
| 81 |
-
**AK_CONTROL_SAMPLERS_COLOR_STATE_MAPPINGS,
|
| 82 |
-
**AKKSamplerSettings_STATE_MAPPINGS,
|
| 83 |
-
**AKProjectSettingsOut_STATE_MAPPINGS,
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 87 |
-
**AKIndexMultiple_DISPLAY,
|
| 88 |
-
**AKCLIPEncodeMultiple_DISPLAY,
|
| 89 |
-
**GROUPCHK_DISPLAY,
|
| 90 |
-
**RPSTATE_DISPLAY,
|
| 91 |
-
**PRTSTATE_DISPLAY,
|
| 92 |
-
**CLCSTATE_DISPLAY,
|
| 93 |
-
**CLCOMBINE_STATE_DISPLAY,
|
| 94 |
-
**AKBSTATE_DISPLAY,
|
| 95 |
-
**AKPIPESTATE_DISPLAY,
|
| 96 |
-
**AKPIPEL_STATE_DISPLAY,
|
| 97 |
-
**SETTERSTATE_DISPLAY,
|
| 98 |
-
**GETTERSTATE_DISPLAY,
|
| 99 |
-
**RESIZESTATE_DISPLAY,
|
| 100 |
-
**ISMSTATE_DISPLAY,
|
| 101 |
-
**AKSAT_STATE_DISPLAY,
|
| 102 |
-
**AKRALPHA_STATE_DISPLAY,
|
| 103 |
-
**AKRCOLOR_STATE_DISPLAY,
|
| 104 |
-
**AK_CONTROL_SAMPLERS_COLOR_STATE_DISPLAY,
|
| 105 |
-
**AKKSamplerSettings_STATE_DISPLAY,
|
| 106 |
-
**AKProjectSettingsOut_STATE_DISPLAY,
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/icon.jpg
DELETED
|
Binary file (28.1 kB)
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/AKBase.py
DELETED
|
@@ -1,248 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
# === AKXZ embedded JSON fallback (read from IMAGE tensors) ===
|
| 3 |
-
import json as _akxz_json
|
| 4 |
-
import torch as _akxz_torch
|
| 5 |
-
|
| 6 |
-
_AKXZ_MAGIC = b"AKXZ"
|
| 7 |
-
_AKXZ_HEADER_LEN = 8
|
| 8 |
-
|
| 9 |
-
def _akxz_float01_to_bytes(x: _akxz_torch.Tensor) -> bytes:
|
| 10 |
-
x = _akxz_torch.clamp(x * 255.0 + 0.5, 0, 255).to(_akxz_torch.uint8).cpu()
|
| 11 |
-
return x.numpy().tobytes()
|
| 12 |
-
|
| 13 |
-
def akxz_extract_image_cfg_list(images: _akxz_torch.Tensor):
|
| 14 |
-
if not isinstance(images, _akxz_torch.Tensor) or images.ndim != 4:
|
| 15 |
-
return []
|
| 16 |
-
b = int(images.shape[0])
|
| 17 |
-
out = []
|
| 18 |
-
for i in range(b):
|
| 19 |
-
row = images[i, 0, :, :3].reshape(-1)
|
| 20 |
-
raw = _akxz_float01_to_bytes(row)
|
| 21 |
-
if len(raw) < _AKXZ_HEADER_LEN or raw[:4] != _AKXZ_MAGIC:
|
| 22 |
-
continue
|
| 23 |
-
size = int.from_bytes(raw[4:8], "big")
|
| 24 |
-
if size <= 0 or (8 + size) > len(raw):
|
| 25 |
-
continue
|
| 26 |
-
payload = raw[8:8+size]
|
| 27 |
-
try:
|
| 28 |
-
obj = _akxz_json.loads(payload.decode("utf-8"))
|
| 29 |
-
if isinstance(obj, dict):
|
| 30 |
-
out.append(obj)
|
| 31 |
-
except Exception:
|
| 32 |
-
pass
|
| 33 |
-
return out
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
import os
|
| 37 |
-
import json
|
| 38 |
-
|
| 39 |
-
import numpy as np
|
| 40 |
-
from PIL import Image
|
| 41 |
-
from PIL.PngImagePlugin import PngInfo
|
| 42 |
-
|
| 43 |
-
import folder_paths
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
AKBASE_STATE_FILENAME = "ak_base_state.json"
|
| 47 |
-
AKBASE_XZ_CONFIG_FILENAME = "ak_base_xz_config.json"
|
| 48 |
-
|
| 49 |
-
AKBASE_A_FILENAME = "ak_base_image_a.png"
|
| 50 |
-
AKBASE_B_FILENAME = "ak_base_image_b.png"
|
| 51 |
-
|
| 52 |
-
AKBASE_GALLERY_PREFIX = "ak_base_image_xy_"
|
| 53 |
-
AKBASE_GALLERY_MAX = 512
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def _tensor_to_pil(img_tensor):
|
| 57 |
-
arr = img_tensor.detach().cpu().numpy()
|
| 58 |
-
arr = np.clip(arr * 255.0, 0, 255).astype(np.uint8)
|
| 59 |
-
return Image.fromarray(arr)
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def _temp_dir():
|
| 63 |
-
d = folder_paths.get_temp_directory()
|
| 64 |
-
os.makedirs(d, exist_ok=True)
|
| 65 |
-
return d
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def _temp_path(filename: str) -> str:
|
| 69 |
-
return os.path.join(_temp_dir(), filename)
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def _save_temp_png(img_tensor, filename: str, meta: dict = None) -> None:
|
| 73 |
-
img = _tensor_to_pil(img_tensor)
|
| 74 |
-
|
| 75 |
-
pnginfo = None
|
| 76 |
-
if isinstance(meta, dict) and meta:
|
| 77 |
-
pnginfo = PngInfo()
|
| 78 |
-
for k, v in meta.items():
|
| 79 |
-
if v is None:
|
| 80 |
-
v = ""
|
| 81 |
-
pnginfo.add_text(str(k), str(v))
|
| 82 |
-
|
| 83 |
-
img.save(_temp_path(filename), compress_level=4, pnginfo=pnginfo)
|
| 84 |
-
|
| 85 |
-
def _safe_remove(filename: str) -> None:
|
| 86 |
-
try:
|
| 87 |
-
p = _temp_path(filename)
|
| 88 |
-
if os.path.isfile(p):
|
| 89 |
-
os.remove(p)
|
| 90 |
-
except Exception:
|
| 91 |
-
pass
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def _clear_gallery_files() -> None:
|
| 95 |
-
d = _temp_dir()
|
| 96 |
-
for fn in os.listdir(d):
|
| 97 |
-
if fn.startswith(AKBASE_GALLERY_PREFIX) and fn.lower().endswith(".png"):
|
| 98 |
-
try:
|
| 99 |
-
os.remove(os.path.join(d, fn))
|
| 100 |
-
except Exception:
|
| 101 |
-
pass
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
def _clear_compare_files() -> None:
|
| 105 |
-
_safe_remove(AKBASE_A_FILENAME)
|
| 106 |
-
_safe_remove(AKBASE_B_FILENAME)
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
def _write_state(state: dict, filename: str = None) -> None:
|
| 110 |
-
try:
|
| 111 |
-
fn = filename if filename else AKBASE_STATE_FILENAME
|
| 112 |
-
with open(_temp_path(fn), "w", encoding="utf-8") as f:
|
| 113 |
-
json.dump(state, f, ensure_ascii=False)
|
| 114 |
-
except Exception:
|
| 115 |
-
pass
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
class AKBase:
|
| 119 |
-
@classmethod
|
| 120 |
-
def INPUT_TYPES(cls):
|
| 121 |
-
return {
|
| 122 |
-
"required": {
|
| 123 |
-
"a_image": ("IMAGE",),
|
| 124 |
-
},
|
| 125 |
-
"optional": {
|
| 126 |
-
"b_image": ("IMAGE",),
|
| 127 |
-
},
|
| 128 |
-
"hidden": {
|
| 129 |
-
"unique_id": "UNIQUE_ID",
|
| 130 |
-
},
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
RETURN_TYPES = ()
|
| 134 |
-
FUNCTION = "run"
|
| 135 |
-
CATEGORY = "AK"
|
| 136 |
-
OUTPUT_NODE = True
|
| 137 |
-
|
| 138 |
-
def run(
|
| 139 |
-
self,
|
| 140 |
-
a_image,
|
| 141 |
-
b_image=None,
|
| 142 |
-
# ak_settings: str = None,
|
| 143 |
-
unique_id=None,
|
| 144 |
-
):
|
| 145 |
-
a_n = int(a_image.shape[0]) if hasattr(a_image, "shape") else 1
|
| 146 |
-
b_n = int(b_image.shape[0]) if (b_image is not None and hasattr(b_image, "shape")) else 0
|
| 147 |
-
|
| 148 |
-
ak_base_config = "{}"
|
| 149 |
-
|
| 150 |
-
node_id = unique_id if unique_id is not None else getattr(self, "node_id", None)
|
| 151 |
-
node_id = str(node_id) if node_id is not None else None
|
| 152 |
-
suffix = f"_{node_id}" if node_id is not None else ""
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
xz_fname = f"ak_base_xz_config{suffix}.json" if suffix else AKBASE_XZ_CONFIG_FILENAME
|
| 156 |
-
try:
|
| 157 |
-
|
| 158 |
-
content = "{}"
|
| 159 |
-
cfg_list = []
|
| 160 |
-
try:
|
| 161 |
-
if (a_n > 1) or (b_n > 1):
|
| 162 |
-
imgs = []
|
| 163 |
-
if a_n > 1:
|
| 164 |
-
imgs.extend([a_image[i] for i in range(min(a_n, AKBASE_GALLERY_MAX))])
|
| 165 |
-
if b_n > 1 and b_image is not None:
|
| 166 |
-
remaining = AKBASE_GALLERY_MAX - len(imgs)
|
| 167 |
-
if remaining > 0:
|
| 168 |
-
imgs.extend([b_image[i] for i in range(min(b_n, remaining))])
|
| 169 |
-
if imgs:
|
| 170 |
-
cfg_list = akxz_extract_image_cfg_list(_akxz_torch.stack(imgs, dim=0))
|
| 171 |
-
else:
|
| 172 |
-
cfg_list = akxz_extract_image_cfg_list(a_image)
|
| 173 |
-
except Exception:
|
| 174 |
-
cfg_list = []
|
| 175 |
-
|
| 176 |
-
if cfg_list:
|
| 177 |
-
content = json.dumps({"image": cfg_list}, ensure_ascii=False)
|
| 178 |
-
with open(_temp_path(xz_fname), "w", encoding="utf-8") as f:
|
| 179 |
-
f.write(content)
|
| 180 |
-
|
| 181 |
-
except Exception:
|
| 182 |
-
pass
|
| 183 |
-
|
| 184 |
-
if a_n > 1 or b_n > 1:
|
| 185 |
-
_clear_compare_files()
|
| 186 |
-
if suffix:
|
| 187 |
-
_safe_remove(f"ak_base_image_a{suffix}.png")
|
| 188 |
-
_safe_remove(f"ak_base_image_b{suffix}.png")
|
| 189 |
-
_clear_gallery_files()
|
| 190 |
-
|
| 191 |
-
_save_temp_png(a_image[0], f"ak_base_image_a{suffix}.png" if suffix else AKBASE_A_FILENAME)
|
| 192 |
-
if b_image is None:
|
| 193 |
-
_save_temp_png(a_image[0], f"ak_base_image_b{suffix}.png" if suffix else AKBASE_B_FILENAME)
|
| 194 |
-
else:
|
| 195 |
-
_save_temp_png(b_image[0], f"ak_base_image_b{suffix}.png" if suffix else AKBASE_B_FILENAME)
|
| 196 |
-
|
| 197 |
-
imgs = []
|
| 198 |
-
if a_n > 1:
|
| 199 |
-
imgs.extend([a_image[i] for i in range(min(a_n, AKBASE_GALLERY_MAX))])
|
| 200 |
-
|
| 201 |
-
if b_n > 1 and b_image is not None:
|
| 202 |
-
remaining = AKBASE_GALLERY_MAX - len(imgs)
|
| 203 |
-
if remaining > 0:
|
| 204 |
-
imgs.extend([b_image[i] for i in range(min(b_n, remaining))])
|
| 205 |
-
|
| 206 |
-
gallery_prefix = f"{AKBASE_GALLERY_PREFIX}{node_id}_" if node_id is not None else AKBASE_GALLERY_PREFIX
|
| 207 |
-
for i, t in enumerate(imgs):
|
| 208 |
-
_save_temp_png(t, f"{gallery_prefix}{i}.png")
|
| 209 |
-
|
| 210 |
-
_write_state(
|
| 211 |
-
{
|
| 212 |
-
"mode": "gallery",
|
| 213 |
-
"count": len(imgs),
|
| 214 |
-
"gallery_prefix": gallery_prefix,
|
| 215 |
-
},
|
| 216 |
-
filename=(f"ak_base_state{suffix}.json" if suffix else AKBASE_STATE_FILENAME),
|
| 217 |
-
)
|
| 218 |
-
|
| 219 |
-
return {"ui": {"ak_base_saved": [True]}, "result": (ak_base_config,)}
|
| 220 |
-
|
| 221 |
-
_clear_gallery_files()
|
| 222 |
-
if suffix:
|
| 223 |
-
_safe_remove(f"ak_base_image_a{suffix}.png")
|
| 224 |
-
_safe_remove(f"ak_base_image_b{suffix}.png")
|
| 225 |
-
|
| 226 |
-
_save_temp_png(a_image[0], f"ak_base_image_a{suffix}.png" if suffix else AKBASE_A_FILENAME)
|
| 227 |
-
|
| 228 |
-
if b_image is None:
|
| 229 |
-
_save_temp_png(a_image[0], f"ak_base_image_b{suffix}.png" if suffix else AKBASE_B_FILENAME)
|
| 230 |
-
else:
|
| 231 |
-
_save_temp_png(b_image[0], f"ak_base_image_b{suffix}.png" if suffix else AKBASE_B_FILENAME)
|
| 232 |
-
|
| 233 |
-
_write_state(
|
| 234 |
-
{
|
| 235 |
-
"mode": "compare",
|
| 236 |
-
"a": {"filename": (f"ak_base_image_a{suffix}.png" if suffix else AKBASE_A_FILENAME), "type": "temp", "subfolder": ""},
|
| 237 |
-
"b": {"filename": (f"ak_base_image_b{suffix}.png" if suffix else AKBASE_B_FILENAME), "type": "temp", "subfolder": ""},
|
| 238 |
-
"preview_width": 512,
|
| 239 |
-
"preview_height": 512,
|
| 240 |
-
},
|
| 241 |
-
filename=(f"ak_base_state{suffix}.json" if suffix else AKBASE_STATE_FILENAME),
|
| 242 |
-
)
|
| 243 |
-
|
| 244 |
-
return {"ui": {"ak_base_saved": [True]}, "result": (ak_base_config,)}
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
NODE_CLASS_MAPPINGS = {"AK Base": AKBase}
|
| 248 |
-
NODE_DISPLAY_NAME_MAPPINGS = {"AK Base": "AK Base"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/AKCLIPEncodeMultiple.py
DELETED
|
@@ -1,243 +0,0 @@
|
|
| 1 |
-
import zlib
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
class AnyType(str):
|
| 5 |
-
def __ne__(self, __value: object) -> bool:
|
| 6 |
-
return False
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
ANY_TYPE = AnyType("*")
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
class AKCLIPEncodeMultiple:
|
| 13 |
-
empty_cache = {}
|
| 14 |
-
# text_cache = {}
|
| 15 |
-
|
| 16 |
-
last_items = None
|
| 17 |
-
idx_cache = {}
|
| 18 |
-
hash_cache = {}
|
| 19 |
-
|
| 20 |
-
@classmethod
|
| 21 |
-
def INPUT_TYPES(cls):
|
| 22 |
-
return {
|
| 23 |
-
"required": {
|
| 24 |
-
"str_list": (ANY_TYPE, {"forceInput": False}),
|
| 25 |
-
"clip": ("CLIP",),
|
| 26 |
-
"starting_index": ("INT", {"default": 0, "min": 0, "step": 1}),
|
| 27 |
-
"length": ("INT", {"default": 1, "min": 1, "max": 20, "step": 1}),
|
| 28 |
-
},
|
| 29 |
-
"optional": {
|
| 30 |
-
"mask_list": ("MASK",),
|
| 31 |
-
},
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
RETURN_TYPES = ("CONDITIONING",) + ("CONDITIONING",) * 20
|
| 35 |
-
RETURN_NAMES = ("combined_con",) + tuple(f"cond_{i}" for i in range(20))
|
| 36 |
-
|
| 37 |
-
FUNCTION = "execute"
|
| 38 |
-
CATEGORY = "AK/conditioning"
|
| 39 |
-
OUTPUT_NODE = False
|
| 40 |
-
|
| 41 |
-
INPUT_IS_LIST = True
|
| 42 |
-
|
| 43 |
-
@classmethod
|
| 44 |
-
def _get_empty_cond(cls, clip):
|
| 45 |
-
key = id(clip)
|
| 46 |
-
cached = cls.empty_cache.get(key)
|
| 47 |
-
if cached is not None:
|
| 48 |
-
return cached
|
| 49 |
-
|
| 50 |
-
tokens = clip.tokenize("")
|
| 51 |
-
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
|
| 52 |
-
empty = [[cond, {"pooled_output": pooled}]]
|
| 53 |
-
cls.empty_cache[key] = empty
|
| 54 |
-
return empty
|
| 55 |
-
|
| 56 |
-
@classmethod
|
| 57 |
-
def _encode_text(cls, clip, text):
|
| 58 |
-
tokens = clip.tokenize(text)
|
| 59 |
-
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
|
| 60 |
-
return [[cond, {"pooled_output": pooled}]]
|
| 61 |
-
|
| 62 |
-
@staticmethod
|
| 63 |
-
def _clip_key(clip):
|
| 64 |
-
inner = getattr(clip, "cond_stage_model", None)
|
| 65 |
-
if inner is not None:
|
| 66 |
-
return id(inner)
|
| 67 |
-
inner = getattr(clip, "clip", None) or getattr(clip, "model", None)
|
| 68 |
-
if inner is not None:
|
| 69 |
-
return id(inner)
|
| 70 |
-
return id(clip)
|
| 71 |
-
|
| 72 |
-
@staticmethod
|
| 73 |
-
def _apply_mask_to_cond(base_cond, mask):
|
| 74 |
-
if mask is None:
|
| 75 |
-
return base_cond
|
| 76 |
-
cond_with_mask = []
|
| 77 |
-
for t, data in base_cond:
|
| 78 |
-
data_copy = dict(data) if data is not None else {}
|
| 79 |
-
data_copy["mask"] = mask
|
| 80 |
-
data_copy["mask_strength"] = 1.0
|
| 81 |
-
cond_with_mask.append([t, data_copy])
|
| 82 |
-
return cond_with_mask
|
| 83 |
-
|
| 84 |
-
@classmethod
|
| 85 |
-
def _compute_hash(cls, items, masks, start, length):
|
| 86 |
-
max_bytes = 64 * 1024
|
| 87 |
-
buf = bytearray()
|
| 88 |
-
|
| 89 |
-
def add_bytes(b):
|
| 90 |
-
nonlocal buf
|
| 91 |
-
if not b:
|
| 92 |
-
return
|
| 93 |
-
remaining = max_bytes - len(buf)
|
| 94 |
-
if remaining <= 0:
|
| 95 |
-
return
|
| 96 |
-
if len(b) > remaining:
|
| 97 |
-
buf.extend(b[:remaining])
|
| 98 |
-
else:
|
| 99 |
-
buf.extend(b)
|
| 100 |
-
|
| 101 |
-
for v in items:
|
| 102 |
-
s = "" if v is None else v
|
| 103 |
-
add_bytes(s.encode("utf-8", errors="ignore") + b"\n")
|
| 104 |
-
if len(buf) >= max_bytes:
|
| 105 |
-
break
|
| 106 |
-
|
| 107 |
-
if len(buf) < max_bytes and masks:
|
| 108 |
-
for m in masks:
|
| 109 |
-
if m is None:
|
| 110 |
-
add_bytes(b"MNONE\n")
|
| 111 |
-
else:
|
| 112 |
-
try:
|
| 113 |
-
t = m
|
| 114 |
-
if isinstance(t, (list, tuple)):
|
| 115 |
-
t = t[0]
|
| 116 |
-
if hasattr(t, "detach"):
|
| 117 |
-
t = t.detach()
|
| 118 |
-
arr = t.cpu().contiguous().numpy()
|
| 119 |
-
add_bytes(arr.tobytes())
|
| 120 |
-
except Exception:
|
| 121 |
-
add_bytes(repr(type(m)).encode("utf-8", errors="ignore"))
|
| 122 |
-
if len(buf) >= max_bytes:
|
| 123 |
-
break
|
| 124 |
-
|
| 125 |
-
add_bytes(f"|start={start}|len={length}".encode("ascii"))
|
| 126 |
-
|
| 127 |
-
return zlib.adler32(bytes(buf)) & 0xFFFFFFFF
|
| 128 |
-
|
| 129 |
-
def execute(self, clip, str_list, starting_index, length, mask_list=None):
|
| 130 |
-
if isinstance(clip, (list, tuple)):
|
| 131 |
-
clip_obj = clip[0]
|
| 132 |
-
else:
|
| 133 |
-
clip_obj = clip
|
| 134 |
-
|
| 135 |
-
if isinstance(str_list, (list, tuple)):
|
| 136 |
-
if len(str_list) == 1 and isinstance(str_list[0], (list, tuple)):
|
| 137 |
-
items = list(str_list[0])
|
| 138 |
-
else:
|
| 139 |
-
items = list(str_list)
|
| 140 |
-
else:
|
| 141 |
-
items = [str_list]
|
| 142 |
-
|
| 143 |
-
if not isinstance(items, list) or not all(
|
| 144 |
-
(isinstance(v, str) or v is None) for v in items
|
| 145 |
-
):
|
| 146 |
-
raise RuntimeError("Require array of strings")
|
| 147 |
-
|
| 148 |
-
masks = []
|
| 149 |
-
if mask_list is not None:
|
| 150 |
-
if isinstance(mask_list, (list, tuple)):
|
| 151 |
-
if len(mask_list) == 1 and isinstance(mask_list[0], (list, tuple)):
|
| 152 |
-
masks = list(mask_list[0])
|
| 153 |
-
else:
|
| 154 |
-
masks = list(mask_list)
|
| 155 |
-
else:
|
| 156 |
-
masks = [mask_list]
|
| 157 |
-
|
| 158 |
-
if isinstance(starting_index, (list, tuple)):
|
| 159 |
-
start_raw = starting_index[0] if starting_index else 0
|
| 160 |
-
else:
|
| 161 |
-
start_raw = starting_index
|
| 162 |
-
|
| 163 |
-
if isinstance(length, (list, tuple)):
|
| 164 |
-
length_raw = length[0] if length else 1
|
| 165 |
-
else:
|
| 166 |
-
length_raw = length
|
| 167 |
-
|
| 168 |
-
start = max(0, int(start_raw))
|
| 169 |
-
length_val = max(1, min(20, int(length_raw)))
|
| 170 |
-
|
| 171 |
-
if AKCLIPEncodeMultiple.last_items is None or len(AKCLIPEncodeMultiple.last_items) != len(items):
|
| 172 |
-
AKCLIPEncodeMultiple.last_items = [None] * len(items)
|
| 173 |
-
AKCLIPEncodeMultiple.idx_cache.clear()
|
| 174 |
-
|
| 175 |
-
clip_id = self._clip_key(clip_obj)
|
| 176 |
-
items_copy = list(items)
|
| 177 |
-
masks_copy = list(masks) if masks else []
|
| 178 |
-
hval = self._compute_hash(items_copy, masks_copy, start, length_val)
|
| 179 |
-
cache_key = (clip_id, hval)
|
| 180 |
-
|
| 181 |
-
cached_entry = AKCLIPEncodeMultiple.hash_cache.get(cache_key)
|
| 182 |
-
if cached_entry is not None:
|
| 183 |
-
combined_cached, per_idx_cached = cached_entry
|
| 184 |
-
per_idx_cached = list(per_idx_cached)
|
| 185 |
-
if len(per_idx_cached) < 20:
|
| 186 |
-
per_idx_cached.extend([None] * (20 - len(per_idx_cached)))
|
| 187 |
-
elif len(per_idx_cached) > 20:
|
| 188 |
-
per_idx_cached = per_idx_cached[:20]
|
| 189 |
-
return (combined_cached,) + tuple(per_idx_cached)
|
| 190 |
-
|
| 191 |
-
result = []
|
| 192 |
-
empty_cond = None
|
| 193 |
-
combined_cond = None
|
| 194 |
-
|
| 195 |
-
for i in range(length_val):
|
| 196 |
-
idx = start + i
|
| 197 |
-
|
| 198 |
-
cond = None
|
| 199 |
-
if 0 <= idx < len(items):
|
| 200 |
-
v = items[idx]
|
| 201 |
-
|
| 202 |
-
mask_for_idx = None
|
| 203 |
-
if 0 <= idx < len(masks):
|
| 204 |
-
mask_for_idx = masks[idx]
|
| 205 |
-
|
| 206 |
-
if v is None:
|
| 207 |
-
if empty_cond is None:
|
| 208 |
-
empty_cond = self._get_empty_cond(clip_obj)
|
| 209 |
-
base_cond = empty_cond
|
| 210 |
-
AKCLIPEncodeMultiple.last_items[idx] = None
|
| 211 |
-
else:
|
| 212 |
-
prev = AKCLIPEncodeMultiple.last_items[idx]
|
| 213 |
-
if v == prev:
|
| 214 |
-
cached = AKCLIPEncodeMultiple.idx_cache.get((clip_id, idx))
|
| 215 |
-
if cached is not None:
|
| 216 |
-
base_cond = cached
|
| 217 |
-
else:
|
| 218 |
-
base_cond = self._encode_text(clip_obj, v)
|
| 219 |
-
AKCLIPEncodeMultiple.idx_cache[(clip_id, idx)] = base_cond
|
| 220 |
-
else:
|
| 221 |
-
base_cond = self._encode_text(clip_obj, v)
|
| 222 |
-
AKCLIPEncodeMultiple.idx_cache[(clip_id, idx)] = base_cond
|
| 223 |
-
AKCLIPEncodeMultiple.last_items[idx] = v
|
| 224 |
-
cond = self._apply_mask_to_cond(base_cond, mask_for_idx)
|
| 225 |
-
if v is not None and cond is not None:
|
| 226 |
-
if combined_cond is None:
|
| 227 |
-
combined_cond = []
|
| 228 |
-
for t, data in cond:
|
| 229 |
-
data_copy = dict(data) if data is not None else {}
|
| 230 |
-
combined_cond.append([t, data_copy])
|
| 231 |
-
|
| 232 |
-
result.append(cond)
|
| 233 |
-
|
| 234 |
-
if length_val < 20:
|
| 235 |
-
result.extend([None] * (20 - length_val))
|
| 236 |
-
|
| 237 |
-
AKCLIPEncodeMultiple.hash_cache[cache_key] = (combined_cond, list(result))
|
| 238 |
-
|
| 239 |
-
return (combined_cond,) + tuple(result)
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
NODE_CLASS_MAPPINGS = {"AKCLIPEncodeMultiple": AKCLIPEncodeMultiple}
|
| 243 |
-
NODE_DISPLAY_NAME_MAPPINGS = {"AKCLIPEncodeMultiple": "AK CLIP Encode Multiple"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/AKContrastAndSaturateImage.py
DELETED
|
@@ -1,281 +0,0 @@
|
|
| 1 |
-
import torch
|
| 2 |
-
import numpy as np
|
| 3 |
-
from PIL import Image
|
| 4 |
-
|
| 5 |
-
# Hue band definitions (soft ranges in degrees)
|
| 6 |
-
FEATHER_DEG = 15.0
|
| 7 |
-
|
| 8 |
-
RED_RANGES_SOFT = [(315.0, 345.0), (15.0, 45.0)]
|
| 9 |
-
YELLOW_RANGES_SOFT = [(15.0, 45.0), (75.0, 105.0)]
|
| 10 |
-
GREEN_RANGES_SOFT = [(75.0, 105.0), (135.0, 165.0)]
|
| 11 |
-
CYAN_RANGES_SOFT = [(135.0, 165.0), (195.0, 225.0)]
|
| 12 |
-
BLUE_RANGES_SOFT = [(195.0, 225.0), (225.0, 285.0)]
|
| 13 |
-
MAGENTA_RANGES_SOFT = [(255.0, 285.0), (315.0, 345.0)]
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def _compute_band_weight(h_deg: np.ndarray, ranges_soft):
|
| 17 |
-
"""Compute per-pixel weight for a hue band with soft feathered edges."""
|
| 18 |
-
if h_deg.ndim != 2:
|
| 19 |
-
raise ValueError("Expected 2D hue array")
|
| 20 |
-
|
| 21 |
-
weight_total = np.zeros_like(h_deg, dtype=np.float32)
|
| 22 |
-
|
| 23 |
-
for start_soft, end_soft in ranges_soft:
|
| 24 |
-
width = end_soft - start_soft
|
| 25 |
-
if width <= 0.0:
|
| 26 |
-
continue
|
| 27 |
-
|
| 28 |
-
feather = min(FEATHER_DEG, width * 0.5)
|
| 29 |
-
start_hard = start_soft + feather
|
| 30 |
-
end_hard = end_soft - feather
|
| 31 |
-
|
| 32 |
-
seg_weight = np.zeros_like(h_deg, dtype=np.float32)
|
| 33 |
-
|
| 34 |
-
# Fully inside hard region
|
| 35 |
-
inside_hard = (h_deg >= start_hard) & (h_deg <= end_hard)
|
| 36 |
-
seg_weight[inside_hard] = 1.0
|
| 37 |
-
|
| 38 |
-
# Soft rising edge
|
| 39 |
-
if feather > 0.0:
|
| 40 |
-
rising = (h_deg >= start_soft) & (h_deg < start_hard)
|
| 41 |
-
seg_weight[rising] = (h_deg[rising] - start_soft) / feather
|
| 42 |
-
|
| 43 |
-
falling = (h_deg > end_hard) & (h_deg <= end_soft)
|
| 44 |
-
seg_weight[falling] = (end_soft - h_deg[falling]) / feather
|
| 45 |
-
|
| 46 |
-
weight_total = np.maximum(weight_total, seg_weight)
|
| 47 |
-
|
| 48 |
-
return weight_total
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def _slider_to_factor(value: int) -> float:
|
| 52 |
-
"""Convert slider value in [-100, 100] to saturation multiplier."""
|
| 53 |
-
v = float(value)
|
| 54 |
-
return max(0.0, 1.0 + v / 100.0)
|
| 55 |
-
|
| 56 |
-
def _apply_brightness_contrast_rgb01(frame: np.ndarray, brightness: int, contrast: int) -> np.ndarray:
|
| 57 |
-
"""Apply brightness and contrast to an RGB image in [0,1] float32 format."""
|
| 58 |
-
out = frame.astype(np.float32, copy=False)
|
| 59 |
-
|
| 60 |
-
if brightness != 0:
|
| 61 |
-
out = out + (float(brightness) / 100.0)
|
| 62 |
-
|
| 63 |
-
if contrast != 0:
|
| 64 |
-
factor = 1.0 + (float(contrast) / 100.0)
|
| 65 |
-
out = (out - 0.5) * factor + 0.5
|
| 66 |
-
|
| 67 |
-
return np.clip(out, 0.0, 1.0)
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
class AKContrastAndSaturateImage:
|
| 72 |
-
@classmethod
|
| 73 |
-
def INPUT_TYPES(cls):
|
| 74 |
-
return {
|
| 75 |
-
"required": {
|
| 76 |
-
"image": ("IMAGE",),
|
| 77 |
-
"brightness": (
|
| 78 |
-
"INT",
|
| 79 |
-
{
|
| 80 |
-
"default": 0,
|
| 81 |
-
"min": -100,
|
| 82 |
-
"max": 100,
|
| 83 |
-
"step": 1,
|
| 84 |
-
},
|
| 85 |
-
),
|
| 86 |
-
"contrast": (
|
| 87 |
-
"INT",
|
| 88 |
-
{
|
| 89 |
-
"default": 0,
|
| 90 |
-
"min": -100,
|
| 91 |
-
"max": 100,
|
| 92 |
-
"step": 1,
|
| 93 |
-
},
|
| 94 |
-
),
|
| 95 |
-
"master": (
|
| 96 |
-
"INT",
|
| 97 |
-
{
|
| 98 |
-
"default": 0,
|
| 99 |
-
"min": -100,
|
| 100 |
-
"max": 100,
|
| 101 |
-
"step": 1,
|
| 102 |
-
},
|
| 103 |
-
),
|
| 104 |
-
"reds": (
|
| 105 |
-
"INT",
|
| 106 |
-
{
|
| 107 |
-
"default": 0,
|
| 108 |
-
"min": -100,
|
| 109 |
-
"max": 100,
|
| 110 |
-
"step": 1,
|
| 111 |
-
},
|
| 112 |
-
),
|
| 113 |
-
"yellows": (
|
| 114 |
-
"INT",
|
| 115 |
-
{
|
| 116 |
-
"default": 0,
|
| 117 |
-
"min": -100,
|
| 118 |
-
"max": 100,
|
| 119 |
-
"step": 1,
|
| 120 |
-
},
|
| 121 |
-
),
|
| 122 |
-
"greens": (
|
| 123 |
-
"INT",
|
| 124 |
-
{
|
| 125 |
-
"default": 0,
|
| 126 |
-
"min": -100,
|
| 127 |
-
"max": 100,
|
| 128 |
-
"step": 1,
|
| 129 |
-
},
|
| 130 |
-
),
|
| 131 |
-
"cyans": (
|
| 132 |
-
"INT",
|
| 133 |
-
{
|
| 134 |
-
"default": 0,
|
| 135 |
-
"min": -100,
|
| 136 |
-
"max": 100,
|
| 137 |
-
"step": 1,
|
| 138 |
-
},
|
| 139 |
-
),
|
| 140 |
-
"blues": (
|
| 141 |
-
"INT",
|
| 142 |
-
{
|
| 143 |
-
"default": 0,
|
| 144 |
-
"min": -100,
|
| 145 |
-
"max": 100,
|
| 146 |
-
"step": 1,
|
| 147 |
-
},
|
| 148 |
-
),
|
| 149 |
-
"magentas": (
|
| 150 |
-
"INT",
|
| 151 |
-
{
|
| 152 |
-
"default": 0,
|
| 153 |
-
"min": -100,
|
| 154 |
-
"max": 100,
|
| 155 |
-
"step": 1,
|
| 156 |
-
},
|
| 157 |
-
),
|
| 158 |
-
}
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
RETURN_TYPES = ("IMAGE",)
|
| 162 |
-
RETURN_NAMES = ("image",)
|
| 163 |
-
FUNCTION = "apply"
|
| 164 |
-
CATEGORY = "AK/image"
|
| 165 |
-
|
| 166 |
-
def apply(
|
| 167 |
-
self,
|
| 168 |
-
image: torch.Tensor,
|
| 169 |
-
brightness: int,
|
| 170 |
-
contrast: int,
|
| 171 |
-
master: int,
|
| 172 |
-
reds: int,
|
| 173 |
-
yellows: int,
|
| 174 |
-
greens: int,
|
| 175 |
-
cyans: int,
|
| 176 |
-
blues: int,
|
| 177 |
-
magentas: int,
|
| 178 |
-
):
|
| 179 |
-
if not isinstance(image, torch.Tensor):
|
| 180 |
-
raise TypeError("Expected image as torch.Tensor")
|
| 181 |
-
|
| 182 |
-
if image.ndim != 4 or image.shape[-1] != 3:
|
| 183 |
-
raise ValueError("Expected image with shape [B, H, W, 3]")
|
| 184 |
-
|
| 185 |
-
if (
|
| 186 |
-
brightness == 0
|
| 187 |
-
and contrast == 0
|
| 188 |
-
and master == 0
|
| 189 |
-
and reds == 0
|
| 190 |
-
and yellows == 0
|
| 191 |
-
and greens == 0
|
| 192 |
-
and cyans == 0
|
| 193 |
-
and blues == 0
|
| 194 |
-
and magentas == 0
|
| 195 |
-
):
|
| 196 |
-
return (image,)
|
| 197 |
-
|
| 198 |
-
device = image.device
|
| 199 |
-
batch_size, h, w, c = image.shape
|
| 200 |
-
|
| 201 |
-
img_np = image.detach().cpu().numpy()
|
| 202 |
-
out_list = []
|
| 203 |
-
|
| 204 |
-
master_factor = _slider_to_factor(master)
|
| 205 |
-
reds_factor = _slider_to_factor(reds)
|
| 206 |
-
yellows_factor = _slider_to_factor(yellows)
|
| 207 |
-
greens_factor = _slider_to_factor(greens)
|
| 208 |
-
cyans_factor = _slider_to_factor(cyans)
|
| 209 |
-
blues_factor = _slider_to_factor(blues)
|
| 210 |
-
magentas_factor = _slider_to_factor(magentas)
|
| 211 |
-
|
| 212 |
-
for i in range(batch_size):
|
| 213 |
-
frame = img_np[i]
|
| 214 |
-
|
| 215 |
-
if brightness != 0 or contrast != 0:
|
| 216 |
-
frame = _apply_brightness_contrast_rgb01(frame, brightness, contrast)
|
| 217 |
-
|
| 218 |
-
frame_u8 = (frame * 255.0).clip(0, 255).astype(np.uint8)
|
| 219 |
-
|
| 220 |
-
pil_rgb = Image.fromarray(frame_u8, mode="RGB")
|
| 221 |
-
pil_hsv = pil_rgb.convert("HSV")
|
| 222 |
-
hsv = np.array(pil_hsv, dtype=np.uint8)
|
| 223 |
-
|
| 224 |
-
H = hsv[..., 0].astype(np.float32)
|
| 225 |
-
S = hsv[..., 1].astype(np.float32)
|
| 226 |
-
V = hsv[..., 2]
|
| 227 |
-
|
| 228 |
-
h_deg = H * (360.0 / 255.0)
|
| 229 |
-
|
| 230 |
-
# Base master factor
|
| 231 |
-
S = S * master_factor
|
| 232 |
-
|
| 233 |
-
# Per-band masks
|
| 234 |
-
reds_w = _compute_band_weight(h_deg, RED_RANGES_SOFT)
|
| 235 |
-
yellows_w = _compute_band_weight(h_deg, YELLOW_RANGES_SOFT)
|
| 236 |
-
greens_w = _compute_band_weight(h_deg, GREEN_RANGES_SOFT)
|
| 237 |
-
cyans_w = _compute_band_weight(h_deg, CYAN_RANGES_SOFT)
|
| 238 |
-
blues_w = _compute_band_weight(h_deg, BLUE_RANGES_SOFT)
|
| 239 |
-
magentas_w = _compute_band_weight(h_deg, MAGENTA_RANGES_SOFT)
|
| 240 |
-
|
| 241 |
-
# Sequential multiplicative adjustments
|
| 242 |
-
def apply_band(S_arr, weight, factor):
|
| 243 |
-
if factor == 1.0:
|
| 244 |
-
return S_arr
|
| 245 |
-
band_scale = 1.0 + weight * (factor - 1.0)
|
| 246 |
-
return S_arr * band_scale
|
| 247 |
-
|
| 248 |
-
S = apply_band(S, reds_w, reds_factor)
|
| 249 |
-
S = apply_band(S, yellows_w, yellows_factor)
|
| 250 |
-
S = apply_band(S, greens_w, greens_factor)
|
| 251 |
-
S = apply_band(S, cyans_w, cyans_factor)
|
| 252 |
-
S = apply_band(S, blues_w, blues_factor)
|
| 253 |
-
S = apply_band(S, magentas_w, magentas_factor)
|
| 254 |
-
|
| 255 |
-
S = np.clip(S, 0.0, 255.0).astype(np.uint8)
|
| 256 |
-
|
| 257 |
-
hsv_new = np.stack(
|
| 258 |
-
[
|
| 259 |
-
H.astype(np.uint8),
|
| 260 |
-
S,
|
| 261 |
-
V.astype(np.uint8),
|
| 262 |
-
],
|
| 263 |
-
axis=-1,
|
| 264 |
-
)
|
| 265 |
-
|
| 266 |
-
pil_hsv_new = Image.fromarray(hsv_new, mode="HSV")
|
| 267 |
-
pil_rgb_new = pil_hsv_new.convert("RGB")
|
| 268 |
-
rgb_np = np.array(pil_rgb_new).astype(np.float32) / 255.0
|
| 269 |
-
out_list.append(rgb_np)
|
| 270 |
-
|
| 271 |
-
out_np = np.stack(out_list, axis=0)
|
| 272 |
-
out_tensor = torch.from_numpy(out_np).to(device)
|
| 273 |
-
return (out_tensor,)
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
NODE_CLASS_MAPPINGS = {
|
| 277 |
-
"AKContrastAndSaturateImage": AKContrastAndSaturateImage,
|
| 278 |
-
}
|
| 279 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 280 |
-
"AKContrastAndSaturateImage": "AK Contrast & Saturate Image",
|
| 281 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/AKControlMultipleKSamplers.py
DELETED
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
import comfy.samplers
|
| 2 |
-
|
| 3 |
-
class AKControlMultipleKSamplers:
|
| 4 |
-
"""
|
| 5 |
-
UI-only control node (no outputs). Frontend JS drives synchronization.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
@classmethod
|
| 9 |
-
def INPUT_TYPES(cls):
|
| 10 |
-
return {
|
| 11 |
-
"required": {
|
| 12 |
-
"choose_ksampler": (["<none>"], {"default": "<none>"}),
|
| 13 |
-
|
| 14 |
-
"seed ": ("INT", {"default": 0, "min": 0, "max": 0x7FFFFFFF, "step": 1}),
|
| 15 |
-
"steps": ("INT", {"default": 15, "min": 1, "max": 100, "step": 1}),
|
| 16 |
-
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step": 0.1}),
|
| 17 |
-
"sampler_name": (
|
| 18 |
-
comfy.samplers.SAMPLER_NAMES,
|
| 19 |
-
{"default": comfy.samplers.SAMPLER_NAMES[0]}
|
| 20 |
-
),
|
| 21 |
-
"scheduler": (
|
| 22 |
-
comfy.samplers.SCHEDULER_NAMES,
|
| 23 |
-
{"default": comfy.samplers.SCHEDULER_NAMES[0]}
|
| 24 |
-
),
|
| 25 |
-
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
| 26 |
-
},
|
| 27 |
-
"hidden": {
|
| 28 |
-
"_ak_state_json": ("STRING", {"default": "{}", "multiline": True}),
|
| 29 |
-
}
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
RETURN_TYPES = ()
|
| 33 |
-
FUNCTION = "noop"
|
| 34 |
-
CATEGORY = "AK/sampling"
|
| 35 |
-
|
| 36 |
-
def noop(self, **kwargs):
|
| 37 |
-
return ()
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
NODE_CLASS_MAPPINGS = {
|
| 41 |
-
"AK Control Multiple KSamplers": AKControlMultipleKSamplers
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 45 |
-
"AK Control Multiple KSamplers": "AK Control Multiple KSamplers"
|
| 46 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/AKIndexMultiple.py
DELETED
|
@@ -1,72 +0,0 @@
|
|
| 1 |
-
# nodes/IndexMultiple.py
|
| 2 |
-
|
| 3 |
-
class AnyType(str):
|
| 4 |
-
def __ne__(self, __value: object) -> bool:
|
| 5 |
-
return False
|
| 6 |
-
|
| 7 |
-
ANY_TYPE = AnyType("*")
|
| 8 |
-
|
| 9 |
-
class AKIndexMultiple:
|
| 10 |
-
@classmethod
|
| 11 |
-
def INPUT_TYPES(cls):
|
| 12 |
-
return {
|
| 13 |
-
"required": {
|
| 14 |
-
"input_any": (ANY_TYPE, {"forceInput": False}),
|
| 15 |
-
"starting_index": ("INT", {"default": 0, "min": 0, "step": 1}),
|
| 16 |
-
"length": ("INT", {"default": 1, "min": 1, "max": 50, "step": 1}),
|
| 17 |
-
},
|
| 18 |
-
"optional": {
|
| 19 |
-
"if_none": (ANY_TYPE, {}),
|
| 20 |
-
}
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
RETURN_TYPES = (ANY_TYPE,) * 50
|
| 24 |
-
RETURN_NAMES = tuple(f"item_{i}" for i in range(50))
|
| 25 |
-
|
| 26 |
-
FUNCTION = "execute"
|
| 27 |
-
CATEGORY = "AK/utils"
|
| 28 |
-
OUTPUT_NODE = False
|
| 29 |
-
|
| 30 |
-
INPUT_IS_LIST = True
|
| 31 |
-
|
| 32 |
-
def execute(self, input_any, starting_index, length, if_none=None):
|
| 33 |
-
if isinstance(input_any, (list, tuple)):
|
| 34 |
-
if len(input_any) == 1 and isinstance(input_any[0], (list, tuple)):
|
| 35 |
-
items = list(input_any[0])
|
| 36 |
-
else:
|
| 37 |
-
items = list(input_any)
|
| 38 |
-
else:
|
| 39 |
-
items = [input_any]
|
| 40 |
-
|
| 41 |
-
start = max(0, int(starting_index[0] if isinstance(starting_index, (list, tuple)) else starting_index))
|
| 42 |
-
length_val = max(1, min(50, int(length[0] if isinstance(length, (list, tuple)) else length)))
|
| 43 |
-
|
| 44 |
-
# for i, v in enumerate(items[:10]):
|
| 45 |
-
# print(f" [{i}] {v}")
|
| 46 |
-
|
| 47 |
-
if isinstance(if_none, (list, tuple)):
|
| 48 |
-
fallback = if_none[0] if len(if_none) > 0 else None
|
| 49 |
-
else:
|
| 50 |
-
fallback = if_none
|
| 51 |
-
|
| 52 |
-
# print(fallback)
|
| 53 |
-
result = []
|
| 54 |
-
|
| 55 |
-
for i in range(50):
|
| 56 |
-
idx = start + i
|
| 57 |
-
|
| 58 |
-
if i < length_val and 0 <= idx < len(items):
|
| 59 |
-
v = items[idx]
|
| 60 |
-
if v is None and fallback is not None:
|
| 61 |
-
v = fallback
|
| 62 |
-
else:
|
| 63 |
-
v = fallback
|
| 64 |
-
|
| 65 |
-
result.append(v)
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
return tuple(result)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
NODE_CLASS_MAPPINGS = {"AKIndexMultiple": AKIndexMultiple}
|
| 72 |
-
NODE_DISPLAY_NAME_MAPPINGS = {"AKIndexMultiple": "AK Index Multiple"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/AKKSamplerSettings.py
DELETED
|
@@ -1,152 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
import zlib
|
| 3 |
-
import comfy.samplers
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
class AKKSamplerSettings:
|
| 7 |
-
@classmethod
|
| 8 |
-
def INPUT_TYPES(cls):
|
| 9 |
-
return {
|
| 10 |
-
"required": {
|
| 11 |
-
# Don't name this input exactly "seed" or ComfyUI may auto-randomize it
|
| 12 |
-
# depending on global seed behavior, causing this node to look "changed" every run.
|
| 13 |
-
"seed_value": ("INT", {"default": 0, "min": 0, "max": 2147483647, "step": 1}),
|
| 14 |
-
"steps": ("INT", {"default": 20, "min": 1, "max": 10000, "step": 1}),
|
| 15 |
-
"cfg": ("FLOAT", {"default": 7.0, "min": 0.0, "max": 100.0, "step": 0.1}),
|
| 16 |
-
"sampler_name": (comfy.samplers.SAMPLER_NAMES, {"default": comfy.samplers.SAMPLER_NAMES[0]}),
|
| 17 |
-
"scheduler": (comfy.samplers.SCHEDULER_NAMES, {"default": comfy.samplers.SCHEDULER_NAMES[0]}),
|
| 18 |
-
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
| 19 |
-
"xz_steps": ("INT", {"default": 1, "min": 1, "max": 9999, "step": 1}),
|
| 20 |
-
},
|
| 21 |
-
"hidden": {
|
| 22 |
-
"node_id": "UNIQUE_ID",
|
| 23 |
-
},
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
RETURN_TYPES = ("STRING",)
|
| 27 |
-
RETURN_NAMES = ("sampler_settings",)
|
| 28 |
-
FUNCTION = "run"
|
| 29 |
-
CATEGORY = "AK/sampling"
|
| 30 |
-
|
| 31 |
-
def run(
|
| 32 |
-
self,
|
| 33 |
-
seed_value: int,
|
| 34 |
-
steps: int,
|
| 35 |
-
cfg: float,
|
| 36 |
-
sampler_name: str,
|
| 37 |
-
scheduler: str,
|
| 38 |
-
denoise: float,
|
| 39 |
-
xz_steps: int,
|
| 40 |
-
node_id=None,
|
| 41 |
-
):
|
| 42 |
-
from_id = str(node_id) if node_id is not None else ""
|
| 43 |
-
seed = int(seed_value)
|
| 44 |
-
if seed < 0:
|
| 45 |
-
seed = 0
|
| 46 |
-
|
| 47 |
-
data = {
|
| 48 |
-
"seed": seed,
|
| 49 |
-
"steps": int(steps),
|
| 50 |
-
"cfg": float(cfg),
|
| 51 |
-
"sampler_name": str(sampler_name),
|
| 52 |
-
"scheduler": str(scheduler),
|
| 53 |
-
"denoise": float(denoise),
|
| 54 |
-
"xz_steps": int(xz_steps),
|
| 55 |
-
"from_id": from_id,
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
payload = (
|
| 59 |
-
f"{data['seed']}|{data['steps']}|{data['cfg']}|{data['sampler_name']}|"
|
| 60 |
-
f"{data['scheduler']}|{data['denoise']}|{data['xz_steps']}|{from_id}"
|
| 61 |
-
)
|
| 62 |
-
h = zlib.adler32(payload.encode("utf-8")) & 0xFFFFFFFF
|
| 63 |
-
data["hash"] = int(h)
|
| 64 |
-
|
| 65 |
-
s = json.dumps(data, ensure_ascii=False)
|
| 66 |
-
return (s,)
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
class AKKSamplerSettingsOut:
|
| 70 |
-
@classmethod
|
| 71 |
-
def INPUT_TYPES(cls):
|
| 72 |
-
return {
|
| 73 |
-
"required": {
|
| 74 |
-
"sampler_settings": ("STRING", {"forceInput": True}),
|
| 75 |
-
}
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
RETURN_TYPES = (
|
| 79 |
-
"INT",
|
| 80 |
-
"INT",
|
| 81 |
-
"FLOAT",
|
| 82 |
-
comfy.samplers.SAMPLER_NAMES,
|
| 83 |
-
comfy.samplers.SCHEDULER_NAMES,
|
| 84 |
-
"FLOAT",
|
| 85 |
-
"INT",
|
| 86 |
-
)
|
| 87 |
-
RETURN_NAMES = (
|
| 88 |
-
"seed",
|
| 89 |
-
"steps",
|
| 90 |
-
"cfg",
|
| 91 |
-
"sampler_name",
|
| 92 |
-
"scheduler",
|
| 93 |
-
"denoise",
|
| 94 |
-
"xz_steps",
|
| 95 |
-
)
|
| 96 |
-
FUNCTION = "run"
|
| 97 |
-
CATEGORY = "AK/sampling"
|
| 98 |
-
|
| 99 |
-
def run(self, sampler_settings: str):
|
| 100 |
-
data = {}
|
| 101 |
-
if isinstance(sampler_settings, str) and sampler_settings.strip():
|
| 102 |
-
try:
|
| 103 |
-
data = json.loads(sampler_settings)
|
| 104 |
-
if not isinstance(data, dict):
|
| 105 |
-
data = {}
|
| 106 |
-
except Exception:
|
| 107 |
-
data = {}
|
| 108 |
-
|
| 109 |
-
seed = int(data.get("seed", 0) or 0)
|
| 110 |
-
if seed < 0:
|
| 111 |
-
seed = 0
|
| 112 |
-
|
| 113 |
-
steps = int(data.get("steps", 20) or 20)
|
| 114 |
-
|
| 115 |
-
try:
|
| 116 |
-
cfg = float(data.get("cfg", 7.0))
|
| 117 |
-
except Exception:
|
| 118 |
-
cfg = 7.0
|
| 119 |
-
|
| 120 |
-
sampler_name = str(data.get("sampler_name", comfy.samplers.SAMPLER_NAMES[0]))
|
| 121 |
-
if sampler_name not in comfy.samplers.SAMPLER_NAMES:
|
| 122 |
-
sampler_name = comfy.samplers.SAMPLER_NAMES[0]
|
| 123 |
-
|
| 124 |
-
scheduler = str(data.get("scheduler", comfy.samplers.SCHEDULER_NAMES[0]))
|
| 125 |
-
if scheduler not in comfy.samplers.SCHEDULER_NAMES:
|
| 126 |
-
scheduler = comfy.samplers.SCHEDULER_NAMES[0]
|
| 127 |
-
|
| 128 |
-
try:
|
| 129 |
-
denoise = float(data.get("denoise", 1.0))
|
| 130 |
-
except Exception:
|
| 131 |
-
denoise = 1.0
|
| 132 |
-
|
| 133 |
-
xz_steps_raw = data.get("xz_steps", 1)
|
| 134 |
-
try:
|
| 135 |
-
xz_steps = int(xz_steps_raw)
|
| 136 |
-
except Exception:
|
| 137 |
-
xz_steps = 1
|
| 138 |
-
if xz_steps < 1:
|
| 139 |
-
xz_steps = 1
|
| 140 |
-
|
| 141 |
-
return (seed, steps, cfg, sampler_name, scheduler, denoise, xz_steps)
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
NODE_CLASS_MAPPINGS = {
|
| 145 |
-
"AKKSamplerSettings": AKKSamplerSettings,
|
| 146 |
-
"AKKSamplerSettingsOut": AKKSamplerSettingsOut,
|
| 147 |
-
}
|
| 148 |
-
|
| 149 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 150 |
-
"AKKSamplerSettings": "AK KSampler Settings",
|
| 151 |
-
"AKKSamplerSettingsOut": "AK KSampler Settings Out",
|
| 152 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/AKPipeLoop.py
DELETED
|
@@ -1,184 +0,0 @@
|
|
| 1 |
-
# AKPipeLoop.py
|
| 2 |
-
|
| 3 |
-
class AKPipeLoop:
|
| 4 |
-
IDX_HASH = 0
|
| 5 |
-
IDX_MODEL = 1
|
| 6 |
-
IDX_CLIP = 2
|
| 7 |
-
IDX_VAE = 3
|
| 8 |
-
IDX_POS = 4
|
| 9 |
-
IDX_NEG = 5
|
| 10 |
-
IDX_LATENT = 6
|
| 11 |
-
IDX_IMAGE = 7
|
| 12 |
-
|
| 13 |
-
@classmethod
|
| 14 |
-
def INPUT_TYPES(cls):
|
| 15 |
-
return {
|
| 16 |
-
"required": {
|
| 17 |
-
"pipe_in_1": ("AK_PIPE",),
|
| 18 |
-
},
|
| 19 |
-
"optional": {
|
| 20 |
-
"pipe_in_2": ("AK_PIPE",),
|
| 21 |
-
"pipe_in_3": ("AK_PIPE",),
|
| 22 |
-
"pipe_in_4": ("AK_PIPE",),
|
| 23 |
-
"pipe_in_5": ("AK_PIPE",),
|
| 24 |
-
"pipe_in_6": ("AK_PIPE",),
|
| 25 |
-
"pipe_in_7": ("AK_PIPE",),
|
| 26 |
-
"pipe_in_8": ("AK_PIPE",),
|
| 27 |
-
"pipe_in_9": ("AK_PIPE",),
|
| 28 |
-
"pipe_in_10": ("AK_PIPE",),
|
| 29 |
-
},
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
RETURN_TYPES = (
|
| 33 |
-
"AK_PIPE",
|
| 34 |
-
"MODEL",
|
| 35 |
-
"CLIP",
|
| 36 |
-
"VAE",
|
| 37 |
-
"CONDITIONING",
|
| 38 |
-
"CONDITIONING",
|
| 39 |
-
"LATENT",
|
| 40 |
-
"IMAGE",
|
| 41 |
-
)
|
| 42 |
-
|
| 43 |
-
RETURN_NAMES = (
|
| 44 |
-
"pipe_out",
|
| 45 |
-
"model",
|
| 46 |
-
"clip",
|
| 47 |
-
"vae",
|
| 48 |
-
"positive",
|
| 49 |
-
"negative",
|
| 50 |
-
"latent",
|
| 51 |
-
"image",
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
FUNCTION = "run"
|
| 55 |
-
CATEGORY = "AK/pipe"
|
| 56 |
-
OUTPUT_NODE = True
|
| 57 |
-
|
| 58 |
-
@classmethod
|
| 59 |
-
def IS_CHANGED(cls, **kwargs):
|
| 60 |
-
# Force this node to always re-run
|
| 61 |
-
return float("nan")
|
| 62 |
-
|
| 63 |
-
def __init__(self):
|
| 64 |
-
# Stored hashes per input index (0..9 for pipe_in_1..pipe_in_10).
|
| 65 |
-
self._stored_hashes = {}
|
| 66 |
-
|
| 67 |
-
def _normalize_pipe(self, pipe):
|
| 68 |
-
if pipe is None:
|
| 69 |
-
return None
|
| 70 |
-
|
| 71 |
-
if not isinstance(pipe, tuple):
|
| 72 |
-
pipe = tuple(pipe)
|
| 73 |
-
|
| 74 |
-
# Old format: 7-tuple without hash -> prepend None hash.
|
| 75 |
-
if len(pipe) == 7:
|
| 76 |
-
pipe = (None,) + pipe
|
| 77 |
-
elif len(pipe) < 7:
|
| 78 |
-
pipe = (None,) + pipe + (None,) * (7 - len(pipe))
|
| 79 |
-
elif len(pipe) > 8:
|
| 80 |
-
pipe = pipe[:8]
|
| 81 |
-
|
| 82 |
-
return pipe
|
| 83 |
-
|
| 84 |
-
def _get_hash_from_pipe(self, pipe):
|
| 85 |
-
if pipe is None:
|
| 86 |
-
return None
|
| 87 |
-
if not isinstance(pipe, tuple):
|
| 88 |
-
pipe = tuple(pipe)
|
| 89 |
-
if len(pipe) == 0:
|
| 90 |
-
return None
|
| 91 |
-
# If it is old 7-tuple without hash, treat hash as None.
|
| 92 |
-
if len(pipe) == 7:
|
| 93 |
-
return None
|
| 94 |
-
return pipe[self.IDX_HASH]
|
| 95 |
-
|
| 96 |
-
def _outputs_from_pipe(self, pipe):
|
| 97 |
-
# Assumes pipe is already normalized to at least 8 elements.
|
| 98 |
-
model = pipe[self.IDX_MODEL]
|
| 99 |
-
clip = pipe[self.IDX_CLIP]
|
| 100 |
-
vae = pipe[self.IDX_VAE]
|
| 101 |
-
positive = pipe[self.IDX_POS]
|
| 102 |
-
negative = pipe[self.IDX_NEG]
|
| 103 |
-
latent = pipe[self.IDX_LATENT]
|
| 104 |
-
image = pipe[self.IDX_IMAGE]
|
| 105 |
-
return (pipe, model, clip, vae, positive, negative, latent, image)
|
| 106 |
-
|
| 107 |
-
def run(
|
| 108 |
-
self,
|
| 109 |
-
pipe_in_1,
|
| 110 |
-
pipe_in_2=None,
|
| 111 |
-
pipe_in_3=None,
|
| 112 |
-
pipe_in_4=None,
|
| 113 |
-
pipe_in_5=None,
|
| 114 |
-
pipe_in_6=None,
|
| 115 |
-
pipe_in_7=None,
|
| 116 |
-
pipe_in_8=None,
|
| 117 |
-
pipe_in_9=None,
|
| 118 |
-
pipe_in_10=None,
|
| 119 |
-
):
|
| 120 |
-
inputs = [
|
| 121 |
-
pipe_in_1,
|
| 122 |
-
pipe_in_2,
|
| 123 |
-
pipe_in_3,
|
| 124 |
-
pipe_in_4,
|
| 125 |
-
pipe_in_5,
|
| 126 |
-
pipe_in_6,
|
| 127 |
-
pipe_in_7,
|
| 128 |
-
pipe_in_8,
|
| 129 |
-
pipe_in_9,
|
| 130 |
-
pipe_in_10,
|
| 131 |
-
]
|
| 132 |
-
|
| 133 |
-
current_hashes = {}
|
| 134 |
-
changed_indices = []
|
| 135 |
-
normalized_pipes = {}
|
| 136 |
-
|
| 137 |
-
# First pass: compute hashes for all valid inputs and detect changes
|
| 138 |
-
for idx, raw_pipe in enumerate(inputs):
|
| 139 |
-
if raw_pipe is None:
|
| 140 |
-
continue
|
| 141 |
-
|
| 142 |
-
pipe = self._normalize_pipe(raw_pipe)
|
| 143 |
-
if pipe is None:
|
| 144 |
-
continue
|
| 145 |
-
|
| 146 |
-
normalized_pipes[idx] = pipe
|
| 147 |
-
h = self._get_hash_from_pipe(pipe)
|
| 148 |
-
current_hashes[idx] = h
|
| 149 |
-
|
| 150 |
-
prev = self._stored_hashes.get(idx, None)
|
| 151 |
-
if idx not in self._stored_hashes or h != prev:
|
| 152 |
-
changed_indices.append(idx)
|
| 153 |
-
|
| 154 |
-
# Update stored hashes snapshot to current state
|
| 155 |
-
self._stored_hashes = current_hashes
|
| 156 |
-
|
| 157 |
-
# If there is at least one changed input, use the first one
|
| 158 |
-
if changed_indices:
|
| 159 |
-
first_idx = changed_indices[0]
|
| 160 |
-
pipe = normalized_pipes[first_idx]
|
| 161 |
-
return self._outputs_from_pipe(pipe)
|
| 162 |
-
|
| 163 |
-
# No hashes changed: choose the last non-None current input
|
| 164 |
-
last_pipe = None
|
| 165 |
-
for raw_pipe in inputs:
|
| 166 |
-
if raw_pipe is not None:
|
| 167 |
-
p = self._normalize_pipe(raw_pipe)
|
| 168 |
-
if p is not None:
|
| 169 |
-
last_pipe = p
|
| 170 |
-
|
| 171 |
-
if last_pipe is not None:
|
| 172 |
-
return self._outputs_from_pipe(last_pipe)
|
| 173 |
-
|
| 174 |
-
# Degenerate empty outputs if nothing valid found
|
| 175 |
-
return (None, None, None, None, None, None, None, None)
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
NODE_CLASS_MAPPINGS = {
|
| 179 |
-
"AK Pipe Loop": AKPipeLoop,
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 183 |
-
"AK Pipe Loop": "AK Pipe Loop",
|
| 184 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/AKProjectSettingsOut.py
DELETED
|
@@ -1,178 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
import io
|
| 3 |
-
import base64
|
| 4 |
-
import os
|
| 5 |
-
|
| 6 |
-
from PIL import Image
|
| 7 |
-
import numpy as np
|
| 8 |
-
import torch
|
| 9 |
-
import folder_paths
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
_GARBAGE_SUBFOLDER = "garbage"
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
def _is_under_dir(path: str, root: str) -> bool:
|
| 16 |
-
try:
|
| 17 |
-
path_abs = os.path.abspath(path)
|
| 18 |
-
root_abs = os.path.abspath(root)
|
| 19 |
-
return os.path.commonpath([path_abs, root_abs]) == root_abs
|
| 20 |
-
except Exception:
|
| 21 |
-
return False
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def _safe_join_under(root: str, *parts: str) -> str:
|
| 25 |
-
joined = os.path.abspath(os.path.join(root, *[str(p or "") for p in parts]))
|
| 26 |
-
if not _is_under_dir(joined, root):
|
| 27 |
-
return ""
|
| 28 |
-
return joined
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def _load_image_to_tensor(abs_path: str):
|
| 32 |
-
img = Image.open(abs_path).convert("RGB")
|
| 33 |
-
np_img = np.array(img).astype(np.float32) / 255.0
|
| 34 |
-
return torch.from_numpy(np_img)[None,]
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def _find_first_subfolder_abs_path_by_filename(filename: str) -> str:
|
| 38 |
-
if not filename:
|
| 39 |
-
return ""
|
| 40 |
-
|
| 41 |
-
input_dir_abs = os.path.abspath(folder_paths.get_input_directory())
|
| 42 |
-
|
| 43 |
-
target = os.path.basename(str(filename)).strip()
|
| 44 |
-
if not target:
|
| 45 |
-
return ""
|
| 46 |
-
|
| 47 |
-
target_l = target.lower()
|
| 48 |
-
garbage_root = os.path.join(input_dir_abs, _GARBAGE_SUBFOLDER)
|
| 49 |
-
|
| 50 |
-
for dirpath, dirnames, filenames in os.walk(input_dir_abs, topdown=False):
|
| 51 |
-
dirpath_abs = os.path.abspath(dirpath)
|
| 52 |
-
|
| 53 |
-
# ignore input root
|
| 54 |
-
if dirpath_abs == input_dir_abs:
|
| 55 |
-
continue
|
| 56 |
-
|
| 57 |
-
# ignore /input/garbage/ (and any nested subfolders)
|
| 58 |
-
if _is_under_dir(dirpath_abs, garbage_root):
|
| 59 |
-
continue
|
| 60 |
-
|
| 61 |
-
for fn in filenames:
|
| 62 |
-
if str(fn).lower() == target_l:
|
| 63 |
-
return os.path.join(dirpath_abs, "")
|
| 64 |
-
|
| 65 |
-
return ""
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
class AKProjectSettingsOut:
|
| 69 |
-
@classmethod
|
| 70 |
-
def INPUT_TYPES(cls):
|
| 71 |
-
return {
|
| 72 |
-
"required": {
|
| 73 |
-
"ak_project_settings_json": ("STRING", {"default": "", "multiline": False}),
|
| 74 |
-
}
|
| 75 |
-
}
|
| 76 |
-
|
| 77 |
-
RETURN_TYPES = (
|
| 78 |
-
"STRING",
|
| 79 |
-
"STRING",
|
| 80 |
-
"INT",
|
| 81 |
-
"INT",
|
| 82 |
-
"BOOLEAN",
|
| 83 |
-
"IMAGE",
|
| 84 |
-
"STRING",
|
| 85 |
-
"STRING",
|
| 86 |
-
)
|
| 87 |
-
|
| 88 |
-
RETURN_NAMES = (
|
| 89 |
-
"output_filename",
|
| 90 |
-
"output_subfolder",
|
| 91 |
-
"width",
|
| 92 |
-
"height",
|
| 93 |
-
"do_resize",
|
| 94 |
-
"image",
|
| 95 |
-
"image_filename",
|
| 96 |
-
"image_filepath",
|
| 97 |
-
)
|
| 98 |
-
|
| 99 |
-
FUNCTION = "run"
|
| 100 |
-
CATEGORY = "AK/settings"
|
| 101 |
-
DESCRIPTION = (
|
| 102 |
-
"Outputs the values of fields defined in the Project Settings panel. "
|
| 103 |
-
"Additionally, it can resolve and output the image file path for images "
|
| 104 |
-
"located in ComfyUI’s native input directory, including nested subfolders."
|
| 105 |
-
)
|
| 106 |
-
|
| 107 |
-
def run(self, ak_project_settings_json):
|
| 108 |
-
try:
|
| 109 |
-
vals = json.loads(ak_project_settings_json or "{}")
|
| 110 |
-
except Exception:
|
| 111 |
-
vals = {}
|
| 112 |
-
|
| 113 |
-
output_filename = str(vals.get("output_filename", ""))
|
| 114 |
-
output_subfolder = str(vals.get("output_subfolder", ""))
|
| 115 |
-
width = int(vals.get("width", 0) or 0)
|
| 116 |
-
height = int(vals.get("height", 0) or 0)
|
| 117 |
-
|
| 118 |
-
do_resize_raw = int(vals.get("do_resize", 0) or 0)
|
| 119 |
-
do_resize = bool(do_resize_raw == 1)
|
| 120 |
-
|
| 121 |
-
image = None
|
| 122 |
-
|
| 123 |
-
open_image_filename = str(vals.get("open_image_filename") or "").strip()
|
| 124 |
-
open_image_subfolder = str(vals.get("open_image_subfolder") or "").strip()
|
| 125 |
-
open_image_type = str(vals.get("open_image_type") or "input").strip() or "input"
|
| 126 |
-
|
| 127 |
-
image_filename = os.path.splitext(os.path.basename(open_image_filename))[0] if open_image_filename else ""
|
| 128 |
-
|
| 129 |
-
# image_filepath: folder path (abs) where the file is found in input subfolders; ignore /input/garbage/
|
| 130 |
-
image_filepath = ""
|
| 131 |
-
try:
|
| 132 |
-
if open_image_filename:
|
| 133 |
-
image_filepath = _find_first_subfolder_abs_path_by_filename(open_image_filename) or ""
|
| 134 |
-
except Exception:
|
| 135 |
-
image_filepath = ""
|
| 136 |
-
|
| 137 |
-
# Prefer loading by (filename, subfolder, type) from input directory
|
| 138 |
-
if open_image_filename and open_image_type == "input":
|
| 139 |
-
try:
|
| 140 |
-
input_dir_abs = os.path.abspath(folder_paths.get_input_directory())
|
| 141 |
-
abs_path = _safe_join_under(input_dir_abs, open_image_subfolder, open_image_filename)
|
| 142 |
-
if abs_path and os.path.isfile(abs_path):
|
| 143 |
-
image = _load_image_to_tensor(abs_path)
|
| 144 |
-
except Exception:
|
| 145 |
-
image = None
|
| 146 |
-
|
| 147 |
-
# Backward compatibility: if JSON still contains a data URL, try loading from it
|
| 148 |
-
if image is None:
|
| 149 |
-
image_data = vals.get("open_image", "")
|
| 150 |
-
if isinstance(image_data, str) and image_data.startswith("data:image"):
|
| 151 |
-
try:
|
| 152 |
-
_, b64 = image_data.split(",", 1)
|
| 153 |
-
raw = base64.b64decode(b64)
|
| 154 |
-
img = Image.open(io.BytesIO(raw)).convert("RGB")
|
| 155 |
-
np_img = np.array(img).astype(np.float32) / 255.0
|
| 156 |
-
image = torch.from_numpy(np_img)[None,]
|
| 157 |
-
except Exception:
|
| 158 |
-
image = None
|
| 159 |
-
|
| 160 |
-
return (
|
| 161 |
-
output_filename,
|
| 162 |
-
output_subfolder,
|
| 163 |
-
width,
|
| 164 |
-
height,
|
| 165 |
-
do_resize,
|
| 166 |
-
image,
|
| 167 |
-
image_filename,
|
| 168 |
-
image_filepath,
|
| 169 |
-
)
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
NODE_CLASS_MAPPINGS = {
|
| 173 |
-
"AKProjectSettingsOut": AKProjectSettingsOut
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 177 |
-
"AKProjectSettingsOut": "AK Project Settings Out"
|
| 178 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/AKReplaceAlphaWithColor.py
DELETED
|
@@ -1,201 +0,0 @@
|
|
| 1 |
-
import torch
|
| 2 |
-
import torch.nn.functional as F
|
| 3 |
-
import re
|
| 4 |
-
|
| 5 |
-
class AKReplaceAlphaWithColor:
|
| 6 |
-
@classmethod
|
| 7 |
-
def INPUT_TYPES(cls):
|
| 8 |
-
return {
|
| 9 |
-
"required": {
|
| 10 |
-
"color_pick_mode": (["user_color", "auto_color"], {"default": "user_color"}),
|
| 11 |
-
"threshold": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}),
|
| 12 |
-
"image": ("IMAGE",),
|
| 13 |
-
"color_rgb": ("STRING", {"default": "8, 39, 245", "multiline": False}),
|
| 14 |
-
"pad_size": ("INT", {"default": 0, "min": 0, "step": 1}),
|
| 15 |
-
},
|
| 16 |
-
"optional": {
|
| 17 |
-
"mask": ("MASK",),
|
| 18 |
-
},
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
RETURN_TYPES = ("IMAGE",)
|
| 22 |
-
FUNCTION = "replace_alpha"
|
| 23 |
-
CATEGORY = "AK/image"
|
| 24 |
-
|
| 25 |
-
@staticmethod
|
| 26 |
-
def _parse_rgb(s: str):
|
| 27 |
-
parts = [p for p in re.split(r"[,\s;]+", (s or "").strip()) if p != ""]
|
| 28 |
-
if len(parts) != 3:
|
| 29 |
-
return (8, 39, 245)
|
| 30 |
-
vals = []
|
| 31 |
-
for p in parts:
|
| 32 |
-
try:
|
| 33 |
-
v = int(float(p))
|
| 34 |
-
except Exception:
|
| 35 |
-
return (8, 39, 245)
|
| 36 |
-
vals.append(max(0, min(255, v)))
|
| 37 |
-
return tuple(vals)
|
| 38 |
-
|
| 39 |
-
@staticmethod
|
| 40 |
-
def _auto_pick_color(src_rgb_01: torch.Tensor, threshold_255: int):
|
| 41 |
-
if src_rgb_01.dim() != 4 or src_rgb_01.shape[-1] != 3:
|
| 42 |
-
return None
|
| 43 |
-
|
| 44 |
-
thr = int(threshold_255)
|
| 45 |
-
if thr < 0:
|
| 46 |
-
thr = 0
|
| 47 |
-
elif thr > 255:
|
| 48 |
-
thr = 255
|
| 49 |
-
|
| 50 |
-
pix = src_rgb_01.reshape(-1, 3).clamp(0.0, 1.0)
|
| 51 |
-
n = int(pix.shape[0])
|
| 52 |
-
if n <= 0:
|
| 53 |
-
return None
|
| 54 |
-
|
| 55 |
-
max_samples = 20000
|
| 56 |
-
if n > max_samples:
|
| 57 |
-
idx = torch.randperm(n, device=pix.device)[:max_samples]
|
| 58 |
-
pix = pix.index_select(0, idx)
|
| 59 |
-
|
| 60 |
-
pix255 = (pix * 255.0).round()
|
| 61 |
-
|
| 62 |
-
step = 16
|
| 63 |
-
vals = torch.arange(0, 256, step, device=pix.device, dtype=pix255.dtype)
|
| 64 |
-
|
| 65 |
-
try:
|
| 66 |
-
rr, gg, bb = torch.meshgrid(vals, vals, vals, indexing="ij")
|
| 67 |
-
except TypeError:
|
| 68 |
-
rr, gg, bb = torch.meshgrid(vals, vals, vals)
|
| 69 |
-
|
| 70 |
-
cand = torch.stack([rr.reshape(-1), gg.reshape(-1), bb.reshape(-1)], dim=1)
|
| 71 |
-
m = int(cand.shape[0])
|
| 72 |
-
|
| 73 |
-
best_min_d2 = None
|
| 74 |
-
best = None
|
| 75 |
-
|
| 76 |
-
pix255_t = pix255.t().contiguous()
|
| 77 |
-
p2 = (pix255 * pix255).sum(dim=1).view(1, -1)
|
| 78 |
-
|
| 79 |
-
chunk = 256
|
| 80 |
-
for s in range(0, m, chunk):
|
| 81 |
-
c = cand[s:s + chunk]
|
| 82 |
-
c2 = (c * c).sum(dim=1, keepdim=True)
|
| 83 |
-
cross = 2.0 * (c @ pix255_t)
|
| 84 |
-
d2 = (c2 + p2 - cross).clamp_min(0.0)
|
| 85 |
-
min_d2 = d2.min(dim=1).values
|
| 86 |
-
|
| 87 |
-
cur_best_val = min_d2.max()
|
| 88 |
-
if best_min_d2 is None or cur_best_val > best_min_d2:
|
| 89 |
-
best_min_d2 = cur_best_val
|
| 90 |
-
best_idx = int(min_d2.argmax().item())
|
| 91 |
-
best = c[best_idx]
|
| 92 |
-
|
| 93 |
-
if best is None or best_min_d2 is None:
|
| 94 |
-
return None
|
| 95 |
-
|
| 96 |
-
thr2 = float(thr * thr)
|
| 97 |
-
if float(best_min_d2.item()) <= thr2 + 1e-6:
|
| 98 |
-
return None
|
| 99 |
-
|
| 100 |
-
return (int(best[0].item()), int(best[1].item()), int(best[2].item()))
|
| 101 |
-
|
| 102 |
-
@staticmethod
|
| 103 |
-
def _compute_pad(h: int, w: int, pad_size: int):
|
| 104 |
-
p = int(pad_size)
|
| 105 |
-
if p <= 0:
|
| 106 |
-
return (0, 0, 0, 0)
|
| 107 |
-
|
| 108 |
-
if w >= h:
|
| 109 |
-
add_w = p
|
| 110 |
-
add_h = int(round(p * (h / max(1, w))))
|
| 111 |
-
else:
|
| 112 |
-
add_h = p
|
| 113 |
-
add_w = int(round(p * (w / max(1, h))))
|
| 114 |
-
|
| 115 |
-
left = add_w // 2
|
| 116 |
-
right = add_w - left
|
| 117 |
-
top = add_h // 2
|
| 118 |
-
bottom = add_h - top
|
| 119 |
-
return (left, right, top, bottom)
|
| 120 |
-
|
| 121 |
-
def replace_alpha(self, color_pick_mode, threshold, image, color_rgb, pad_size=0, mask=None):
|
| 122 |
-
if image.dim() != 4:
|
| 123 |
-
return (image,)
|
| 124 |
-
|
| 125 |
-
c = image.shape[-1]
|
| 126 |
-
if c < 3:
|
| 127 |
-
return (image,)
|
| 128 |
-
|
| 129 |
-
src_rgb = image[..., :3].clamp(0.0, 1.0)
|
| 130 |
-
|
| 131 |
-
# If a mask is provided but the input image has no alpha channel, do nothing.
|
| 132 |
-
if mask is not None and c < 4:
|
| 133 |
-
return (image,)
|
| 134 |
-
|
| 135 |
-
mode = str(color_pick_mode or "user_color")
|
| 136 |
-
thr = int(threshold) if threshold is not None else 0
|
| 137 |
-
|
| 138 |
-
if mode == "auto_color":
|
| 139 |
-
picked = self._auto_pick_color(src_rgb, thr)
|
| 140 |
-
if picked is None:
|
| 141 |
-
picked = self._parse_rgb(color_rgb)
|
| 142 |
-
else:
|
| 143 |
-
picked = self._parse_rgb(color_rgb)
|
| 144 |
-
|
| 145 |
-
color = torch.tensor([picked[0], picked[1], picked[2]], device=image.device, dtype=image.dtype) / 255.0
|
| 146 |
-
color_view = color.view(1, 1, 1, 3).expand_as(src_rgb)
|
| 147 |
-
|
| 148 |
-
if mask is not None:
|
| 149 |
-
# Mask must match image spatial resolution.
|
| 150 |
-
try:
|
| 151 |
-
ih = int(image.shape[1])
|
| 152 |
-
iw = int(image.shape[2])
|
| 153 |
-
if mask.dim() == 3:
|
| 154 |
-
mh, mw = int(mask.shape[1]), int(mask.shape[2])
|
| 155 |
-
elif mask.dim() == 4:
|
| 156 |
-
mh, mw = int(mask.shape[1]), int(mask.shape[2])
|
| 157 |
-
else:
|
| 158 |
-
return (image,)
|
| 159 |
-
if mh != ih or mw != iw:
|
| 160 |
-
return (image,)
|
| 161 |
-
except Exception:
|
| 162 |
-
return (image,)
|
| 163 |
-
m = mask
|
| 164 |
-
if m.dim() == 3:
|
| 165 |
-
m = m.unsqueeze(-1)
|
| 166 |
-
elif m.dim() == 4 and m.shape[-1] != 1:
|
| 167 |
-
m = m[..., :1]
|
| 168 |
-
m = m.to(device=image.device, dtype=image.dtype).clamp(0.0, 1.0)
|
| 169 |
-
out = src_rgb * (1.0 - m) + color_view * m
|
| 170 |
-
else:
|
| 171 |
-
if c < 4:
|
| 172 |
-
out = image
|
| 173 |
-
else:
|
| 174 |
-
alpha = image[..., 3:4].clamp(0.0, 1.0)
|
| 175 |
-
out = src_rgb * alpha + color_view * (1.0 - alpha)
|
| 176 |
-
|
| 177 |
-
p = int(pad_size) if pad_size is not None else 0
|
| 178 |
-
if p > 0 and out.dim() == 4 and out.shape[-1] >= 3:
|
| 179 |
-
b, h, w, ch = out.shape
|
| 180 |
-
left, right, top, bottom = self._compute_pad(h, w, p)
|
| 181 |
-
if left or right or top or bottom:
|
| 182 |
-
out_nchw = out[..., :3].permute(0, 3, 1, 2).contiguous()
|
| 183 |
-
padded = []
|
| 184 |
-
for ci in range(3):
|
| 185 |
-
v = float(color[ci].item())
|
| 186 |
-
ch_t = out_nchw[:, ci:ci+1, :, :]
|
| 187 |
-
ch_p = F.pad(ch_t, (left, right, top, bottom), mode="constant", value=v)
|
| 188 |
-
padded.append(ch_p)
|
| 189 |
-
out_nchw = torch.cat(padded, dim=1)
|
| 190 |
-
out = out_nchw.permute(0, 2, 3, 1).contiguous()
|
| 191 |
-
|
| 192 |
-
return (out,)
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
NODE_CLASS_MAPPINGS = {
|
| 196 |
-
"AK Replace Alpha with Color": AKReplaceAlphaWithColor,
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 200 |
-
"AK Replace Alpha with Color": "AK Replace Alpha with Color",
|
| 201 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/AKReplaceColorWithAlpha.py
DELETED
|
@@ -1,153 +0,0 @@
|
|
| 1 |
-
import torch
|
| 2 |
-
import re
|
| 3 |
-
|
| 4 |
-
class AKReplaceColorWithAlpha:
|
| 5 |
-
@classmethod
|
| 6 |
-
def INPUT_TYPES(cls):
|
| 7 |
-
return {
|
| 8 |
-
"required": {
|
| 9 |
-
"image": ("IMAGE",),
|
| 10 |
-
"crop_size": ("INT", {"default": 0, "min": 0, "step": 1}),
|
| 11 |
-
"color_pick_mode": (["user_color", "left_top_pixel", "right_top_pixel", "left_bottom_pixel", "right_bottom_pixel"], {"default": "user_color"}),
|
| 12 |
-
"color_rgb": ("STRING", {"default": "8, 39, 245", "multiline": False}),
|
| 13 |
-
"threshold": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}),
|
| 14 |
-
"softness": ("INT", {"default": 0, "min": 0, "max": 255, "step": 1}),
|
| 15 |
-
}
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
RETURN_TYPES = ("IMAGE",)
|
| 19 |
-
FUNCTION = "replace_color"
|
| 20 |
-
CATEGORY = "AK/image"
|
| 21 |
-
|
| 22 |
-
@staticmethod
|
| 23 |
-
def _parse_rgb(s: str):
|
| 24 |
-
parts = [p for p in re.split(r"[,\s;]+", (s or "").strip()) if p != ""]
|
| 25 |
-
if len(parts) != 3:
|
| 26 |
-
return (8, 39, 245)
|
| 27 |
-
vals = []
|
| 28 |
-
for p in parts:
|
| 29 |
-
try:
|
| 30 |
-
v = int(float(p))
|
| 31 |
-
except Exception:
|
| 32 |
-
return (8, 39, 245)
|
| 33 |
-
vals.append(max(0, min(255, v)))
|
| 34 |
-
return tuple(vals)
|
| 35 |
-
|
| 36 |
-
@staticmethod
|
| 37 |
-
def _pick_corner_color(src_rgb: torch.Tensor, mode: str):
|
| 38 |
-
# src_rgb: [B, H, W, 3] in 0..1
|
| 39 |
-
_, h, w, _ = src_rgb.shape
|
| 40 |
-
if mode == "left_top_pixel":
|
| 41 |
-
y, x = 0, 0
|
| 42 |
-
elif mode == "right_top_pixel":
|
| 43 |
-
y, x = 0, max(0, w - 1)
|
| 44 |
-
elif mode == "left_bottom_pixel":
|
| 45 |
-
y, x = max(0, h - 1), 0
|
| 46 |
-
elif mode == "right_bottom_pixel":
|
| 47 |
-
y, x = max(0, h - 1), max(0, w - 1)
|
| 48 |
-
else:
|
| 49 |
-
y, x = 0, 0
|
| 50 |
-
return src_rgb[0, y, x, :].clamp(0.0, 1.0)
|
| 51 |
-
|
| 52 |
-
@staticmethod
|
| 53 |
-
def _compute_crop(h: int, w: int, crop_size: int):
|
| 54 |
-
p = int(crop_size)
|
| 55 |
-
if p <= 0:
|
| 56 |
-
return (0, 0, 0, 0)
|
| 57 |
-
|
| 58 |
-
# Preserve aspect: shrink the larger dimension by p, shrink the other proportionally.
|
| 59 |
-
if w >= h:
|
| 60 |
-
sub_w = min(p, max(0, w - 1))
|
| 61 |
-
sub_h = int(round(sub_w * (h / max(1, w))))
|
| 62 |
-
else:
|
| 63 |
-
sub_h = min(p, max(0, h - 1))
|
| 64 |
-
sub_w = int(round(sub_h * (w / max(1, h))))
|
| 65 |
-
|
| 66 |
-
left = sub_w // 2
|
| 67 |
-
right = sub_w - left
|
| 68 |
-
top = sub_h // 2
|
| 69 |
-
bottom = sub_h - top
|
| 70 |
-
return (left, right, top, bottom)
|
| 71 |
-
|
| 72 |
-
@staticmethod
|
| 73 |
-
def _center_crop(img: torch.Tensor, crop_size: int):
|
| 74 |
-
# img: [B,H,W,C]
|
| 75 |
-
if img is None or img.dim() != 4:
|
| 76 |
-
return img
|
| 77 |
-
b, h, w, c = img.shape
|
| 78 |
-
left, right, top, bottom = AKReplaceColorWithAlpha._compute_crop(h, w, crop_size)
|
| 79 |
-
if left == 0 and right == 0 and top == 0 and bottom == 0:
|
| 80 |
-
return img
|
| 81 |
-
y0 = top
|
| 82 |
-
y1 = h - bottom
|
| 83 |
-
x0 = left
|
| 84 |
-
x1 = w - right
|
| 85 |
-
if y1 <= y0 or x1 <= x0:
|
| 86 |
-
return img
|
| 87 |
-
return img[:, y0:y1, x0:x1, :]
|
| 88 |
-
|
| 89 |
-
def replace_color(self, image, crop_size, color_pick_mode, color_rgb, threshold, softness):
|
| 90 |
-
if image.dim() != 4:
|
| 91 |
-
return (image,)
|
| 92 |
-
|
| 93 |
-
cs = int(crop_size) if crop_size is not None else 0
|
| 94 |
-
if cs > 0:
|
| 95 |
-
image = self._center_crop(image, cs)
|
| 96 |
-
|
| 97 |
-
c = image.shape[-1]
|
| 98 |
-
if c < 3:
|
| 99 |
-
return (image,)
|
| 100 |
-
|
| 101 |
-
src_rgb = image[..., :3].clamp(0.0, 1.0)
|
| 102 |
-
|
| 103 |
-
if c >= 4:
|
| 104 |
-
src_a = image[..., 3:4].clamp(0.0, 1.0)
|
| 105 |
-
else:
|
| 106 |
-
src_a = torch.ones_like(image[..., :1])
|
| 107 |
-
|
| 108 |
-
mode = str(color_pick_mode or "user_color")
|
| 109 |
-
|
| 110 |
-
if mode == "user_color":
|
| 111 |
-
rgb = self._parse_rgb(color_rgb)
|
| 112 |
-
target = torch.tensor([rgb[0], rgb[1], rgb[2]], device=image.device, dtype=image.dtype) / 255.0
|
| 113 |
-
else:
|
| 114 |
-
target = self._pick_corner_color(src_rgb, mode).to(device=image.device, dtype=image.dtype)
|
| 115 |
-
|
| 116 |
-
thr = int(threshold) if threshold is not None else 0
|
| 117 |
-
soft = int(softness) if softness is not None else 0
|
| 118 |
-
if thr < 0:
|
| 119 |
-
thr = 0
|
| 120 |
-
elif thr > 255:
|
| 121 |
-
thr = 255
|
| 122 |
-
if soft < 0:
|
| 123 |
-
soft = 0
|
| 124 |
-
elif soft > 255:
|
| 125 |
-
soft = 255
|
| 126 |
-
|
| 127 |
-
t0 = float(thr) / 255.0
|
| 128 |
-
t1 = float(min(255, thr + soft)) / 255.0
|
| 129 |
-
|
| 130 |
-
d2 = (src_rgb - target.view(1, 1, 1, 3)).pow(2).sum(dim=-1, keepdim=True)
|
| 131 |
-
|
| 132 |
-
if soft <= 0:
|
| 133 |
-
sel = d2 <= (t0 * t0 + 1e-12)
|
| 134 |
-
out_a = torch.where(sel, torch.zeros_like(src_a), src_a)
|
| 135 |
-
else:
|
| 136 |
-
d = torch.sqrt(d2 + 1e-12)
|
| 137 |
-
denom = t1 - t0
|
| 138 |
-
if denom <= 1e-8:
|
| 139 |
-
denom = 1e-8
|
| 140 |
-
alpha_factor = ((d - t0) / denom).clamp(0.0, 1.0)
|
| 141 |
-
out_a = (src_a * alpha_factor).clamp(0.0, 1.0)
|
| 142 |
-
|
| 143 |
-
out = torch.cat([src_rgb, out_a], dim=-1)
|
| 144 |
-
return (out,)
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
NODE_CLASS_MAPPINGS = {
|
| 148 |
-
"AK Replace Color with Alpha": AKReplaceColorWithAlpha,
|
| 149 |
-
}
|
| 150 |
-
|
| 151 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 152 |
-
"AK Replace Color with Alpha": "AK Replace Color with Alpha",
|
| 153 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/CLIPTextEncodeAndCombineCached.py
DELETED
|
@@ -1,68 +0,0 @@
|
|
| 1 |
-
class CLIPTextEncodeAndCombineCached:
|
| 2 |
-
_last_text = None
|
| 3 |
-
_last_encoded = None
|
| 4 |
-
_last_clip_id = None
|
| 5 |
-
|
| 6 |
-
@classmethod
|
| 7 |
-
def INPUT_TYPES(cls):
|
| 8 |
-
return {
|
| 9 |
-
"required": {
|
| 10 |
-
"clip": ("CLIP",),
|
| 11 |
-
"text": ("STRING", {"multiline": True, "default": ""}),
|
| 12 |
-
},
|
| 13 |
-
"optional": {
|
| 14 |
-
"conditioning": ("CONDITIONING",),
|
| 15 |
-
},
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
RETURN_TYPES = ("CONDITIONING",)
|
| 19 |
-
RETURN_NAMES = ("conditioning",)
|
| 20 |
-
FUNCTION = "execute"
|
| 21 |
-
CATEGORY = "AK/conditioning"
|
| 22 |
-
OUTPUT_NODE = False
|
| 23 |
-
|
| 24 |
-
@classmethod
|
| 25 |
-
def _normalize_text(cls, text):
|
| 26 |
-
if text is None:
|
| 27 |
-
return ""
|
| 28 |
-
return str(text).replace("\r\n", "\n").replace("\r", "\n")
|
| 29 |
-
|
| 30 |
-
@classmethod
|
| 31 |
-
def _has_meaningful_text(cls, text):
|
| 32 |
-
return bool(text and text.strip())
|
| 33 |
-
|
| 34 |
-
@classmethod
|
| 35 |
-
def _encode_cached(cls, clip, text):
|
| 36 |
-
clip_id = id(clip)
|
| 37 |
-
if cls._last_clip_id == clip_id and cls._last_text == text and cls._last_encoded is not None:
|
| 38 |
-
return cls._last_encoded
|
| 39 |
-
|
| 40 |
-
tokens = clip.tokenize(text)
|
| 41 |
-
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
|
| 42 |
-
conditioning = [[cond, {"pooled_output": pooled}]]
|
| 43 |
-
|
| 44 |
-
cls._last_clip_id = clip_id
|
| 45 |
-
cls._last_text = text
|
| 46 |
-
cls._last_encoded = conditioning
|
| 47 |
-
|
| 48 |
-
return conditioning
|
| 49 |
-
|
| 50 |
-
@classmethod
|
| 51 |
-
def execute(cls, clip, text, conditioning=None):
|
| 52 |
-
text = cls._normalize_text(text)
|
| 53 |
-
|
| 54 |
-
if not cls._has_meaningful_text(text):
|
| 55 |
-
if conditioning is not None:
|
| 56 |
-
return (conditioning,)
|
| 57 |
-
return ([],)
|
| 58 |
-
|
| 59 |
-
new_cond = cls._encode_cached(clip, text)
|
| 60 |
-
|
| 61 |
-
if conditioning is None:
|
| 62 |
-
return (new_cond,)
|
| 63 |
-
|
| 64 |
-
return (conditioning + new_cond,)
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
NODE_CLASS_MAPPINGS = {"CLIPTextEncodeAndCombineCached": CLIPTextEncodeAndCombineCached}
|
| 68 |
-
NODE_DISPLAY_NAME_MAPPINGS = {"CLIPTextEncodeAndCombineCached": "CLIP Text Encode & Combine (Cached)"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/CLIPTextEncodeCached.py
DELETED
|
@@ -1,50 +0,0 @@
|
|
| 1 |
-
class CLIPTextEncodeCached:
|
| 2 |
-
# Runtime cache: text_hash -> conditioning
|
| 3 |
-
_last_text = None
|
| 4 |
-
_last_cond = None
|
| 5 |
-
_last_clip_id = None
|
| 6 |
-
|
| 7 |
-
@classmethod
|
| 8 |
-
def INPUT_TYPES(cls):
|
| 9 |
-
return {
|
| 10 |
-
"required": {
|
| 11 |
-
"clip": ("CLIP",),
|
| 12 |
-
"text": (
|
| 13 |
-
"STRING",
|
| 14 |
-
{"multiline": True, "default": "", "forceInput": True},
|
| 15 |
-
),
|
| 16 |
-
}
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
RETURN_TYPES = ("CONDITIONING",)
|
| 20 |
-
RETURN_NAMES = ("conditioning",)
|
| 21 |
-
FUNCTION = "execute"
|
| 22 |
-
CATEGORY = "AK/conditioning"
|
| 23 |
-
OUTPUT_NODE = False
|
| 24 |
-
|
| 25 |
-
@classmethod
|
| 26 |
-
def execute(cls, clip, text):
|
| 27 |
-
if text is None:
|
| 28 |
-
text = ""
|
| 29 |
-
|
| 30 |
-
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
| 31 |
-
|
| 32 |
-
clip_id = id(clip)
|
| 33 |
-
|
| 34 |
-
if cls._last_clip_id == clip_id and cls._last_text == text and cls._last_cond is not None:
|
| 35 |
-
return (cls._last_cond,)
|
| 36 |
-
|
| 37 |
-
tokens = clip.tokenize(text)
|
| 38 |
-
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
|
| 39 |
-
conditioning = [[cond, {"pooled_output": pooled}]]
|
| 40 |
-
|
| 41 |
-
cls._last_clip_id = clip_id
|
| 42 |
-
cls._last_text = text
|
| 43 |
-
cls._last_cond = conditioning
|
| 44 |
-
|
| 45 |
-
return (conditioning,)
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
NODE_CLASS_MAPPINGS = {"CLIPTextEncodeCached": CLIPTextEncodeCached}
|
| 49 |
-
|
| 50 |
-
NODE_DISPLAY_NAME_MAPPINGS = {"CLIPTextEncodeCached": "CLIP Text Encode (Cached)"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/Getter.py
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
class AnyType(str):
|
| 2 |
-
def __ne__(self, __value: object) -> bool:
|
| 3 |
-
return False
|
| 4 |
-
|
| 5 |
-
ANY_TYPE = AnyType("*")
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
class Getter:
|
| 9 |
-
@classmethod
|
| 10 |
-
def INPUT_TYPES(cls):
|
| 11 |
-
return {
|
| 12 |
-
"required": {},
|
| 13 |
-
"optional": {
|
| 14 |
-
"inp": (ANY_TYPE,),
|
| 15 |
-
},
|
| 16 |
-
"hidden": {
|
| 17 |
-
"var_name": "STRING",
|
| 18 |
-
"unique_id": "UNIQUE_ID",
|
| 19 |
-
},
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
RETURN_TYPES = (ANY_TYPE,)
|
| 23 |
-
RETURN_NAMES = ("OBJ",)
|
| 24 |
-
FUNCTION = "get"
|
| 25 |
-
CATEGORY = "AK/pipe"
|
| 26 |
-
|
| 27 |
-
def get(self, inp=None, var_name="", unique_id=None):
|
| 28 |
-
if inp is None:
|
| 29 |
-
raise Exception(f"[Getter {unique_id} {var_name}] inp is not connected")
|
| 30 |
-
return (inp,)
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
NODE_CLASS_MAPPINGS = {
|
| 34 |
-
"Getter": Getter,
|
| 35 |
-
}
|
| 36 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 37 |
-
"Getter": "Getter",
|
| 38 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/IsOneOfGroupsActive.py
DELETED
|
@@ -1,43 +0,0 @@
|
|
| 1 |
-
import math
|
| 2 |
-
|
| 3 |
-
class IsOneOfGroupsActive:
|
| 4 |
-
@classmethod
|
| 5 |
-
def INPUT_TYPES(cls):
|
| 6 |
-
return {
|
| 7 |
-
"required": {
|
| 8 |
-
"group_name_contains": (
|
| 9 |
-
"STRING",
|
| 10 |
-
{
|
| 11 |
-
"default": "",
|
| 12 |
-
"multiline": False,
|
| 13 |
-
},
|
| 14 |
-
),
|
| 15 |
-
"active_state": (
|
| 16 |
-
"BOOLEAN",
|
| 17 |
-
{
|
| 18 |
-
"default": False,
|
| 19 |
-
},
|
| 20 |
-
),
|
| 21 |
-
}
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
RETURN_TYPES = ("BOOLEAN",)
|
| 25 |
-
RETURN_NAMES = ("boolean",)
|
| 26 |
-
FUNCTION = "pass_state"
|
| 27 |
-
CATEGORY = "AK/logic"
|
| 28 |
-
|
| 29 |
-
@classmethod
|
| 30 |
-
def IS_CHANGED(cls, group_name_contains, active_state):
|
| 31 |
-
return float("nan")
|
| 32 |
-
|
| 33 |
-
def pass_state(self, group_name_contains, active_state):
|
| 34 |
-
return (active_state,)
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
NODE_CLASS_MAPPINGS = {
|
| 38 |
-
"IsOneOfGroupsActive": IsOneOfGroupsActive,
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 42 |
-
"IsOneOfGroupsActive": "IsOneOfGroupsActive",
|
| 43 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/PreviewRawText.py
DELETED
|
@@ -1,99 +0,0 @@
|
|
| 1 |
-
from __future__ import annotations
|
| 2 |
-
|
| 3 |
-
import json
|
| 4 |
-
import logging
|
| 5 |
-
|
| 6 |
-
logger = logging.getLogger(__name__)
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
class PreviewRawText:
|
| 10 |
-
@classmethod
|
| 11 |
-
def INPUT_TYPES(cls):
|
| 12 |
-
return {
|
| 13 |
-
"required": {},
|
| 14 |
-
"optional": {
|
| 15 |
-
"text": ("STRING", {"forceInput": True}),
|
| 16 |
-
"list_values": (
|
| 17 |
-
"LIST",
|
| 18 |
-
{
|
| 19 |
-
"element_type": "STRING",
|
| 20 |
-
"forceInput": True,
|
| 21 |
-
},
|
| 22 |
-
),
|
| 23 |
-
},
|
| 24 |
-
"hidden": {
|
| 25 |
-
"unique_id": "UNIQUE_ID",
|
| 26 |
-
},
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
RETURN_TYPES = ("STRING",)
|
| 30 |
-
FUNCTION = "notify"
|
| 31 |
-
OUTPUT_NODE = True
|
| 32 |
-
|
| 33 |
-
CATEGORY = "AK/utils"
|
| 34 |
-
|
| 35 |
-
@staticmethod
|
| 36 |
-
def _pretty_json_if_possible(value: str) -> str:
|
| 37 |
-
if value is None:
|
| 38 |
-
return ""
|
| 39 |
-
s = value.strip()
|
| 40 |
-
if not s:
|
| 41 |
-
return value
|
| 42 |
-
if s[0] not in "{[":
|
| 43 |
-
return value
|
| 44 |
-
try:
|
| 45 |
-
obj = json.loads(s)
|
| 46 |
-
except Exception:
|
| 47 |
-
return value
|
| 48 |
-
try:
|
| 49 |
-
return json.dumps(obj, ensure_ascii=False, indent=2)
|
| 50 |
-
except Exception:
|
| 51 |
-
return value
|
| 52 |
-
|
| 53 |
-
def notify(self, text=None, list_values=None, unique_id=None):
|
| 54 |
-
base_text = "" if text is None else str(text)
|
| 55 |
-
base_text = self._pretty_json_if_possible(base_text)
|
| 56 |
-
|
| 57 |
-
def normalize_list_values(value):
|
| 58 |
-
if value is None:
|
| 59 |
-
return ""
|
| 60 |
-
if not isinstance(value, list):
|
| 61 |
-
v = "NONE" if value is None else str(value)
|
| 62 |
-
return self._pretty_json_if_possible(v)
|
| 63 |
-
|
| 64 |
-
lines = []
|
| 65 |
-
|
| 66 |
-
for v in value:
|
| 67 |
-
if isinstance(v, list):
|
| 68 |
-
for inner in v:
|
| 69 |
-
if inner is None:
|
| 70 |
-
lines.append("NONE")
|
| 71 |
-
else:
|
| 72 |
-
lines.append(self._pretty_json_if_possible(str(inner)))
|
| 73 |
-
else:
|
| 74 |
-
if v is None:
|
| 75 |
-
lines.append("NONE")
|
| 76 |
-
else:
|
| 77 |
-
lines.append(self._pretty_json_if_possible(str(v)))
|
| 78 |
-
|
| 79 |
-
return "\n\n".join(lines)
|
| 80 |
-
|
| 81 |
-
list_text = normalize_list_values(list_values)
|
| 82 |
-
|
| 83 |
-
if base_text and list_text:
|
| 84 |
-
final_text = base_text + "\n\n" + list_text
|
| 85 |
-
elif list_text:
|
| 86 |
-
final_text = list_text
|
| 87 |
-
else:
|
| 88 |
-
final_text = base_text
|
| 89 |
-
|
| 90 |
-
return {"ui": {"text": final_text}, "result": (final_text,)}
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
NODE_CLASS_MAPPINGS = {
|
| 94 |
-
"PreviewRawText": PreviewRawText,
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 98 |
-
"PreviewRawText": "Preview Raw Text",
|
| 99 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/RepeatGroupState.py
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
class RepeatGroupState:
|
| 2 |
-
@classmethod
|
| 3 |
-
def INPUT_TYPES(cls):
|
| 4 |
-
return {
|
| 5 |
-
"required": {
|
| 6 |
-
"group_name_contains": (
|
| 7 |
-
"STRING",
|
| 8 |
-
{
|
| 9 |
-
"default": "",
|
| 10 |
-
"multiline": False,
|
| 11 |
-
},
|
| 12 |
-
),
|
| 13 |
-
}
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
RETURN_TYPES = ()
|
| 17 |
-
RETURN_NAMES = ()
|
| 18 |
-
FUNCTION = "do_nothing"
|
| 19 |
-
CATEGORY = "AK/logic"
|
| 20 |
-
|
| 21 |
-
@classmethod
|
| 22 |
-
def IS_CHANGED(cls, group_name_contains):
|
| 23 |
-
return float("nan")
|
| 24 |
-
|
| 25 |
-
def do_nothing(self, group_name_contains):
|
| 26 |
-
return ()
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
NODE_CLASS_MAPPINGS = {
|
| 30 |
-
"RepeatGroupState": RepeatGroupState,
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 34 |
-
"RepeatGroupState": "Repeat Group State",
|
| 35 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/nodes/Setter.py
DELETED
|
@@ -1,136 +0,0 @@
|
|
| 1 |
-
import sys, types, bisect
|
| 2 |
-
|
| 3 |
-
_STORE_KEY = "ak_var_nodes_store"
|
| 4 |
-
|
| 5 |
-
class AnyType(str):
|
| 6 |
-
def __ne__(self, __value: object) -> bool:
|
| 7 |
-
return False
|
| 8 |
-
|
| 9 |
-
ANY_TYPE = AnyType("*")
|
| 10 |
-
|
| 11 |
-
def _get_store():
|
| 12 |
-
st = sys.modules.get(_STORE_KEY)
|
| 13 |
-
if st is None:
|
| 14 |
-
st = types.SimpleNamespace(
|
| 15 |
-
last_prompt_obj_id=None,
|
| 16 |
-
allowed_ids_by_name={},
|
| 17 |
-
values_by_name={},
|
| 18 |
-
names_sorted=[],
|
| 19 |
-
last_name_by_setter_id={}
|
| 20 |
-
)
|
| 21 |
-
sys.modules[_STORE_KEY] = st
|
| 22 |
-
if not hasattr(st, "last_name_by_setter_id"):
|
| 23 |
-
st.last_name_by_setter_id = {}
|
| 24 |
-
return st
|
| 25 |
-
|
| 26 |
-
def _rebuild_from_prompt(st, prompt):
|
| 27 |
-
st.allowed_ids_by_name.clear()
|
| 28 |
-
st.names_sorted.clear()
|
| 29 |
-
valid_names = set()
|
| 30 |
-
|
| 31 |
-
try:
|
| 32 |
-
if isinstance(prompt, dict):
|
| 33 |
-
for node_id, node in prompt.items():
|
| 34 |
-
if not isinstance(node, dict):
|
| 35 |
-
continue
|
| 36 |
-
if node.get("class_type") != "Setter":
|
| 37 |
-
continue
|
| 38 |
-
inputs = node.get("inputs") or {}
|
| 39 |
-
name = inputs.get("var_name", "")
|
| 40 |
-
if name is None:
|
| 41 |
-
name = ""
|
| 42 |
-
if not isinstance(name, str):
|
| 43 |
-
name = str(name)
|
| 44 |
-
name = name.strip()
|
| 45 |
-
if not name:
|
| 46 |
-
continue
|
| 47 |
-
|
| 48 |
-
sid = str(node_id)
|
| 49 |
-
old_name = st.last_name_by_setter_id.get(sid)
|
| 50 |
-
|
| 51 |
-
if old_name and old_name != name:
|
| 52 |
-
if name not in st.values_by_name:
|
| 53 |
-
v = st.values_by_name.get(old_name, None)
|
| 54 |
-
if v is not None:
|
| 55 |
-
st.values_by_name[name] = v
|
| 56 |
-
if old_name in st.values_by_name and old_name != name:
|
| 57 |
-
del st.values_by_name[old_name]
|
| 58 |
-
|
| 59 |
-
st.last_name_by_setter_id[sid] = name
|
| 60 |
-
|
| 61 |
-
valid_names.add(name)
|
| 62 |
-
if name not in st.allowed_ids_by_name:
|
| 63 |
-
st.allowed_ids_by_name[name] = sid
|
| 64 |
-
if name not in st.values_by_name:
|
| 65 |
-
st.values_by_name[name] = None
|
| 66 |
-
bisect.insort(st.names_sorted, name)
|
| 67 |
-
|
| 68 |
-
for k in list(st.values_by_name.keys()):
|
| 69 |
-
if k not in valid_names:
|
| 70 |
-
del st.values_by_name[k]
|
| 71 |
-
|
| 72 |
-
for sid, nm in list(st.last_name_by_setter_id.items()):
|
| 73 |
-
if nm not in valid_names:
|
| 74 |
-
del st.last_name_by_setter_id[sid]
|
| 75 |
-
except Exception:
|
| 76 |
-
st.allowed_ids_by_name.clear()
|
| 77 |
-
st.names_sorted.clear()
|
| 78 |
-
|
| 79 |
-
class Setter:
|
| 80 |
-
|
| 81 |
-
@classmethod
|
| 82 |
-
def INPUT_TYPES(cls):
|
| 83 |
-
return {
|
| 84 |
-
"required": {
|
| 85 |
-
"obj": (ANY_TYPE,),
|
| 86 |
-
"var_name": ("STRING", {"default": ""}),
|
| 87 |
-
},
|
| 88 |
-
"hidden": {
|
| 89 |
-
"prompt": "PROMPT",
|
| 90 |
-
"unique_id": "UNIQUE_ID",
|
| 91 |
-
}
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
RETURN_TYPES = (ANY_TYPE,)
|
| 95 |
-
RETURN_NAMES = ("OUT",)
|
| 96 |
-
FUNCTION = "set"
|
| 97 |
-
OUTPUT_NODE = True
|
| 98 |
-
CATEGORY = "AK/pipe"
|
| 99 |
-
|
| 100 |
-
def set(self, obj, var_name, prompt=None, unique_id=None):
|
| 101 |
-
st = _get_store()
|
| 102 |
-
|
| 103 |
-
pid = None
|
| 104 |
-
try:
|
| 105 |
-
pid = id(prompt)
|
| 106 |
-
except Exception:
|
| 107 |
-
pid = None
|
| 108 |
-
|
| 109 |
-
if pid is not None and pid != st.last_prompt_obj_id:
|
| 110 |
-
st.last_prompt_obj_id = pid
|
| 111 |
-
_rebuild_from_prompt(st, prompt)
|
| 112 |
-
|
| 113 |
-
if var_name is None:
|
| 114 |
-
var_name = ""
|
| 115 |
-
if not isinstance(var_name, str):
|
| 116 |
-
var_name = str(var_name)
|
| 117 |
-
name = var_name.strip()
|
| 118 |
-
if not name:
|
| 119 |
-
raise Exception(f"[Setter {unique_id}] var_name is empty")
|
| 120 |
-
|
| 121 |
-
if name not in st.allowed_ids_by_name:
|
| 122 |
-
st.allowed_ids_by_name[name] = str(unique_id)
|
| 123 |
-
st.values_by_name[name] = None
|
| 124 |
-
bisect.insort(st.names_sorted, name)
|
| 125 |
-
|
| 126 |
-
# value = obj[0] if isinstance(obj, (list, tuple)) else obj
|
| 127 |
-
value = obj
|
| 128 |
-
st.values_by_name[name] = value
|
| 129 |
-
return (value,)
|
| 130 |
-
|
| 131 |
-
NODE_CLASS_MAPPINGS = {
|
| 132 |
-
"Setter": Setter,
|
| 133 |
-
}
|
| 134 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 135 |
-
"Setter": "Setter",
|
| 136 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-AK-Pack/pyproject.toml
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
[project]
|
| 2 |
-
name = "comfyui-ak-pack"
|
| 3 |
-
publisher = "akawana2"
|
| 4 |
-
version = "3.01"
|
| 5 |
-
frontend_version = "3.01"
|
| 6 |
-
description = "Utility tools. Index Multiple, CLIP Multiple, Pipe, Getter, Setter."
|
| 7 |
-
readme = "README.md"
|
| 8 |
-
requires-python = ">=3.10"
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
packages = ["nodes"]
|
| 12 |
-
|
| 13 |
-
[project.urls]
|
| 14 |
-
Repository = "https://github.com/akawana/ComfyUI-AK-Pack"
|
| 15 |
-
Documentation = "https://github.com/akawana/ComfyUI-AK-Pack/wiki"
|
| 16 |
-
"Bug Tracker" = "https://github.com/akawana/ComfyUI-AK-Pack/issues"
|
| 17 |
-
|
| 18 |
-
[tool.comfy_node]
|
| 19 |
-
package = "nodes"
|
| 20 |
-
publisher = "akawana2"
|
| 21 |
-
display_name = "AK Pack"
|
| 22 |
-
summary = "Index Multiple, CLIP Multiple, Pipe, Getter, Setter."
|
| 23 |
-
tags = ["utility", "list", "group", "clip"]
|
| 24 |
-
|
| 25 |
-
[tool.comfy]
|
| 26 |
-
PublisherId = "akawana2" # TODO (fill in Publisher ID from Comfy Registry Website).
|
| 27 |
-
DisplayName = "AK Pack" # Display name for the Custom Node. Can be changed later.
|
| 28 |
-
Icon = "https://raw.githubusercontent.com/akawana/ComfyUI-Utils-extra/refs/heads/main/icon.jpg"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/classes/AD_AnyFileList.py
DELETED
|
@@ -1,88 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import torch
|
| 3 |
-
import numpy as np
|
| 4 |
-
from PIL import Image
|
| 5 |
-
import csv
|
| 6 |
-
|
| 7 |
-
class AD_AnyFileList:
|
| 8 |
-
@classmethod
|
| 9 |
-
def INPUT_TYPES(s):
|
| 10 |
-
return {
|
| 11 |
-
"required": {
|
| 12 |
-
"Directory": ("STRING", {"default": ""}),
|
| 13 |
-
"Load_Cap": ("INT", {"default": 100, "min": 1, "max": 1000}),
|
| 14 |
-
"Skip_Frame": ("INT", {"default": 0, "min": 0, "max": 100}),
|
| 15 |
-
"Filter_By": (["*", "images", "text"],),
|
| 16 |
-
"Extension": ("STRING", {"default": "*"}),
|
| 17 |
-
"Deep_Search": ("BOOLEAN", {"default": False}),
|
| 18 |
-
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff})
|
| 19 |
-
}
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
RETURN_TYPES = ("STRING", "IMAGE", "STRING", "STRING", "STRING", "STRING", "INT", "STRING")
|
| 23 |
-
RETURN_NAMES = ("Directory_Output", "Image_List", "Text_List", "File_Path_List", "File_Name_List", "File_Name_With_Extension_List", "Total_Files", "Merged_Text")
|
| 24 |
-
FUNCTION = "process_files"
|
| 25 |
-
OUTPUT_NODE = True
|
| 26 |
-
OUTPUT_IS_LIST = (False, True, True, True, True, True, False, False)
|
| 27 |
-
CATEGORY = "🌻 Addoor/Batch"
|
| 28 |
-
|
| 29 |
-
def process_files(self, Directory, Load_Cap, Skip_Frame, Filter_By, Extension, Deep_Search, seed):
|
| 30 |
-
file_extensions = {
|
| 31 |
-
"images": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"],
|
| 32 |
-
"text": [".txt", ".csv", ".md", ".json"],
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
file_paths = []
|
| 36 |
-
if Deep_Search:
|
| 37 |
-
for root, _, files in os.walk(Directory):
|
| 38 |
-
for file in files:
|
| 39 |
-
file_path = os.path.join(root, file)
|
| 40 |
-
file_extension = os.path.splitext(file)[1].lower()
|
| 41 |
-
if (Filter_By == "*" or file_extension in file_extensions.get(Filter_By, [])) and \
|
| 42 |
-
(Extension == "*" or file.lower().endswith(Extension.lower())):
|
| 43 |
-
file_paths.append(file_path)
|
| 44 |
-
else:
|
| 45 |
-
for file in os.listdir(Directory):
|
| 46 |
-
file_path = os.path.join(Directory, file)
|
| 47 |
-
if os.path.isfile(file_path):
|
| 48 |
-
file_extension = os.path.splitext(file)[1].lower()
|
| 49 |
-
if (Filter_By == "*" or file_extension in file_extensions.get(Filter_By, [])) and \
|
| 50 |
-
(Extension == "*" or file.lower().endswith(Extension.lower())):
|
| 51 |
-
file_paths.append(file_path)
|
| 52 |
-
|
| 53 |
-
file_paths = sorted(file_paths)[Skip_Frame:Skip_Frame + Load_Cap]
|
| 54 |
-
|
| 55 |
-
image_list = []
|
| 56 |
-
text_list = []
|
| 57 |
-
file_name_list = []
|
| 58 |
-
file_name_with_extension_list = []
|
| 59 |
-
merged_text = ""
|
| 60 |
-
|
| 61 |
-
for file_path in file_paths:
|
| 62 |
-
file_extension = os.path.splitext(file_path)[1].lower()
|
| 63 |
-
if file_extension in file_extensions["images"]:
|
| 64 |
-
try:
|
| 65 |
-
img = Image.open(file_path).convert("RGB")
|
| 66 |
-
image = torch.from_numpy(np.array(img).astype(np.float32) / 255.0).unsqueeze(0)
|
| 67 |
-
image_list.append(image)
|
| 68 |
-
except Exception as e:
|
| 69 |
-
print(f"Error loading image '{file_path}': {e}")
|
| 70 |
-
elif file_extension in file_extensions["text"]:
|
| 71 |
-
try:
|
| 72 |
-
if file_extension == '.csv':
|
| 73 |
-
with open(file_path, 'r', newline='', encoding='utf-8') as csvfile:
|
| 74 |
-
content = '\n'.join([','.join(row) for row in csv.reader(csvfile)])
|
| 75 |
-
else:
|
| 76 |
-
with open(file_path, 'r', encoding='utf-8') as txtfile:
|
| 77 |
-
content = txtfile.read()
|
| 78 |
-
text_list.append(content)
|
| 79 |
-
merged_text += content + "\n\n"
|
| 80 |
-
except Exception as e:
|
| 81 |
-
print(f"Error reading file '{file_path}': {e}")
|
| 82 |
-
|
| 83 |
-
file_name_list.append(os.path.splitext(os.path.basename(file_path))[0])
|
| 84 |
-
file_name_with_extension_list.append(os.path.basename(file_path))
|
| 85 |
-
|
| 86 |
-
total_files = len(file_paths)
|
| 87 |
-
return (Directory, image_list, text_list, file_paths, file_name_list, file_name_with_extension_list, total_files, merged_text.strip())
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/classes/AD_BatchImageLoadFromDir.py
DELETED
|
@@ -1,52 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import torch
|
| 3 |
-
import numpy as np
|
| 4 |
-
from PIL import Image
|
| 5 |
-
|
| 6 |
-
class AD_BatchImageLoadFromDir:
|
| 7 |
-
@classmethod
|
| 8 |
-
def INPUT_TYPES(s):
|
| 9 |
-
return {
|
| 10 |
-
"required": {
|
| 11 |
-
"Directory": ("STRING", {"default": ""}),
|
| 12 |
-
"Load_Cap": ("INT", {"default": 100, "min": 1, "max": 1000}),
|
| 13 |
-
"Skip_Frame": ("INT", {"default": 0, "min": 0, "max": 100}),
|
| 14 |
-
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff})
|
| 15 |
-
}
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
RETURN_TYPES = ("IMAGE", "STRING", "STRING", "STRING", "INT")
|
| 19 |
-
RETURN_NAMES = ("Images", "Image_Paths", "Image_Names_suffix", "Image_Names", "Count")
|
| 20 |
-
FUNCTION = "load_images"
|
| 21 |
-
OUTPUT_NODE = True
|
| 22 |
-
OUTPUT_IS_LIST = (True, True, True, True, False)
|
| 23 |
-
CATEGORY = "🌻 Addoor/Batch"
|
| 24 |
-
|
| 25 |
-
def load_images(self, Directory, Load_Cap, Skip_Frame, seed):
|
| 26 |
-
image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif', '.webp']
|
| 27 |
-
file_paths = []
|
| 28 |
-
for root, dirs, files in os.walk(Directory):
|
| 29 |
-
for file in files:
|
| 30 |
-
if any(file.lower().endswith(ext) for ext in image_extensions):
|
| 31 |
-
file_paths.append(os.path.join(root, file))
|
| 32 |
-
file_paths = sorted(file_paths)[Skip_Frame:Skip_Frame + Load_Cap]
|
| 33 |
-
|
| 34 |
-
images = []
|
| 35 |
-
image_paths = []
|
| 36 |
-
image_names_suffix = []
|
| 37 |
-
image_names = []
|
| 38 |
-
|
| 39 |
-
for file_path in file_paths:
|
| 40 |
-
try:
|
| 41 |
-
img = Image.open(file_path).convert("RGB")
|
| 42 |
-
image = torch.from_numpy(np.array(img).astype(np.float32) / 255.0).unsqueeze(0)
|
| 43 |
-
images.append(image)
|
| 44 |
-
image_paths.append(file_path)
|
| 45 |
-
image_names_suffix.append(os.path.basename(file_path))
|
| 46 |
-
image_names.append(os.path.splitext(os.path.basename(file_path))[0])
|
| 47 |
-
except Exception as e:
|
| 48 |
-
print(f"Error loading image '{file_path}': {e}")
|
| 49 |
-
|
| 50 |
-
count = len(images)
|
| 51 |
-
return (images, image_paths, image_names_suffix, image_names, count)
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/classes/AD_LoadImageAdvanced.py
DELETED
|
@@ -1,87 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import hashlib
|
| 3 |
-
from PIL import Image, ImageSequence, ImageOps
|
| 4 |
-
import numpy as np
|
| 5 |
-
import torch
|
| 6 |
-
import folder_paths
|
| 7 |
-
|
| 8 |
-
class AD_LoadImageAdvanced:
|
| 9 |
-
@classmethod
|
| 10 |
-
def INPUT_TYPES(s):
|
| 11 |
-
input_dir = folder_paths.get_input_directory()
|
| 12 |
-
|
| 13 |
-
files = []
|
| 14 |
-
for root, dirs, filenames in os.walk(input_dir):
|
| 15 |
-
for filename in filenames:
|
| 16 |
-
full_path = os.path.join(root, filename)
|
| 17 |
-
relative_path = os.path.relpath(full_path, input_dir)
|
| 18 |
-
relative_path = relative_path.replace("\\", "/")
|
| 19 |
-
files.append(relative_path)
|
| 20 |
-
|
| 21 |
-
return {"required":
|
| 22 |
-
{"image": (sorted(files), {"image_upload": True}),
|
| 23 |
-
"strip_extension": ("BOOLEAN", {"default": True}),
|
| 24 |
-
"rgba": ("BOOLEAN", {"default": False})}
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
CATEGORY = "🌻 Addoor/Image"
|
| 28 |
-
|
| 29 |
-
RETURN_TYPES = ("IMAGE", "MASK", "STRING")
|
| 30 |
-
RETURN_NAMES = ("IMAGE", "MASK", "FILENAME")
|
| 31 |
-
|
| 32 |
-
FUNCTION = "load_image"
|
| 33 |
-
|
| 34 |
-
def load_image(self, image, strip_extension, rgba):
|
| 35 |
-
image_path = folder_paths.get_annotated_filepath(image)
|
| 36 |
-
img = Image.open(image_path)
|
| 37 |
-
output_images = []
|
| 38 |
-
output_masks = []
|
| 39 |
-
for i in ImageSequence.Iterator(img):
|
| 40 |
-
i = ImageOps.exif_transpose(i)
|
| 41 |
-
if i.mode == 'I':
|
| 42 |
-
i = i.point(lambda i: i * (1 / 255))
|
| 43 |
-
if rgba:
|
| 44 |
-
image = i.convert("RGBA")
|
| 45 |
-
image = np.array(image).astype(np.float32) / 255.0
|
| 46 |
-
image = torch.from_numpy(image)[None,]
|
| 47 |
-
else:
|
| 48 |
-
image = i.convert("RGB")
|
| 49 |
-
image = np.array(image).astype(np.float32) / 255.0
|
| 50 |
-
image = torch.from_numpy(image)[None,]
|
| 51 |
-
if 'A' in i.getbands():
|
| 52 |
-
mask = np.array(i.getchannel('A')).astype(np.float32) / 255.0
|
| 53 |
-
mask = 1. - torch.from_numpy(mask)
|
| 54 |
-
else:
|
| 55 |
-
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
|
| 56 |
-
output_images.append(image)
|
| 57 |
-
output_masks.append(mask.unsqueeze(0))
|
| 58 |
-
|
| 59 |
-
if len(output_images) > 1:
|
| 60 |
-
output_image = torch.cat(output_images, dim=0)
|
| 61 |
-
output_mask = torch.cat(output_masks, dim=0)
|
| 62 |
-
else:
|
| 63 |
-
output_image = output_images[0]
|
| 64 |
-
output_mask = output_masks[0]
|
| 65 |
-
|
| 66 |
-
if strip_extension:
|
| 67 |
-
filename = os.path.splitext(image_path)[0]
|
| 68 |
-
else:
|
| 69 |
-
filename = os.path.basename(image_path)
|
| 70 |
-
|
| 71 |
-
return (output_image, output_mask, filename,)
|
| 72 |
-
|
| 73 |
-
@classmethod
|
| 74 |
-
def IS_CHANGED(s, image, strip_extension, rgba):
|
| 75 |
-
image_path = folder_paths.get_annotated_filepath(image)
|
| 76 |
-
m = hashlib.sha256()
|
| 77 |
-
with open(image_path, 'rb') as f:
|
| 78 |
-
m.update(f.read())
|
| 79 |
-
return m.digest().hex()
|
| 80 |
-
|
| 81 |
-
@classmethod
|
| 82 |
-
def VALIDATE_INPUTS(s, image, strip_extension, rgba):
|
| 83 |
-
if not folder_paths.exists_annotated_filepath(image):
|
| 84 |
-
return "Invalid image file: {}".format(image)
|
| 85 |
-
|
| 86 |
-
return True
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/classes/AD_PromptReplace.py
DELETED
|
@@ -1,59 +0,0 @@
|
|
| 1 |
-
import random
|
| 2 |
-
|
| 3 |
-
class AD_PromptReplace:
|
| 4 |
-
def __init__(self):
|
| 5 |
-
self.current_index = None
|
| 6 |
-
|
| 7 |
-
@classmethod
|
| 8 |
-
def INPUT_TYPES(cls):
|
| 9 |
-
return {
|
| 10 |
-
"required": {
|
| 11 |
-
"Content": ("STRING", {"default": "", "multiline": True}),
|
| 12 |
-
"Match": ("STRING", {"default": ""}),
|
| 13 |
-
"Replace": ("STRING", {"default": "", "multiline": True}),
|
| 14 |
-
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
| 15 |
-
"Increment": ("INT", {"default": 1, "min": 0, "max": 1000}),
|
| 16 |
-
}
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
RETURN_TYPES = ("STRING", "INT")
|
| 20 |
-
RETURN_NAMES = ("Replaced_Text", "new_seed")
|
| 21 |
-
FUNCTION = "replace_prompt"
|
| 22 |
-
CATEGORY = "🌻 Addoor/Utilities"
|
| 23 |
-
|
| 24 |
-
def replace_prompt(self, Content: str, Match: str, Replace: str, seed: int, Increment: int):
|
| 25 |
-
content_lines = Content.split('\n')
|
| 26 |
-
replace_lines = Replace.split('\n') if Replace else []
|
| 27 |
-
|
| 28 |
-
if not content_lines:
|
| 29 |
-
content_lines = [""]
|
| 30 |
-
if not replace_lines:
|
| 31 |
-
replace_lines = [""]
|
| 32 |
-
|
| 33 |
-
# Initialize current_index if it's None
|
| 34 |
-
if self.current_index is None:
|
| 35 |
-
if Increment == 0:
|
| 36 |
-
# Random starting point
|
| 37 |
-
random.seed(seed)
|
| 38 |
-
self.current_index = random.randint(0, len(replace_lines) - 1)
|
| 39 |
-
else:
|
| 40 |
-
# Start from the beginning
|
| 41 |
-
self.current_index = 0
|
| 42 |
-
|
| 43 |
-
# Select a line based on increment
|
| 44 |
-
if Increment == 0:
|
| 45 |
-
# Random selection using seed
|
| 46 |
-
random.seed(seed)
|
| 47 |
-
selected_index = random.randint(0, len(replace_lines) - 1)
|
| 48 |
-
else:
|
| 49 |
-
# Sequential selection based on increment
|
| 50 |
-
selected_index = self.current_index
|
| 51 |
-
self.current_index = (self.current_index + Increment) % len(replace_lines)
|
| 52 |
-
|
| 53 |
-
selected_replace = replace_lines[selected_index]
|
| 54 |
-
|
| 55 |
-
# Perform text replacement
|
| 56 |
-
replaced_content = Content.replace(Match, selected_replace) if Match else Content
|
| 57 |
-
|
| 58 |
-
return (replaced_content, seed)
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/example_workflow/RunningHub API.json
DELETED
|
@@ -1,500 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"last_node_id": 11,
|
| 3 |
-
"last_link_id": 8,
|
| 4 |
-
"nodes": [
|
| 5 |
-
{
|
| 6 |
-
"id": 3,
|
| 7 |
-
"type": "RunningHubWorkflowExecutor",
|
| 8 |
-
"pos": [
|
| 9 |
-
1814.84912109375,
|
| 10 |
-
670.4561157226562
|
| 11 |
-
],
|
| 12 |
-
"size": [
|
| 13 |
-
352.79998779296875,
|
| 14 |
-
318
|
| 15 |
-
],
|
| 16 |
-
"flags": {},
|
| 17 |
-
"order": 6,
|
| 18 |
-
"mode": 0,
|
| 19 |
-
"inputs": [
|
| 20 |
-
{
|
| 21 |
-
"name": "node_info_list",
|
| 22 |
-
"type": "NODEINFOLIST",
|
| 23 |
-
"link": 2,
|
| 24 |
-
"label": "node_info_list"
|
| 25 |
-
},
|
| 26 |
-
{
|
| 27 |
-
"name": "api_key",
|
| 28 |
-
"type": "STRING",
|
| 29 |
-
"link": 1,
|
| 30 |
-
"widget": {
|
| 31 |
-
"name": "api_key"
|
| 32 |
-
},
|
| 33 |
-
"label": "api_key"
|
| 34 |
-
},
|
| 35 |
-
{
|
| 36 |
-
"name": "workflow_id",
|
| 37 |
-
"type": "STRING",
|
| 38 |
-
"link": 4,
|
| 39 |
-
"widget": {
|
| 40 |
-
"name": "workflow_id"
|
| 41 |
-
},
|
| 42 |
-
"label": "workflow_id"
|
| 43 |
-
}
|
| 44 |
-
],
|
| 45 |
-
"outputs": [
|
| 46 |
-
{
|
| 47 |
-
"name": "file_urls",
|
| 48 |
-
"type": "FILEURL_LIST",
|
| 49 |
-
"links": [
|
| 50 |
-
5
|
| 51 |
-
],
|
| 52 |
-
"label": "file_urls",
|
| 53 |
-
"slot_index": 0
|
| 54 |
-
},
|
| 55 |
-
{
|
| 56 |
-
"name": "task_id",
|
| 57 |
-
"type": "STRING",
|
| 58 |
-
"links": null,
|
| 59 |
-
"label": "task_id"
|
| 60 |
-
},
|
| 61 |
-
{
|
| 62 |
-
"name": "msg",
|
| 63 |
-
"type": "STRING",
|
| 64 |
-
"links": null,
|
| 65 |
-
"label": "msg"
|
| 66 |
-
},
|
| 67 |
-
{
|
| 68 |
-
"name": "promptTips",
|
| 69 |
-
"type": "STRING",
|
| 70 |
-
"links": null,
|
| 71 |
-
"label": "promptTips"
|
| 72 |
-
},
|
| 73 |
-
{
|
| 74 |
-
"name": "taskStatus",
|
| 75 |
-
"type": "STRING",
|
| 76 |
-
"links": null,
|
| 77 |
-
"label": "taskStatus"
|
| 78 |
-
},
|
| 79 |
-
{
|
| 80 |
-
"name": "fileType",
|
| 81 |
-
"type": "STRING",
|
| 82 |
-
"links": null,
|
| 83 |
-
"label": "fileType"
|
| 84 |
-
},
|
| 85 |
-
{
|
| 86 |
-
"name": "code",
|
| 87 |
-
"type": "STRING",
|
| 88 |
-
"links": null,
|
| 89 |
-
"label": "code"
|
| 90 |
-
},
|
| 91 |
-
{
|
| 92 |
-
"name": "json",
|
| 93 |
-
"type": "STRING",
|
| 94 |
-
"links": null,
|
| 95 |
-
"label": "json"
|
| 96 |
-
}
|
| 97 |
-
],
|
| 98 |
-
"properties": {
|
| 99 |
-
"Node name for S&R": "RunningHubWorkflowExecutor"
|
| 100 |
-
},
|
| 101 |
-
"widgets_values": [
|
| 102 |
-
"",
|
| 103 |
-
"",
|
| 104 |
-
34390019738778,
|
| 105 |
-
"randomize",
|
| 106 |
-
60,
|
| 107 |
-
5
|
| 108 |
-
]
|
| 109 |
-
},
|
| 110 |
-
{
|
| 111 |
-
"id": 9,
|
| 112 |
-
"type": "RunningHubImageUploader",
|
| 113 |
-
"pos": [
|
| 114 |
-
621.948974609375,
|
| 115 |
-
753.6560668945312
|
| 116 |
-
],
|
| 117 |
-
"size": [
|
| 118 |
-
315,
|
| 119 |
-
58
|
| 120 |
-
],
|
| 121 |
-
"flags": {},
|
| 122 |
-
"order": 0,
|
| 123 |
-
"mode": 0,
|
| 124 |
-
"inputs": [
|
| 125 |
-
{
|
| 126 |
-
"name": "image",
|
| 127 |
-
"type": "IMAGE",
|
| 128 |
-
"link": null,
|
| 129 |
-
"label": "image"
|
| 130 |
-
}
|
| 131 |
-
],
|
| 132 |
-
"outputs": [
|
| 133 |
-
{
|
| 134 |
-
"name": "filename",
|
| 135 |
-
"type": "STRING",
|
| 136 |
-
"links": null,
|
| 137 |
-
"label": "filename"
|
| 138 |
-
}
|
| 139 |
-
],
|
| 140 |
-
"properties": {
|
| 141 |
-
"Node name for S&R": "RunningHubImageUploader"
|
| 142 |
-
},
|
| 143 |
-
"widgets_values": [
|
| 144 |
-
""
|
| 145 |
-
]
|
| 146 |
-
},
|
| 147 |
-
{
|
| 148 |
-
"id": 6,
|
| 149 |
-
"type": "RunningHubWorkflowID",
|
| 150 |
-
"pos": [
|
| 151 |
-
1438.5477294921875,
|
| 152 |
-
882.5560913085938
|
| 153 |
-
],
|
| 154 |
-
"size": [
|
| 155 |
-
315,
|
| 156 |
-
58
|
| 157 |
-
],
|
| 158 |
-
"flags": {},
|
| 159 |
-
"order": 1,
|
| 160 |
-
"mode": 0,
|
| 161 |
-
"inputs": [],
|
| 162 |
-
"outputs": [
|
| 163 |
-
{
|
| 164 |
-
"name": "STRING",
|
| 165 |
-
"type": "STRING",
|
| 166 |
-
"links": [
|
| 167 |
-
4
|
| 168 |
-
],
|
| 169 |
-
"slot_index": 0,
|
| 170 |
-
"label": "STRING"
|
| 171 |
-
}
|
| 172 |
-
],
|
| 173 |
-
"properties": {
|
| 174 |
-
"Node name for S&R": "RunningHubWorkflowID"
|
| 175 |
-
},
|
| 176 |
-
"widgets_values": [
|
| 177 |
-
"1867408429454426113"
|
| 178 |
-
]
|
| 179 |
-
},
|
| 180 |
-
{
|
| 181 |
-
"id": 4,
|
| 182 |
-
"type": "RunningHubAccountStatus",
|
| 183 |
-
"pos": [
|
| 184 |
-
1819.9490966796875,
|
| 185 |
-
1044.7562255859375
|
| 186 |
-
],
|
| 187 |
-
"size": [
|
| 188 |
-
341,
|
| 189 |
-
54
|
| 190 |
-
],
|
| 191 |
-
"flags": {},
|
| 192 |
-
"order": 5,
|
| 193 |
-
"mode": 0,
|
| 194 |
-
"inputs": [
|
| 195 |
-
{
|
| 196 |
-
"name": "api_key",
|
| 197 |
-
"type": "STRING",
|
| 198 |
-
"link": 3,
|
| 199 |
-
"widget": {
|
| 200 |
-
"name": "api_key"
|
| 201 |
-
},
|
| 202 |
-
"label": "api_key"
|
| 203 |
-
}
|
| 204 |
-
],
|
| 205 |
-
"outputs": [
|
| 206 |
-
{
|
| 207 |
-
"name": "remain_coins",
|
| 208 |
-
"type": "INT",
|
| 209 |
-
"links": null,
|
| 210 |
-
"label": "remain_coins",
|
| 211 |
-
"slot_index": 0
|
| 212 |
-
},
|
| 213 |
-
{
|
| 214 |
-
"name": "current_task_counts",
|
| 215 |
-
"type": "INT",
|
| 216 |
-
"links": null,
|
| 217 |
-
"label": "current_task_counts"
|
| 218 |
-
}
|
| 219 |
-
],
|
| 220 |
-
"properties": {
|
| 221 |
-
"Node name for S&R": "RunningHubAccountStatus"
|
| 222 |
-
},
|
| 223 |
-
"widgets_values": [
|
| 224 |
-
""
|
| 225 |
-
]
|
| 226 |
-
},
|
| 227 |
-
{
|
| 228 |
-
"id": 8,
|
| 229 |
-
"type": "RunningHubNodeInfoReplace",
|
| 230 |
-
"pos": [
|
| 231 |
-
1024.0535888671875,
|
| 232 |
-
956.73486328125
|
| 233 |
-
],
|
| 234 |
-
"size": [
|
| 235 |
-
400,
|
| 236 |
-
338
|
| 237 |
-
],
|
| 238 |
-
"flags": {},
|
| 239 |
-
"order": 2,
|
| 240 |
-
"mode": 0,
|
| 241 |
-
"inputs": [],
|
| 242 |
-
"outputs": [
|
| 243 |
-
{
|
| 244 |
-
"name": "node_info",
|
| 245 |
-
"type": "NODEINFO",
|
| 246 |
-
"links": null,
|
| 247 |
-
"label": "node_info"
|
| 248 |
-
},
|
| 249 |
-
{
|
| 250 |
-
"name": "new_seed",
|
| 251 |
-
"type": "INT",
|
| 252 |
-
"links": null,
|
| 253 |
-
"label": "new_seed"
|
| 254 |
-
}
|
| 255 |
-
],
|
| 256 |
-
"properties": {
|
| 257 |
-
"Node name for S&R": "RunningHubNodeInfoReplace"
|
| 258 |
-
},
|
| 259 |
-
"widgets_values": [
|
| 260 |
-
"",
|
| 261 |
-
"",
|
| 262 |
-
"",
|
| 263 |
-
"",
|
| 264 |
-
"",
|
| 265 |
-
"",
|
| 266 |
-
842958099223905,
|
| 267 |
-
"randomize",
|
| 268 |
-
1,
|
| 269 |
-
"normal"
|
| 270 |
-
]
|
| 271 |
-
},
|
| 272 |
-
{
|
| 273 |
-
"id": 11,
|
| 274 |
-
"type": "PreviewImage",
|
| 275 |
-
"pos": [
|
| 276 |
-
2522.2890625,
|
| 277 |
-
673.9710693359375
|
| 278 |
-
],
|
| 279 |
-
"size": [
|
| 280 |
-
355.91650390625,
|
| 281 |
-
358.8138427734375
|
| 282 |
-
],
|
| 283 |
-
"flags": {},
|
| 284 |
-
"order": 8,
|
| 285 |
-
"mode": 0,
|
| 286 |
-
"inputs": [
|
| 287 |
-
{
|
| 288 |
-
"name": "images",
|
| 289 |
-
"type": "IMAGE",
|
| 290 |
-
"link": 8,
|
| 291 |
-
"label": "图像"
|
| 292 |
-
}
|
| 293 |
-
],
|
| 294 |
-
"outputs": [],
|
| 295 |
-
"properties": {
|
| 296 |
-
"Node name for S&R": "PreviewImage"
|
| 297 |
-
}
|
| 298 |
-
},
|
| 299 |
-
{
|
| 300 |
-
"id": 5,
|
| 301 |
-
"type": "RunningHubFileSaver",
|
| 302 |
-
"pos": [
|
| 303 |
-
2187.75634765625,
|
| 304 |
-
669.653564453125
|
| 305 |
-
],
|
| 306 |
-
"size": [
|
| 307 |
-
315,
|
| 308 |
-
218
|
| 309 |
-
],
|
| 310 |
-
"flags": {},
|
| 311 |
-
"order": 7,
|
| 312 |
-
"mode": 0,
|
| 313 |
-
"inputs": [
|
| 314 |
-
{
|
| 315 |
-
"name": "file_urls",
|
| 316 |
-
"type": "FILEURL_LIST",
|
| 317 |
-
"link": 5,
|
| 318 |
-
"label": "file_urls"
|
| 319 |
-
}
|
| 320 |
-
],
|
| 321 |
-
"outputs": [
|
| 322 |
-
{
|
| 323 |
-
"name": "output_paths",
|
| 324 |
-
"type": "STRING",
|
| 325 |
-
"links": [],
|
| 326 |
-
"label": "output_paths",
|
| 327 |
-
"slot_index": 0
|
| 328 |
-
},
|
| 329 |
-
{
|
| 330 |
-
"name": "previews",
|
| 331 |
-
"type": "IMAGE",
|
| 332 |
-
"links": [
|
| 333 |
-
8
|
| 334 |
-
],
|
| 335 |
-
"label": "previews",
|
| 336 |
-
"slot_index": 1
|
| 337 |
-
},
|
| 338 |
-
{
|
| 339 |
-
"name": "file_types",
|
| 340 |
-
"type": "STRING",
|
| 341 |
-
"links": null,
|
| 342 |
-
"label": "file_types"
|
| 343 |
-
}
|
| 344 |
-
],
|
| 345 |
-
"properties": {
|
| 346 |
-
"Node name for S&R": "RunningHubFileSaver"
|
| 347 |
-
},
|
| 348 |
-
"widgets_values": [
|
| 349 |
-
"outputs",
|
| 350 |
-
"RH_Output",
|
| 351 |
-
"_",
|
| 352 |
-
4,
|
| 353 |
-
"auto",
|
| 354 |
-
false
|
| 355 |
-
]
|
| 356 |
-
},
|
| 357 |
-
{
|
| 358 |
-
"id": 1,
|
| 359 |
-
"type": "RunningHubInit",
|
| 360 |
-
"pos": [
|
| 361 |
-
1439.84912109375,
|
| 362 |
-
768.4561157226562
|
| 363 |
-
],
|
| 364 |
-
"size": [
|
| 365 |
-
315,
|
| 366 |
-
58
|
| 367 |
-
],
|
| 368 |
-
"flags": {
|
| 369 |
-
"collapsed": false
|
| 370 |
-
},
|
| 371 |
-
"order": 3,
|
| 372 |
-
"mode": 0,
|
| 373 |
-
"inputs": [],
|
| 374 |
-
"outputs": [
|
| 375 |
-
{
|
| 376 |
-
"name": "STRING",
|
| 377 |
-
"type": "STRING",
|
| 378 |
-
"links": [
|
| 379 |
-
1,
|
| 380 |
-
3
|
| 381 |
-
],
|
| 382 |
-
"slot_index": 0,
|
| 383 |
-
"label": "STRING"
|
| 384 |
-
}
|
| 385 |
-
],
|
| 386 |
-
"properties": {
|
| 387 |
-
"Node name for S&R": "RunningHubInit"
|
| 388 |
-
},
|
| 389 |
-
"widgets_values": [
|
| 390 |
-
""
|
| 391 |
-
]
|
| 392 |
-
},
|
| 393 |
-
{
|
| 394 |
-
"id": 2,
|
| 395 |
-
"type": "RunningHubNodeInfo",
|
| 396 |
-
"pos": [
|
| 397 |
-
1021.8431396484375,
|
| 398 |
-
672.1781005859375
|
| 399 |
-
],
|
| 400 |
-
"size": [
|
| 401 |
-
400,
|
| 402 |
-
200
|
| 403 |
-
],
|
| 404 |
-
"flags": {},
|
| 405 |
-
"order": 4,
|
| 406 |
-
"mode": 0,
|
| 407 |
-
"inputs": [
|
| 408 |
-
{
|
| 409 |
-
"name": "previous_node_info",
|
| 410 |
-
"type": "NODEINFOLIST",
|
| 411 |
-
"link": null,
|
| 412 |
-
"shape": 7,
|
| 413 |
-
"label": "previous_node_info"
|
| 414 |
-
}
|
| 415 |
-
],
|
| 416 |
-
"outputs": [
|
| 417 |
-
{
|
| 418 |
-
"name": "NODEINFOLIST",
|
| 419 |
-
"type": "NODEINFOLIST",
|
| 420 |
-
"links": [
|
| 421 |
-
2
|
| 422 |
-
],
|
| 423 |
-
"slot_index": 0,
|
| 424 |
-
"label": "NODEINFOLIST"
|
| 425 |
-
}
|
| 426 |
-
],
|
| 427 |
-
"properties": {
|
| 428 |
-
"Node name for S&R": "RunningHubNodeInfo"
|
| 429 |
-
},
|
| 430 |
-
"widgets_values": [
|
| 431 |
-
"6",
|
| 432 |
-
"text",
|
| 433 |
-
"3D Snake,Wearing light gray clothes,with a gold necklace,glasses,smiling face,light background,high-definitiona,gold necklace sign on its neck that reads \"2025.\" The picture is in the middle. Fireworks in the sky,red background,3D,C4D,",
|
| 434 |
-
""
|
| 435 |
-
]
|
| 436 |
-
}
|
| 437 |
-
],
|
| 438 |
-
"links": [
|
| 439 |
-
[
|
| 440 |
-
1,
|
| 441 |
-
1,
|
| 442 |
-
0,
|
| 443 |
-
3,
|
| 444 |
-
1,
|
| 445 |
-
"STRING"
|
| 446 |
-
],
|
| 447 |
-
[
|
| 448 |
-
2,
|
| 449 |
-
2,
|
| 450 |
-
0,
|
| 451 |
-
3,
|
| 452 |
-
0,
|
| 453 |
-
"NODEINFOLIST"
|
| 454 |
-
],
|
| 455 |
-
[
|
| 456 |
-
3,
|
| 457 |
-
1,
|
| 458 |
-
0,
|
| 459 |
-
4,
|
| 460 |
-
0,
|
| 461 |
-
"STRING"
|
| 462 |
-
],
|
| 463 |
-
[
|
| 464 |
-
4,
|
| 465 |
-
6,
|
| 466 |
-
0,
|
| 467 |
-
3,
|
| 468 |
-
2,
|
| 469 |
-
"STRING"
|
| 470 |
-
],
|
| 471 |
-
[
|
| 472 |
-
5,
|
| 473 |
-
3,
|
| 474 |
-
0,
|
| 475 |
-
5,
|
| 476 |
-
0,
|
| 477 |
-
"FILEURL_LIST"
|
| 478 |
-
],
|
| 479 |
-
[
|
| 480 |
-
8,
|
| 481 |
-
5,
|
| 482 |
-
1,
|
| 483 |
-
11,
|
| 484 |
-
0,
|
| 485 |
-
"IMAGE"
|
| 486 |
-
]
|
| 487 |
-
],
|
| 488 |
-
"groups": [],
|
| 489 |
-
"config": {},
|
| 490 |
-
"extra": {
|
| 491 |
-
"ds": {
|
| 492 |
-
"scale": 0.8264462809917354,
|
| 493 |
-
"offset": [
|
| 494 |
-
-509.6418520759915,
|
| 495 |
-
-255.05204017404154
|
| 496 |
-
]
|
| 497 |
-
}
|
| 498 |
-
},
|
| 499 |
-
"version": 0.4
|
| 500 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/nodes/AddPaddingAdvanced.py
DELETED
|
@@ -1,435 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
@author: ComfyNodePRs
|
| 3 |
-
@title: ComfyUI Advanced Padding
|
| 4 |
-
@description: Advanced padding node with scaling capabilities
|
| 5 |
-
@version: 1.0.0
|
| 6 |
-
@project: https://github.com/ComfyNodePRs/advanced-padding
|
| 7 |
-
@author: https://github.com/ComfyNodePRs
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
import PIL.Image as Image
|
| 11 |
-
import PIL.ImageDraw as ImageDraw
|
| 12 |
-
import numpy as np
|
| 13 |
-
import torch
|
| 14 |
-
import torchvision.transforms as t
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
class AD_PaddingAdvanced:
|
| 18 |
-
def __init__(self):
|
| 19 |
-
pass
|
| 20 |
-
|
| 21 |
-
FUNCTION = "process_image"
|
| 22 |
-
CATEGORY = "🌻 Addoor/image"
|
| 23 |
-
|
| 24 |
-
@classmethod
|
| 25 |
-
def INPUT_TYPES(s):
|
| 26 |
-
return {
|
| 27 |
-
"required": {
|
| 28 |
-
"image": ("IMAGE",),
|
| 29 |
-
"scale_by": ("FLOAT", {
|
| 30 |
-
"default": 1.0,
|
| 31 |
-
"min": 0.1,
|
| 32 |
-
"max": 8.0,
|
| 33 |
-
"step": 0.05,
|
| 34 |
-
"display": "number"
|
| 35 |
-
}),
|
| 36 |
-
"upscale_method": (["nearest-exact", "bilinear", "bicubic", "lanczos"], {"default": "lanczos"}),
|
| 37 |
-
"left": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
|
| 38 |
-
"top": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
|
| 39 |
-
"right": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
|
| 40 |
-
"bottom": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
|
| 41 |
-
"color": ("STRING", {"default": "#ffffff"}),
|
| 42 |
-
"transparent": ("BOOLEAN", {"default": False}),
|
| 43 |
-
},
|
| 44 |
-
"optional": {
|
| 45 |
-
"background": ("IMAGE",),
|
| 46 |
-
}
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
RETURN_TYPES = ("IMAGE", "MASK")
|
| 50 |
-
RETURN_NAMES = ("image", "mask")
|
| 51 |
-
|
| 52 |
-
def add_padding(self, image, left, top, right, bottom, color="#ffffff", transparent=False):
|
| 53 |
-
padded_images = []
|
| 54 |
-
image = [self.tensor2pil(img) for img in image]
|
| 55 |
-
for img in image:
|
| 56 |
-
padded_image = Image.new("RGBA" if transparent else "RGB",
|
| 57 |
-
(img.width + left + right, img.height + top + bottom),
|
| 58 |
-
(0, 0, 0, 0) if transparent else self.hex_to_tuple(color))
|
| 59 |
-
padded_image.paste(img, (left, top))
|
| 60 |
-
padded_images.append(self.pil2tensor(padded_image))
|
| 61 |
-
return torch.cat(padded_images, dim=0)
|
| 62 |
-
|
| 63 |
-
def create_mask(self, image, left, top, right, bottom):
|
| 64 |
-
masks = []
|
| 65 |
-
image = [self.tensor2pil(img) for img in image]
|
| 66 |
-
for img in image:
|
| 67 |
-
shape = (left, top, img.width + left, img.height + top)
|
| 68 |
-
mask_image = Image.new("L", (img.width + left + right, img.height + top + bottom), 255)
|
| 69 |
-
draw = ImageDraw.Draw(mask_image)
|
| 70 |
-
draw.rectangle(shape, fill=0)
|
| 71 |
-
masks.append(self.pil2tensor(mask_image))
|
| 72 |
-
return torch.cat(masks, dim=0)
|
| 73 |
-
|
| 74 |
-
def scale_image(self, image, scale_by, method):
|
| 75 |
-
scaled_images = []
|
| 76 |
-
image = [self.tensor2pil(img) for img in image]
|
| 77 |
-
|
| 78 |
-
resampling_methods = {
|
| 79 |
-
"nearest-exact": Image.Resampling.NEAREST,
|
| 80 |
-
"bilinear": Image.Resampling.BILINEAR,
|
| 81 |
-
"bicubic": Image.Resampling.BICUBIC,
|
| 82 |
-
"lanczos": Image.Resampling.LANCZOS,
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
for img in image:
|
| 86 |
-
# 计算新尺寸
|
| 87 |
-
new_width = int(img.width * scale_by)
|
| 88 |
-
new_height = int(img.height * scale_by)
|
| 89 |
-
|
| 90 |
-
# 使用选定的方法进行缩放
|
| 91 |
-
scaled_img = img.resize(
|
| 92 |
-
(new_width, new_height),
|
| 93 |
-
resampling_methods.get(method, Image.Resampling.LANCZOS)
|
| 94 |
-
)
|
| 95 |
-
scaled_images.append(self.pil2tensor(scaled_img))
|
| 96 |
-
|
| 97 |
-
return torch.cat(scaled_images, dim=0)
|
| 98 |
-
|
| 99 |
-
def hex_to_tuple(self, color):
|
| 100 |
-
if not isinstance(color, str):
|
| 101 |
-
raise ValueError("Color must be a hex string")
|
| 102 |
-
color = color.strip("#")
|
| 103 |
-
return tuple([int(color[i:i + 2], 16) for i in range(0, len(color), 2)])
|
| 104 |
-
|
| 105 |
-
def tensor2pil(self, image):
|
| 106 |
-
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
| 107 |
-
|
| 108 |
-
def pil2tensor(self, image):
|
| 109 |
-
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
| 110 |
-
|
| 111 |
-
def composite_with_background(self, image, background):
|
| 112 |
-
"""将图片居中合成到背景上"""
|
| 113 |
-
image_pil = self.tensor2pil(image[0]) # 获取第一帧
|
| 114 |
-
bg_pil = self.tensor2pil(background[0])
|
| 115 |
-
|
| 116 |
-
# 创建新的景图像
|
| 117 |
-
result = bg_pil.copy()
|
| 118 |
-
|
| 119 |
-
# 计算居中位置
|
| 120 |
-
x = (bg_pil.width - image_pil.width) // 2
|
| 121 |
-
y = (bg_pil.height - image_pil.height) // 2
|
| 122 |
-
|
| 123 |
-
# 如果前景图比背景大,需要裁剪
|
| 124 |
-
if image_pil.width > bg_pil.width or image_pil.height > bg_pil.height:
|
| 125 |
-
# 计算裁剪区域
|
| 126 |
-
crop_left = max(0, (image_pil.width - bg_pil.width) // 2)
|
| 127 |
-
crop_top = max(0, (image_pil.height - bg_pil.height) // 2)
|
| 128 |
-
crop_right = min(image_pil.width, crop_left + bg_pil.width)
|
| 129 |
-
crop_bottom = min(image_pil.height, crop_top + bg_pil.height)
|
| 130 |
-
|
| 131 |
-
# 裁剪图片
|
| 132 |
-
image_pil = image_pil.crop((crop_left, crop_top, crop_right, crop_bottom))
|
| 133 |
-
|
| 134 |
-
# 更新粘贴位置
|
| 135 |
-
x = max(0, (bg_pil.width - image_pil.width) // 2)
|
| 136 |
-
y = max(0, (bg_pil.height - image_pil.height) // 2)
|
| 137 |
-
|
| 138 |
-
# 如果前景图有透明通道,使用alpha通道合成
|
| 139 |
-
if image_pil.mode == 'RGBA':
|
| 140 |
-
result.paste(image_pil, (x, y), image_pil)
|
| 141 |
-
else:
|
| 142 |
-
result.paste(image_pil, (x, y))
|
| 143 |
-
|
| 144 |
-
return self.pil2tensor(result)
|
| 145 |
-
|
| 146 |
-
def process_image(self, image, scale_by, upscale_method, left, top, right, bottom, color, transparent, background=None):
|
| 147 |
-
# 首先进行缩放
|
| 148 |
-
if scale_by != 1.0:
|
| 149 |
-
image = self.scale_image(image, scale_by, upscale_method)
|
| 150 |
-
|
| 151 |
-
# 添加padding
|
| 152 |
-
padded_image = self.add_padding(image, left, top, right, bottom, color, transparent)
|
| 153 |
-
|
| 154 |
-
# 如果有背景图,进行合成
|
| 155 |
-
if background is not None:
|
| 156 |
-
result = []
|
| 157 |
-
for i in range(len(padded_image)):
|
| 158 |
-
# 处理每一帧
|
| 159 |
-
frame = padded_image[i:i+1]
|
| 160 |
-
composited = self.composite_with_background(frame, background)
|
| 161 |
-
result.append(composited)
|
| 162 |
-
padded_image = torch.cat(result, dim=0)
|
| 163 |
-
|
| 164 |
-
# 创建mask
|
| 165 |
-
mask = self.create_mask(image, left, top, right, bottom)
|
| 166 |
-
|
| 167 |
-
return (padded_image, mask)
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
class AD_ImageConcat:
|
| 171 |
-
def __init__(self):
|
| 172 |
-
pass
|
| 173 |
-
|
| 174 |
-
@classmethod
|
| 175 |
-
def INPUT_TYPES(cls):
|
| 176 |
-
return {
|
| 177 |
-
"required": {
|
| 178 |
-
"image1": ("IMAGE",),
|
| 179 |
-
"image2": ("IMAGE",),
|
| 180 |
-
"direction": (["horizontal", "vertical"], {"default": "horizontal"}),
|
| 181 |
-
"match_size": ("BOOLEAN", {"default": True}),
|
| 182 |
-
"method": (["lanczos", "bicubic", "bilinear", "nearest"], {"default": "lanczos"}),
|
| 183 |
-
}
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
RETURN_TYPES = ("IMAGE",)
|
| 187 |
-
FUNCTION = "concat_images"
|
| 188 |
-
CATEGORY = "🌻 Addoor/image"
|
| 189 |
-
|
| 190 |
-
def concat_images(self, image1, image2, direction="horizontal", match_size=False, method="lanczos"):
|
| 191 |
-
try:
|
| 192 |
-
# 转换为 PIL 图像
|
| 193 |
-
img1 = tensor_to_image(image1[0])
|
| 194 |
-
img2 = tensor_to_image(image2[0])
|
| 195 |
-
|
| 196 |
-
# 确保两张图片的模式相同
|
| 197 |
-
if img1.mode != img2.mode:
|
| 198 |
-
if 'A' in img1.mode or 'A' in img2.mode:
|
| 199 |
-
img1 = img1.convert('RGBA')
|
| 200 |
-
img2 = img2.convert('RGBA')
|
| 201 |
-
else:
|
| 202 |
-
img1 = img1.convert('RGB')
|
| 203 |
-
img2 = img2.convert('RGB')
|
| 204 |
-
|
| 205 |
-
# 如果需要匹配尺寸
|
| 206 |
-
if match_size:
|
| 207 |
-
if direction == "horizontal":
|
| 208 |
-
# 横向拼接,匹配高度
|
| 209 |
-
if img1.height != img2.height:
|
| 210 |
-
new_height = img2.height
|
| 211 |
-
new_width = int(img1.width * (new_height / img1.height))
|
| 212 |
-
img1 = img1.resize(
|
| 213 |
-
(new_width, new_height),
|
| 214 |
-
get_sampler_by_name(method)
|
| 215 |
-
)
|
| 216 |
-
else: # vertical
|
| 217 |
-
# 纵向拼接,匹配宽度
|
| 218 |
-
if img1.width != img2.width:
|
| 219 |
-
new_width = img2.width
|
| 220 |
-
new_height = int(img1.height * (new_width / img1.width))
|
| 221 |
-
img1 = img1.resize(
|
| 222 |
-
(new_width, new_height),
|
| 223 |
-
get_sampler_by_name(method)
|
| 224 |
-
)
|
| 225 |
-
|
| 226 |
-
# 创建新图像
|
| 227 |
-
if direction == "horizontal":
|
| 228 |
-
new_width = img1.width + img2.width
|
| 229 |
-
new_height = max(img1.height, img2.height)
|
| 230 |
-
else: # vertical
|
| 231 |
-
new_width = max(img1.width, img2.width)
|
| 232 |
-
new_height = img1.height + img2.height
|
| 233 |
-
|
| 234 |
-
# 创建新的画布
|
| 235 |
-
mode = img1.mode
|
| 236 |
-
new_image = Image.new(mode, (new_width, new_height))
|
| 237 |
-
|
| 238 |
-
# 计算粘贴位置(居中对齐)
|
| 239 |
-
if direction == "horizontal":
|
| 240 |
-
y1 = (new_height - img1.height) // 2
|
| 241 |
-
y2 = (new_height - img2.height) // 2
|
| 242 |
-
new_image.paste(img1, (0, y1))
|
| 243 |
-
new_image.paste(img2, (img1.width, y2))
|
| 244 |
-
else: # vertical
|
| 245 |
-
x1 = (new_width - img1.width) // 2
|
| 246 |
-
x2 = (new_width - img2.width) // 2
|
| 247 |
-
new_image.paste(img1, (x1, 0))
|
| 248 |
-
new_image.paste(img2, (x2, img1.height))
|
| 249 |
-
|
| 250 |
-
# 转换回 tensor
|
| 251 |
-
tensor = image_to_tensor(new_image)
|
| 252 |
-
tensor = tensor.unsqueeze(0)
|
| 253 |
-
tensor = tensor.permute(0, 2, 3, 1)
|
| 254 |
-
|
| 255 |
-
return (tensor,)
|
| 256 |
-
|
| 257 |
-
except Exception as e:
|
| 258 |
-
print(f"Error concatenating images: {str(e)}")
|
| 259 |
-
return (image1,)
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
# 添加颜色常量
|
| 263 |
-
COLORS = [
|
| 264 |
-
"white", "black", "red", "green", "blue", "yellow", "purple", "orange",
|
| 265 |
-
"gray", "brown", "pink", "cyan", "custom"
|
| 266 |
-
]
|
| 267 |
-
|
| 268 |
-
# 颜色映射
|
| 269 |
-
color_mapping = {
|
| 270 |
-
"white": "#FFFFFF",
|
| 271 |
-
"black": "#000000",
|
| 272 |
-
"red": "#FF0000",
|
| 273 |
-
"green": "#00FF00",
|
| 274 |
-
"blue": "#0000FF",
|
| 275 |
-
"yellow": "#FFFF00",
|
| 276 |
-
"purple": "#800080",
|
| 277 |
-
"orange": "#FFA500",
|
| 278 |
-
"gray": "#808080",
|
| 279 |
-
"brown": "#A52A2A",
|
| 280 |
-
"pink": "#FFC0CB",
|
| 281 |
-
"cyan": "#00FFFF"
|
| 282 |
-
}
|
| 283 |
-
|
| 284 |
-
def get_color_values(color_name, color_hex, mapping):
|
| 285 |
-
"""获取颜色值"""
|
| 286 |
-
if color_name == "custom":
|
| 287 |
-
return color_hex
|
| 288 |
-
return mapping.get(color_name, "#000000")
|
| 289 |
-
|
| 290 |
-
# 添加图像处理工具函数
|
| 291 |
-
def get_sampler_by_name(method: str) -> int:
|
| 292 |
-
"""Get PIL resampling method by name."""
|
| 293 |
-
samplers = {
|
| 294 |
-
"lanczos": Image.LANCZOS,
|
| 295 |
-
"bicubic": Image.BICUBIC,
|
| 296 |
-
"hamming": Image.HAMMING,
|
| 297 |
-
"bilinear": Image.BILINEAR,
|
| 298 |
-
"box": Image.BOX,
|
| 299 |
-
"nearest": Image.NEAREST
|
| 300 |
-
}
|
| 301 |
-
return samplers.get(method, Image.LANCZOS)
|
| 302 |
-
|
| 303 |
-
class AD_ColorImage:
|
| 304 |
-
"""Create a solid color image with advanced options."""
|
| 305 |
-
|
| 306 |
-
def __init__(self):
|
| 307 |
-
pass
|
| 308 |
-
|
| 309 |
-
# 预定义颜色映射
|
| 310 |
-
COLOR_PRESETS = {
|
| 311 |
-
"white": "#FFFFFF",
|
| 312 |
-
"black": "#000000",
|
| 313 |
-
"red": "#FF0000",
|
| 314 |
-
"green": "#00FF00",
|
| 315 |
-
"blue": "#0000FF",
|
| 316 |
-
"yellow": "#FFFF00",
|
| 317 |
-
"purple": "#800080",
|
| 318 |
-
"orange": "#FFA500",
|
| 319 |
-
"gray": "#808080",
|
| 320 |
-
"custom": "custom"
|
| 321 |
-
}
|
| 322 |
-
|
| 323 |
-
@classmethod
|
| 324 |
-
def INPUT_TYPES(cls):
|
| 325 |
-
return {
|
| 326 |
-
"required": {
|
| 327 |
-
"width": ("INT", {
|
| 328 |
-
"default": 512,
|
| 329 |
-
"min": 1,
|
| 330 |
-
"max": 8192,
|
| 331 |
-
"step": 1
|
| 332 |
-
}),
|
| 333 |
-
"height": ("INT", {
|
| 334 |
-
"default": 512,
|
| 335 |
-
"min": 1,
|
| 336 |
-
"max": 8192,
|
| 337 |
-
"step": 1
|
| 338 |
-
}),
|
| 339 |
-
"color": (list(cls.COLOR_PRESETS.keys()), {
|
| 340 |
-
"default": "white"
|
| 341 |
-
}),
|
| 342 |
-
"hex_color": ("STRING", {
|
| 343 |
-
"default": "#FFFFFF",
|
| 344 |
-
"multiline": False
|
| 345 |
-
}),
|
| 346 |
-
"alpha": ("FLOAT", {
|
| 347 |
-
"default": 1.0,
|
| 348 |
-
"min": 0.0,
|
| 349 |
-
"max": 1.0,
|
| 350 |
-
"step": 0.01
|
| 351 |
-
}),
|
| 352 |
-
},
|
| 353 |
-
"optional": {
|
| 354 |
-
"reference_image": ("IMAGE",),
|
| 355 |
-
}
|
| 356 |
-
}
|
| 357 |
-
|
| 358 |
-
RETURN_TYPES = ("IMAGE",)
|
| 359 |
-
RETURN_NAMES = ("image",)
|
| 360 |
-
FUNCTION = "create_color_image"
|
| 361 |
-
CATEGORY = "🌻 Addoor/image"
|
| 362 |
-
|
| 363 |
-
def hex_to_rgb(self, hex_color: str) -> tuple:
|
| 364 |
-
"""Convert hex color to RGB tuple."""
|
| 365 |
-
hex_color = hex_color.lstrip('#')
|
| 366 |
-
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
| 367 |
-
|
| 368 |
-
def create_color_image(
|
| 369 |
-
self,
|
| 370 |
-
width: int,
|
| 371 |
-
height: int,
|
| 372 |
-
color: str,
|
| 373 |
-
hex_color: str,
|
| 374 |
-
alpha: float,
|
| 375 |
-
reference_image = None
|
| 376 |
-
):
|
| 377 |
-
try:
|
| 378 |
-
# 如果有参考图片,使用其尺寸
|
| 379 |
-
if reference_image is not None:
|
| 380 |
-
_, height, width, _ = reference_image.shape
|
| 381 |
-
|
| 382 |
-
# 获取颜色值
|
| 383 |
-
if color == "custom":
|
| 384 |
-
rgb_color = self.hex_to_rgb(hex_color)
|
| 385 |
-
else:
|
| 386 |
-
rgb_color = self.hex_to_rgb(self.COLOR_PRESETS[color])
|
| 387 |
-
|
| 388 |
-
# 创建图像
|
| 389 |
-
canvas = Image.new(
|
| 390 |
-
"RGBA",
|
| 391 |
-
(width, height),
|
| 392 |
-
(*rgb_color, int(alpha * 255))
|
| 393 |
-
)
|
| 394 |
-
|
| 395 |
-
# 转换为 tensor
|
| 396 |
-
tensor = image_to_tensor(canvas)
|
| 397 |
-
tensor = tensor.unsqueeze(0)
|
| 398 |
-
tensor = tensor.permute(0, 2, 3, 1)
|
| 399 |
-
|
| 400 |
-
return (tensor,)
|
| 401 |
-
|
| 402 |
-
except Exception as e:
|
| 403 |
-
print(f"Error creating color image: {str(e)}")
|
| 404 |
-
canvas = Image.new(
|
| 405 |
-
"RGB",
|
| 406 |
-
(width, height),
|
| 407 |
-
(0, 0, 0)
|
| 408 |
-
)
|
| 409 |
-
tensor = image_to_tensor(canvas)
|
| 410 |
-
tensor = tensor.unsqueeze(0)
|
| 411 |
-
tensor = tensor.permute(0, 2, 3, 1)
|
| 412 |
-
return (tensor,)
|
| 413 |
-
|
| 414 |
-
def tensor_to_image(tensor):
|
| 415 |
-
"""Convert tensor to PIL Image."""
|
| 416 |
-
if len(tensor.shape) == 4:
|
| 417 |
-
tensor = tensor.squeeze(0) # 移除 batch 维度
|
| 418 |
-
return t.ToPILImage()(tensor.permute(2, 0, 1))
|
| 419 |
-
|
| 420 |
-
def image_to_tensor(image):
|
| 421 |
-
"""Convert PIL Image to tensor."""
|
| 422 |
-
tensor = t.ToTensor()(image)
|
| 423 |
-
return tensor
|
| 424 |
-
|
| 425 |
-
NODE_CLASS_MAPPINGS = {
|
| 426 |
-
"AD_advanced-padding": AD_PaddingAdvanced,
|
| 427 |
-
"AD_image-concat": AD_ImageConcat,
|
| 428 |
-
"AD_color-image": AD_ColorImage,
|
| 429 |
-
}
|
| 430 |
-
|
| 431 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 432 |
-
"AD_advanced-padding": "AD Advanced Padding",
|
| 433 |
-
"AD_image-concat": "AD Image Concatenation",
|
| 434 |
-
"AD_color-image": "AD Color Image",
|
| 435 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/nodes/AddPaddingBase.py
DELETED
|
@@ -1,99 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
@author: ealkanat
|
| 3 |
-
@title: ComfyUI Easy Padding
|
| 4 |
-
@description: A simple custom node for creates padding for given image
|
| 5 |
-
@version: 1.0.2
|
| 6 |
-
@project: https://github.com/erkana/comfyui_easy_padding
|
| 7 |
-
@author: https://github.com/erkana
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
import PIL.Image as Image
|
| 11 |
-
import PIL.ImageDraw as ImageDraw
|
| 12 |
-
import numpy as np
|
| 13 |
-
import torch
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
class AddPaddingBase:
|
| 17 |
-
def __init__(self):
|
| 18 |
-
pass
|
| 19 |
-
|
| 20 |
-
FUNCTION = "resize"
|
| 21 |
-
CATEGORY = "🌻 Addoor/image"
|
| 22 |
-
|
| 23 |
-
def add_padding(self, image, left, top, right, bottom, color="#ffffff", transparent=False):
|
| 24 |
-
padded_images = []
|
| 25 |
-
image = [self.tensor2pil(img) for img in image]
|
| 26 |
-
for img in image:
|
| 27 |
-
padded_image = Image.new("RGBA" if transparent else "RGB",
|
| 28 |
-
(img.width + left + right, img.height + top + bottom),
|
| 29 |
-
(0, 0, 0, 0) if transparent else self.hex_to_tuple(color))
|
| 30 |
-
padded_image.paste(img, (left, top))
|
| 31 |
-
padded_images.append(self.pil2tensor(padded_image))
|
| 32 |
-
return torch.cat(padded_images, dim=0)
|
| 33 |
-
|
| 34 |
-
def create_mask(self, image, left, top, right, bottom):
|
| 35 |
-
masks = []
|
| 36 |
-
image = [self.tensor2pil(img) for img in image]
|
| 37 |
-
for img in image:
|
| 38 |
-
shape = (left, top, img.width + left, img.height + top)
|
| 39 |
-
mask_image = Image.new("L", (img.width + left + right, img.height + top + bottom), 255)
|
| 40 |
-
draw = ImageDraw.Draw(mask_image)
|
| 41 |
-
draw.rectangle(shape, fill=0)
|
| 42 |
-
masks.append(self.pil2tensor(mask_image))
|
| 43 |
-
return torch.cat(masks, dim=0)
|
| 44 |
-
|
| 45 |
-
def hex_to_float(self, color):
|
| 46 |
-
if not isinstance(color, str):
|
| 47 |
-
raise ValueError("Color must be a hex string")
|
| 48 |
-
color = color.strip("#")
|
| 49 |
-
return int(color, 16) / 255.0
|
| 50 |
-
|
| 51 |
-
def hex_to_tuple(self, color):
|
| 52 |
-
if not isinstance(color, str):
|
| 53 |
-
raise ValueError("Color must be a hex string")
|
| 54 |
-
color = color.strip("#")
|
| 55 |
-
return tuple([int(color[i:i + 2], 16) for i in range(0, len(color), 2)])
|
| 56 |
-
|
| 57 |
-
# Tensor to PIL
|
| 58 |
-
def tensor2pil(self, image):
|
| 59 |
-
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
| 60 |
-
|
| 61 |
-
# PIL to Tensor
|
| 62 |
-
def pil2tensor(self, image):
|
| 63 |
-
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
class AddPadding(AddPaddingBase):
|
| 68 |
-
|
| 69 |
-
def __init__(self):
|
| 70 |
-
pass
|
| 71 |
-
|
| 72 |
-
@classmethod
|
| 73 |
-
def INPUT_TYPES(s):
|
| 74 |
-
return {
|
| 75 |
-
"required": {
|
| 76 |
-
"image": ("IMAGE",),
|
| 77 |
-
"left": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
|
| 78 |
-
"top": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
|
| 79 |
-
"right": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
|
| 80 |
-
"bottom": ("INT", {"default": 0, "step": 1, "min": 0, "max": 4096}),
|
| 81 |
-
"color": ("STRING", {"default": "#ffffff"}),
|
| 82 |
-
"transparent": ("BOOLEAN", {"default": False}),
|
| 83 |
-
},
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
RETURN_TYPES = ("IMAGE", "MASK")
|
| 87 |
-
|
| 88 |
-
def resize(self, image, left, top, right, bottom, color, transparent):
|
| 89 |
-
return (self.add_padding(image, left, top, right, bottom, color, transparent),
|
| 90 |
-
self.create_mask(image, left, top, right, bottom),)
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
NODE_CLASS_MAPPINGS = {
|
| 94 |
-
"comfyui-easy-padding": AddPadding,
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 98 |
-
"comfyui-easy-padding": "ComfyUI Easy Padding",
|
| 99 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-FofrToolkit.py
DELETED
|
@@ -1,126 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
@name: "ComfyUI FofrToolkit",
|
| 3 |
-
@version: (1,0,0),
|
| 4 |
-
@author: "fofr",
|
| 5 |
-
@description: "Experimental toolkit for comfyui.",
|
| 6 |
-
@project: "https://github.com/fofr/comfyui-fofr-toolkit",
|
| 7 |
-
@url: "https://github.com/fofr",
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
class ToolkitIncrementer:
|
| 12 |
-
@classmethod
|
| 13 |
-
def INPUT_TYPES(cls):
|
| 14 |
-
return {
|
| 15 |
-
"required": {
|
| 16 |
-
"current_index": (
|
| 17 |
-
"INT",
|
| 18 |
-
{
|
| 19 |
-
"default": 0,
|
| 20 |
-
"min": 0,
|
| 21 |
-
"max": 0xFFFFFFFFFFFFFFFF,
|
| 22 |
-
"control_after_generate": True,
|
| 23 |
-
},
|
| 24 |
-
),
|
| 25 |
-
},
|
| 26 |
-
"optional": {
|
| 27 |
-
"max": (
|
| 28 |
-
"INT",
|
| 29 |
-
{"default": 10, "min": 0, "max": 0xFFFFFFFFFFFFFFFF},
|
| 30 |
-
),
|
| 31 |
-
},
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
RETURN_TYPES = ("INT", "STRING")
|
| 35 |
-
RETURN_NAMES = ("INT", "STRING")
|
| 36 |
-
FUNCTION = "increment"
|
| 37 |
-
CATEGORY = "🌻 Addoor/Utilities"
|
| 38 |
-
|
| 39 |
-
def increment(self, current_index, max=0):
|
| 40 |
-
if max == 0:
|
| 41 |
-
result = current_index
|
| 42 |
-
else:
|
| 43 |
-
result = current_index % (max + 1)
|
| 44 |
-
|
| 45 |
-
return (result, str(result))
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
class ToolkitWidthAndHeightFromAspectRatio:
|
| 49 |
-
@classmethod
|
| 50 |
-
def INPUT_TYPES(cls):
|
| 51 |
-
return {
|
| 52 |
-
"required": {
|
| 53 |
-
"aspect_ratio": (
|
| 54 |
-
[
|
| 55 |
-
"1:1",
|
| 56 |
-
"1:2",
|
| 57 |
-
"2:1",
|
| 58 |
-
"2:3",
|
| 59 |
-
"3:2",
|
| 60 |
-
"3:4",
|
| 61 |
-
"4:3",
|
| 62 |
-
"4:5",
|
| 63 |
-
"5:4",
|
| 64 |
-
"9:16",
|
| 65 |
-
"16:9",
|
| 66 |
-
"9:21",
|
| 67 |
-
"21:9",
|
| 68 |
-
],
|
| 69 |
-
{"default": "1:1"},
|
| 70 |
-
),
|
| 71 |
-
"target_size": ("INT", {"default": 1024, "min": 64, "max": 8192}),
|
| 72 |
-
},
|
| 73 |
-
"optional": {
|
| 74 |
-
"multiple_of": ("INT", {"default": 8, "min": 1, "max": 1024}),
|
| 75 |
-
},
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
RETURN_TYPES = ("INT", "INT")
|
| 79 |
-
RETURN_NAMES = ("width", "height")
|
| 80 |
-
FUNCTION = "width_and_height_from_aspect_ratio"
|
| 81 |
-
CATEGORY = "🌻 Addoor/Utilities"
|
| 82 |
-
|
| 83 |
-
def width_and_height_from_aspect_ratio(
|
| 84 |
-
self, aspect_ratio, target_size, multiple_of=8
|
| 85 |
-
):
|
| 86 |
-
w, h = map(int, aspect_ratio.split(":"))
|
| 87 |
-
scale = (target_size**2 / (w * h)) ** 0.5
|
| 88 |
-
width = round(w * scale / multiple_of) * multiple_of
|
| 89 |
-
height = round(h * scale / multiple_of) * multiple_of
|
| 90 |
-
return (width, height)
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
class ToolkitWidthAndHeightForImageScaling:
|
| 94 |
-
@classmethod
|
| 95 |
-
def INPUT_TYPES(cls):
|
| 96 |
-
return {
|
| 97 |
-
"required": {
|
| 98 |
-
"image": ("IMAGE",),
|
| 99 |
-
"target_size": (
|
| 100 |
-
"INT",
|
| 101 |
-
{"default": 1024, "min": 64, "max": 8192},
|
| 102 |
-
),
|
| 103 |
-
},
|
| 104 |
-
"optional": {
|
| 105 |
-
"multiple_of": ("INT", {"default": 8, "min": 1, "max": 1024}),
|
| 106 |
-
},
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
RETURN_TYPES = ("INT", "INT")
|
| 110 |
-
RETURN_NAMES = ("width", "height")
|
| 111 |
-
FUNCTION = "scale_image_to_target"
|
| 112 |
-
CATEGORY = "🌻 Addoor/Utilities"
|
| 113 |
-
|
| 114 |
-
def scale_image_to_target(self, image, target_size, multiple_of=8):
|
| 115 |
-
h, w = image.shape[1:3]
|
| 116 |
-
scale = (target_size**2 / (w * h)) ** 0.5
|
| 117 |
-
width = round(w * scale / multiple_of) * multiple_of
|
| 118 |
-
height = round(h * scale / multiple_of) * multiple_of
|
| 119 |
-
return (width, height)
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
NODE_CLASS_MAPPINGS = {
|
| 123 |
-
"Incrementer 🪴": ToolkitIncrementer,
|
| 124 |
-
"Width and height from aspect ratio 🪴": ToolkitWidthAndHeightFromAspectRatio,
|
| 125 |
-
"Width and height for scaling image to ideal resolution 🪴": ToolkitWidthAndHeightForImageScaling,
|
| 126 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-imageResize.py
DELETED
|
@@ -1,172 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
@author: palant
|
| 3 |
-
@title: ComfyUI-imageResize
|
| 4 |
-
@description: Custom node for image resizing.
|
| 5 |
-
@version: 1.0.0
|
| 6 |
-
@project: https://github.com/palant/image-resize-comfyui
|
| 7 |
-
@author: https://github.com/palant
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
import torch
|
| 11 |
-
|
| 12 |
-
class ImageResize:
|
| 13 |
-
def __init__(self):
|
| 14 |
-
pass
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
ACTION_TYPE_RESIZE = "resize only"
|
| 18 |
-
ACTION_TYPE_CROP = "crop to ratio"
|
| 19 |
-
ACTION_TYPE_PAD = "pad to ratio"
|
| 20 |
-
RESIZE_MODE_DOWNSCALE = "reduce size only"
|
| 21 |
-
RESIZE_MODE_UPSCALE = "increase size only"
|
| 22 |
-
RESIZE_MODE_ANY = "any"
|
| 23 |
-
RETURN_TYPES = ("IMAGE", "MASK",)
|
| 24 |
-
FUNCTION = "resize"
|
| 25 |
-
CATEGORY = "🌻 Addoor/image"
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
@classmethod
|
| 29 |
-
def INPUT_TYPES(s):
|
| 30 |
-
return {
|
| 31 |
-
"required": {
|
| 32 |
-
"pixels": ("IMAGE",),
|
| 33 |
-
"action": ([s.ACTION_TYPE_RESIZE, s.ACTION_TYPE_CROP, s.ACTION_TYPE_PAD],),
|
| 34 |
-
"smaller_side": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 8}),
|
| 35 |
-
"larger_side": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 8}),
|
| 36 |
-
"scale_factor": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.1}),
|
| 37 |
-
"resize_mode": ([s.RESIZE_MODE_DOWNSCALE, s.RESIZE_MODE_UPSCALE, s.RESIZE_MODE_ANY],),
|
| 38 |
-
"side_ratio": ("STRING", {"default": "4:3"}),
|
| 39 |
-
"crop_pad_position": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 1.0, "step": 0.01}),
|
| 40 |
-
"pad_feathering": ("INT", {"default": 20, "min": 0, "max": 8192, "step": 1}),
|
| 41 |
-
},
|
| 42 |
-
"optional": {
|
| 43 |
-
"mask_optional": ("MASK",),
|
| 44 |
-
},
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
@classmethod
|
| 49 |
-
def VALIDATE_INPUTS(s, action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio, **_):
|
| 50 |
-
if side_ratio is not None:
|
| 51 |
-
if action != s.ACTION_TYPE_RESIZE and s.parse_side_ratio(side_ratio) is None:
|
| 52 |
-
return f"Invalid side ratio: {side_ratio}"
|
| 53 |
-
|
| 54 |
-
if smaller_side is not None and larger_side is not None and scale_factor is not None:
|
| 55 |
-
if int(smaller_side > 0) + int(larger_side > 0) + int(scale_factor > 0) > 1:
|
| 56 |
-
return f"At most one scaling rule (smaller_side, larger_side, scale_factor) should be enabled by setting a non-zero value"
|
| 57 |
-
|
| 58 |
-
if scale_factor is not None:
|
| 59 |
-
if resize_mode == s.RESIZE_MODE_DOWNSCALE and scale_factor > 1.0:
|
| 60 |
-
return f"For resize_mode {s.RESIZE_MODE_DOWNSCALE}, scale_factor should be less than one but got {scale_factor}"
|
| 61 |
-
if resize_mode == s.RESIZE_MODE_UPSCALE and scale_factor > 0.0 and scale_factor < 1.0:
|
| 62 |
-
return f"For resize_mode {s.RESIZE_MODE_UPSCALE}, scale_factor should be larger than one but got {scale_factor}"
|
| 63 |
-
|
| 64 |
-
return True
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
@classmethod
|
| 68 |
-
def parse_side_ratio(s, side_ratio):
|
| 69 |
-
try:
|
| 70 |
-
x, y = map(int, side_ratio.split(":", 1))
|
| 71 |
-
if x < 1 or y < 1:
|
| 72 |
-
raise Exception("Ratio factors have to be positive numbers")
|
| 73 |
-
return float(x) / float(y)
|
| 74 |
-
except:
|
| 75 |
-
return None
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def resize(self, pixels, action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio, crop_pad_position, pad_feathering, mask_optional=None):
|
| 79 |
-
validity = self.VALIDATE_INPUTS(action, smaller_side, larger_side, scale_factor, resize_mode, side_ratio)
|
| 80 |
-
if validity is not True:
|
| 81 |
-
raise Exception(validity)
|
| 82 |
-
|
| 83 |
-
height, width = pixels.shape[1:3]
|
| 84 |
-
if mask_optional is None:
|
| 85 |
-
mask = torch.zeros(1, height, width, dtype=torch.float32)
|
| 86 |
-
else:
|
| 87 |
-
mask = mask_optional
|
| 88 |
-
if mask.shape[1] != height or mask.shape[2] != width:
|
| 89 |
-
mask = torch.nn.functional.interpolate(mask.unsqueeze(0), size=(height, width), mode="bicubic").squeeze(0).clamp(0.0, 1.0)
|
| 90 |
-
|
| 91 |
-
crop_x, crop_y, pad_x, pad_y = (0.0, 0.0, 0.0, 0.0)
|
| 92 |
-
if action == self.ACTION_TYPE_CROP:
|
| 93 |
-
target_ratio = self.parse_side_ratio(side_ratio)
|
| 94 |
-
if height * target_ratio < width:
|
| 95 |
-
crop_x = width - height * target_ratio
|
| 96 |
-
else:
|
| 97 |
-
crop_y = height - width / target_ratio
|
| 98 |
-
elif action == self.ACTION_TYPE_PAD:
|
| 99 |
-
target_ratio = self.parse_side_ratio(side_ratio)
|
| 100 |
-
if height * target_ratio > width:
|
| 101 |
-
pad_x = height * target_ratio - width
|
| 102 |
-
else:
|
| 103 |
-
pad_y = width / target_ratio - height
|
| 104 |
-
|
| 105 |
-
if smaller_side > 0:
|
| 106 |
-
if width + pad_x - crop_x > height + pad_y - crop_y:
|
| 107 |
-
scale_factor = float(smaller_side) / (height + pad_y - crop_y)
|
| 108 |
-
else:
|
| 109 |
-
scale_factor = float(smaller_side) / (width + pad_x - crop_x)
|
| 110 |
-
if larger_side > 0:
|
| 111 |
-
if width + pad_x - crop_x > height + pad_y - crop_y:
|
| 112 |
-
scale_factor = float(larger_side) / (width + pad_x - crop_x)
|
| 113 |
-
else:
|
| 114 |
-
scale_factor = float(larger_side) / (height + pad_y - crop_y)
|
| 115 |
-
|
| 116 |
-
if (resize_mode == self.RESIZE_MODE_DOWNSCALE and scale_factor >= 1.0) or (resize_mode == self.RESIZE_MODE_UPSCALE and scale_factor <= 1.0):
|
| 117 |
-
scale_factor = 0.0
|
| 118 |
-
|
| 119 |
-
if scale_factor > 0.0:
|
| 120 |
-
pixels = torch.nn.functional.interpolate(pixels.movedim(-1, 1), scale_factor=scale_factor, mode="bicubic", antialias=True).movedim(1, -1).clamp(0.0, 1.0)
|
| 121 |
-
mask = torch.nn.functional.interpolate(mask.unsqueeze(0), scale_factor=scale_factor, mode="bicubic", antialias=True).squeeze(0).clamp(0.0, 1.0)
|
| 122 |
-
height, width = pixels.shape[1:3]
|
| 123 |
-
|
| 124 |
-
crop_x *= scale_factor
|
| 125 |
-
crop_y *= scale_factor
|
| 126 |
-
pad_x *= scale_factor
|
| 127 |
-
pad_y *= scale_factor
|
| 128 |
-
|
| 129 |
-
if crop_x > 0.0 or crop_y > 0.0:
|
| 130 |
-
remove_x = (round(crop_x * crop_pad_position), round(crop_x * (1 - crop_pad_position))) if crop_x > 0.0 else (0, 0)
|
| 131 |
-
remove_y = (round(crop_y * crop_pad_position), round(crop_y * (1 - crop_pad_position))) if crop_y > 0.0 else (0, 0)
|
| 132 |
-
pixels = pixels[:, remove_y[0]:height - remove_y[1], remove_x[0]:width - remove_x[1], :]
|
| 133 |
-
mask = mask[:, remove_y[0]:height - remove_y[1], remove_x[0]:width - remove_x[1]]
|
| 134 |
-
elif pad_x > 0.0 or pad_y > 0.0:
|
| 135 |
-
add_x = (round(pad_x * crop_pad_position), round(pad_x * (1 - crop_pad_position))) if pad_x > 0.0 else (0, 0)
|
| 136 |
-
add_y = (round(pad_y * crop_pad_position), round(pad_y * (1 - crop_pad_position))) if pad_y > 0.0 else (0, 0)
|
| 137 |
-
|
| 138 |
-
new_pixels = torch.zeros(pixels.shape[0], height + add_y[0] + add_y[1], width + add_x[0] + add_x[1], pixels.shape[3], dtype=torch.float32)
|
| 139 |
-
new_pixels[:, add_y[0]:height + add_y[0], add_x[0]:width + add_x[0], :] = pixels
|
| 140 |
-
pixels = new_pixels
|
| 141 |
-
|
| 142 |
-
new_mask = torch.ones(mask.shape[0], height + add_y[0] + add_y[1], width + add_x[0] + add_x[1], dtype=torch.float32)
|
| 143 |
-
new_mask[:, add_y[0]:height + add_y[0], add_x[0]:width + add_x[0]] = mask
|
| 144 |
-
mask = new_mask
|
| 145 |
-
|
| 146 |
-
if pad_feathering > 0:
|
| 147 |
-
for i in range(mask.shape[0]):
|
| 148 |
-
for j in range(pad_feathering):
|
| 149 |
-
feather_strength = (1 - j / pad_feathering) * (1 - j / pad_feathering)
|
| 150 |
-
if add_x[0] > 0 and j < width:
|
| 151 |
-
for k in range(height):
|
| 152 |
-
mask[i, k, add_x[0] + j] = max(mask[i, k, add_x[0] + j], feather_strength)
|
| 153 |
-
if add_x[1] > 0 and j < width:
|
| 154 |
-
for k in range(height):
|
| 155 |
-
mask[i, k, width + add_x[0] - j - 1] = max(mask[i, k, width + add_x[0] - j - 1], feather_strength)
|
| 156 |
-
if add_y[0] > 0 and j < height:
|
| 157 |
-
for k in range(width):
|
| 158 |
-
mask[i, add_y[0] + j, k] = max(mask[i, add_y[0] + j, k], feather_strength)
|
| 159 |
-
if add_y[1] > 0 and j < height:
|
| 160 |
-
for k in range(width):
|
| 161 |
-
mask[i, height + add_y[0] - j - 1, k] = max(mask[i, height + add_y[0] - j - 1, k], feather_strength)
|
| 162 |
-
|
| 163 |
-
return (pixels, mask)
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
NODE_CLASS_MAPPINGS = {
|
| 167 |
-
"ImageResize": ImageResize
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 171 |
-
"ImageResize": "Image Resize"
|
| 172 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-textAppend.py
DELETED
|
@@ -1,48 +0,0 @@
|
|
| 1 |
-
# text_append_node.py
|
| 2 |
-
|
| 3 |
-
class TextAppendNode:
|
| 4 |
-
def __init__(self):
|
| 5 |
-
pass
|
| 6 |
-
|
| 7 |
-
@classmethod
|
| 8 |
-
def INPUT_TYPES(cls):
|
| 9 |
-
return {
|
| 10 |
-
"required": {
|
| 11 |
-
"current_text": ("STRING", {"multiline": True}),
|
| 12 |
-
"new_text": ("STRING", {"multiline": True}),
|
| 13 |
-
"mode": (["append", "prepend"],),
|
| 14 |
-
},
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
RETURN_TYPES = ("STRING",)
|
| 18 |
-
FUNCTION = "process_text"
|
| 19 |
-
CATEGORY = "🌻 Addoor/text"
|
| 20 |
-
|
| 21 |
-
def process_text(self, current_text, new_text, mode):
|
| 22 |
-
current_lines = [line.strip() for line in current_text.split('\n') if line.strip()]
|
| 23 |
-
new_lines = [line.strip() for line in new_text.split('\n') if line.strip()]
|
| 24 |
-
|
| 25 |
-
result = []
|
| 26 |
-
|
| 27 |
-
# If current_text is empty, return new_text
|
| 28 |
-
if not current_lines:
|
| 29 |
-
return (new_text,)
|
| 30 |
-
|
| 31 |
-
if mode == "append":
|
| 32 |
-
for current_line in current_lines:
|
| 33 |
-
for new_line in new_lines:
|
| 34 |
-
result.append(f"{current_line} {new_line}")
|
| 35 |
-
elif mode == "prepend":
|
| 36 |
-
for current_line in current_lines:
|
| 37 |
-
for new_line in new_lines:
|
| 38 |
-
result.append(f"{new_line} {current_line}")
|
| 39 |
-
|
| 40 |
-
return ('\n'.join(result),)
|
| 41 |
-
|
| 42 |
-
NODE_CLASS_MAPPINGS = {
|
| 43 |
-
"TextAppendNode": TextAppendNode
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 47 |
-
"TextAppendNode": "Text Append/Prepend"
|
| 48 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/nodes/__init__.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Addoor Nodes for ComfyUI
|
| 3 |
-
Provides nodes for image processing and other utilities
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import os
|
| 7 |
-
import glob
|
| 8 |
-
import logging
|
| 9 |
-
import importlib
|
| 10 |
-
|
| 11 |
-
logging.basicConfig(level=logging.INFO)
|
| 12 |
-
logger = logging.getLogger(__name__)
|
| 13 |
-
|
| 14 |
-
# 首先定义映射字典
|
| 15 |
-
NODE_CLASS_MAPPINGS = {}
|
| 16 |
-
NODE_DISPLAY_NAME_MAPPINGS = {}
|
| 17 |
-
|
| 18 |
-
# 获取当前目录下所有的 .py 文件
|
| 19 |
-
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 20 |
-
py_files = glob.glob(os.path.join(current_dir, "*.py"))
|
| 21 |
-
|
| 22 |
-
# 直接注册所有节点
|
| 23 |
-
for file_path in py_files:
|
| 24 |
-
# 跳过 __init__.py
|
| 25 |
-
if "__init__.py" in file_path:
|
| 26 |
-
continue
|
| 27 |
-
|
| 28 |
-
try:
|
| 29 |
-
# 获取模块名(不含.py)
|
| 30 |
-
module_name = os.path.basename(file_path)[:-3]
|
| 31 |
-
# 使用相对导入
|
| 32 |
-
module = importlib.import_module(f".{module_name}", package=__package__)
|
| 33 |
-
|
| 34 |
-
# 如果模块有节点映射,则更新
|
| 35 |
-
if hasattr(module, 'NODE_CLASS_MAPPINGS'):
|
| 36 |
-
NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
|
| 37 |
-
if hasattr(module, 'NODE_DISPLAY_NAME_MAPPINGS'):
|
| 38 |
-
NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
|
| 39 |
-
|
| 40 |
-
logger.info(f"Imported {module_name} successfully")
|
| 41 |
-
except ImportError as e:
|
| 42 |
-
logger.error(f"Error importing {module_name}: {str(e)}")
|
| 43 |
-
except Exception as e:
|
| 44 |
-
logger.error(f"Error processing {module_name}: {str(e)}")
|
| 45 |
-
|
| 46 |
-
# 如果允许测试节点,导入测试节点
|
| 47 |
-
allow_test_nodes = True
|
| 48 |
-
if allow_test_nodes:
|
| 49 |
-
try:
|
| 50 |
-
from .excluded.experimental_nodes import *
|
| 51 |
-
# 更新映射
|
| 52 |
-
if 'NODE_CLASS_MAPPINGS' in locals():
|
| 53 |
-
NODE_CLASS_MAPPINGS.update(locals().get('NODE_CLASS_MAPPINGS', {}))
|
| 54 |
-
if 'NODE_DISPLAY_NAME_MAPPINGS' in locals():
|
| 55 |
-
NODE_DISPLAY_NAME_MAPPINGS.update(locals().get('NODE_DISPLAY_NAME_MAPPINGS', {}))
|
| 56 |
-
except ModuleNotFoundError:
|
| 57 |
-
pass
|
| 58 |
-
|
| 59 |
-
logger.debug(f"Registered nodes: {list(NODE_CLASS_MAPPINGS.keys())}")
|
| 60 |
-
|
| 61 |
-
WEB_DIRECTORY = "./web"
|
| 62 |
-
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/nodes/imagecreatemask.py
DELETED
|
@@ -1,109 +0,0 @@
|
|
| 1 |
-
from PIL import Image
|
| 2 |
-
import numpy as np
|
| 3 |
-
import torch
|
| 4 |
-
import torchvision.transforms.functional as TF
|
| 5 |
-
|
| 6 |
-
def tensor2pil(image):
|
| 7 |
-
return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8))
|
| 8 |
-
|
| 9 |
-
def pil2tensor(image):
|
| 10 |
-
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
| 11 |
-
|
| 12 |
-
def tensor2mask(t: torch.Tensor) -> torch.Tensor:
|
| 13 |
-
size = t.size()
|
| 14 |
-
if (len(size) < 4):
|
| 15 |
-
return t
|
| 16 |
-
if size[3] == 1:
|
| 17 |
-
return t[:,:,:,0]
|
| 18 |
-
elif size[3] == 4:
|
| 19 |
-
# Use alpha if available
|
| 20 |
-
if torch.min(t[:, :, :, 3]).item() != 1.:
|
| 21 |
-
return t[:,:,:,3]
|
| 22 |
-
# Convert RGB to grayscale
|
| 23 |
-
return TF.rgb_to_grayscale(t.permute(0,3,1,2), num_output_channels=1)[:,0,:,:]
|
| 24 |
-
|
| 25 |
-
class image_concat_mask:
|
| 26 |
-
def __init__(self):
|
| 27 |
-
pass
|
| 28 |
-
|
| 29 |
-
@classmethod
|
| 30 |
-
def INPUT_TYPES(cls):
|
| 31 |
-
return {
|
| 32 |
-
"required": {
|
| 33 |
-
"image1": ("IMAGE",),
|
| 34 |
-
},
|
| 35 |
-
"optional": {
|
| 36 |
-
"image2": ("IMAGE",),
|
| 37 |
-
"mask": ("MASK",),
|
| 38 |
-
}
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
RETURN_TYPES = ("IMAGE", "MASK",)
|
| 42 |
-
RETURN_NAMES = ("image", "mask")
|
| 43 |
-
FUNCTION = "image_concat_mask"
|
| 44 |
-
CATEGORY = "🌻 Addoor/image"
|
| 45 |
-
|
| 46 |
-
def image_concat_mask(self, image1, image2=None, mask=None):
|
| 47 |
-
processed_images = []
|
| 48 |
-
masks = []
|
| 49 |
-
|
| 50 |
-
for idx, img1 in enumerate(image1):
|
| 51 |
-
# Convert tensor to PIL
|
| 52 |
-
pil_image1 = tensor2pil(img1)
|
| 53 |
-
|
| 54 |
-
# Get first image dimensions
|
| 55 |
-
width1, height1 = pil_image1.size
|
| 56 |
-
|
| 57 |
-
if image2 is not None and idx < len(image2):
|
| 58 |
-
# Use provided second image
|
| 59 |
-
pil_image2 = tensor2pil(image2[idx])
|
| 60 |
-
width2, height2 = pil_image2.size
|
| 61 |
-
|
| 62 |
-
# Resize image2 to match height of image1
|
| 63 |
-
new_width2 = int(width2 * (height1 / height2))
|
| 64 |
-
pil_image2 = pil_image2.resize((new_width2, height1), Image.Resampling.LANCZOS)
|
| 65 |
-
else:
|
| 66 |
-
# Create white image with same dimensions as image1
|
| 67 |
-
pil_image2 = Image.new('RGB', (width1, height1), 'white')
|
| 68 |
-
new_width2 = width1
|
| 69 |
-
|
| 70 |
-
# Create new image to hold both images side by side
|
| 71 |
-
combined_image = Image.new('RGB', (width1 + new_width2, height1))
|
| 72 |
-
|
| 73 |
-
# Paste both images
|
| 74 |
-
combined_image.paste(pil_image1, (0, 0))
|
| 75 |
-
combined_image.paste(pil_image2, (width1, 0))
|
| 76 |
-
|
| 77 |
-
# Convert combined image to tensor
|
| 78 |
-
combined_tensor = pil2tensor(combined_image)
|
| 79 |
-
processed_images.append(combined_tensor)
|
| 80 |
-
|
| 81 |
-
# Create mask (0 for left image area, 1 for right image area)
|
| 82 |
-
final_mask = torch.zeros((1, height1, width1 + new_width2))
|
| 83 |
-
final_mask[:, :, width1:] = 1.0 # Set right half to 1
|
| 84 |
-
|
| 85 |
-
# If mask is provided, subtract it from the right side
|
| 86 |
-
if mask is not None and idx < len(mask):
|
| 87 |
-
input_mask = mask[idx]
|
| 88 |
-
# Resize input mask to match height1
|
| 89 |
-
pil_input_mask = tensor2pil(input_mask)
|
| 90 |
-
pil_input_mask = pil_input_mask.resize((new_width2, height1), Image.Resampling.LANCZOS)
|
| 91 |
-
resized_input_mask = pil2tensor(pil_input_mask)
|
| 92 |
-
|
| 93 |
-
# Subtract input mask from the right side
|
| 94 |
-
final_mask[:, :, width1:] *= (1.0 - resized_input_mask)
|
| 95 |
-
|
| 96 |
-
masks.append(final_mask)
|
| 97 |
-
|
| 98 |
-
processed_images = torch.cat(processed_images, dim=0)
|
| 99 |
-
masks = torch.cat(masks, dim=0)
|
| 100 |
-
|
| 101 |
-
return (processed_images, masks)
|
| 102 |
-
|
| 103 |
-
NODE_CLASS_MAPPINGS = {
|
| 104 |
-
"image concat mask": image_concat_mask
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 108 |
-
"image concat mask": "Image Concat with Mask"
|
| 109 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/nodes/multiline_string.py
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
This node provides a multiline input field for a string.
|
| 3 |
-
Input:
|
| 4 |
-
string
|
| 5 |
-
Output:
|
| 6 |
-
STRING: A copy of 'string'.
|
| 7 |
-
"""
|
| 8 |
-
class MultilineString:
|
| 9 |
-
def __init__(self):
|
| 10 |
-
pass
|
| 11 |
-
|
| 12 |
-
@classmethod
|
| 13 |
-
def INPUT_TYPES(s):
|
| 14 |
-
return {
|
| 15 |
-
"required": {
|
| 16 |
-
"string": ("STRING", {
|
| 17 |
-
"multiline": True,
|
| 18 |
-
"default": "",
|
| 19 |
-
}),
|
| 20 |
-
},
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
RETURN_TYPES = ("STRING",)
|
| 24 |
-
RETURN_NAMES = ("STRING",)
|
| 25 |
-
FUNCTION = "get_string"
|
| 26 |
-
OUTPUT_NODE = True
|
| 27 |
-
CATEGORY = "🌻 Addoor/text"
|
| 28 |
-
|
| 29 |
-
def get_string(self, string):
|
| 30 |
-
return (string,)
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
# A dictionary that contains all nodes you want to export with their names
|
| 34 |
-
# NOTE: names should be globally unique
|
| 35 |
-
NODE_CLASS_MAPPINGS = {
|
| 36 |
-
"YANC.MultilineString": MultilineString,
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
# A dictionary that contains the friendly/humanly readable titles for the nodes
|
| 40 |
-
NODE_DISPLAY_NAME_MAPPINGS = {
|
| 41 |
-
"YANC.MultilineString": "Multiline String",
|
| 42 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/rp/RunningHubFileSaver.py
DELETED
|
@@ -1,183 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import requests
|
| 3 |
-
import json
|
| 4 |
-
import numpy as np
|
| 5 |
-
from PIL import Image
|
| 6 |
-
from PIL.PngImagePlugin import PngInfo
|
| 7 |
-
import subprocess
|
| 8 |
-
import mimetypes
|
| 9 |
-
import torch
|
| 10 |
-
import logging
|
| 11 |
-
import io
|
| 12 |
-
import re
|
| 13 |
-
|
| 14 |
-
logging.basicConfig(level=logging.DEBUG)
|
| 15 |
-
logger = logging.getLogger(__name__)
|
| 16 |
-
|
| 17 |
-
class RunningHubFileSaverNode:
|
| 18 |
-
def __init__(self):
|
| 19 |
-
self.compression = 4
|
| 20 |
-
|
| 21 |
-
@classmethod
|
| 22 |
-
def INPUT_TYPES(cls):
|
| 23 |
-
return {
|
| 24 |
-
"required": {
|
| 25 |
-
"file_urls": ("FILEURL_LIST",),
|
| 26 |
-
"directory": ("STRING", {"default": "outputs"}),
|
| 27 |
-
"filename_prefix": ("STRING", {"default": "RH_Output"}),
|
| 28 |
-
"filename_delimiter": ("STRING", {"default": "_"}),
|
| 29 |
-
"filename_number_padding": ("INT", {"default": 4, "min": 0, "max": 9, "step": 1}),
|
| 30 |
-
"file_extension": (["auto", "png", "jpg", "jpeg", "txt", "json", "mp4"], {"default": "auto"}),
|
| 31 |
-
"open_output_directory": ("BOOLEAN", {"default": False}),
|
| 32 |
-
},
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
RETURN_TYPES = ("STRING", "IMAGE", "STRING")
|
| 36 |
-
RETURN_NAMES = ("output_paths", "previews", "file_types")
|
| 37 |
-
FUNCTION = "save_files"
|
| 38 |
-
CATEGORY = "🌻 Addoor/RHAPI"
|
| 39 |
-
|
| 40 |
-
def save_files(self, file_urls, directory, filename_prefix, filename_delimiter, filename_number_padding, file_extension, open_output_directory):
|
| 41 |
-
output_paths = []
|
| 42 |
-
previews = []
|
| 43 |
-
file_types = []
|
| 44 |
-
|
| 45 |
-
try:
|
| 46 |
-
if not os.path.exists(directory):
|
| 47 |
-
os.makedirs(directory)
|
| 48 |
-
|
| 49 |
-
for url in file_urls:
|
| 50 |
-
if not url:
|
| 51 |
-
continue
|
| 52 |
-
|
| 53 |
-
result = self.save_single_file(
|
| 54 |
-
url,
|
| 55 |
-
directory,
|
| 56 |
-
filename_prefix,
|
| 57 |
-
filename_delimiter,
|
| 58 |
-
filename_number_padding,
|
| 59 |
-
file_extension
|
| 60 |
-
)
|
| 61 |
-
|
| 62 |
-
if result:
|
| 63 |
-
path, preview, ftype = result
|
| 64 |
-
output_paths.append(path)
|
| 65 |
-
previews.append(preview)
|
| 66 |
-
file_types.append(ftype)
|
| 67 |
-
|
| 68 |
-
if open_output_directory and output_paths:
|
| 69 |
-
self.open_directory(directory)
|
| 70 |
-
|
| 71 |
-
if not output_paths:
|
| 72 |
-
return ("", torch.zeros((1, 1, 1, 3)), "")
|
| 73 |
-
|
| 74 |
-
if len(previews) > 1:
|
| 75 |
-
combined_preview = torch.cat(previews, dim=0)
|
| 76 |
-
else:
|
| 77 |
-
combined_preview = previews[0] if previews else torch.zeros((1, 1, 1, 3))
|
| 78 |
-
|
| 79 |
-
return (
|
| 80 |
-
json.dumps(output_paths),
|
| 81 |
-
combined_preview,
|
| 82 |
-
json.dumps(file_types)
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
except Exception as e:
|
| 86 |
-
logger.error(f"Error saving files: {str(e)}")
|
| 87 |
-
return ("", torch.zeros((1, 1, 1, 3)), "")
|
| 88 |
-
|
| 89 |
-
def save_single_file(self, file_url, directory, filename_prefix, filename_delimiter, filename_number_padding, file_extension):
|
| 90 |
-
try:
|
| 91 |
-
logger.info(f"Attempting to save file from URL: {file_url}")
|
| 92 |
-
|
| 93 |
-
if not file_url:
|
| 94 |
-
raise ValueError("File URL is empty")
|
| 95 |
-
|
| 96 |
-
if not os.path.exists(directory):
|
| 97 |
-
logger.info(f"Creating directory: {directory}")
|
| 98 |
-
os.makedirs(directory)
|
| 99 |
-
|
| 100 |
-
response = requests.get(file_url, timeout=30)
|
| 101 |
-
response.raise_for_status()
|
| 102 |
-
|
| 103 |
-
content_type = response.headers.get('content-type')
|
| 104 |
-
logger.info(f"Content-Type: {content_type}")
|
| 105 |
-
|
| 106 |
-
if file_extension == "auto":
|
| 107 |
-
file_extension = mimetypes.guess_extension(content_type)
|
| 108 |
-
if file_extension:
|
| 109 |
-
file_extension = file_extension[1:] # Remove the leading dot
|
| 110 |
-
if file_extension == "jpeg":
|
| 111 |
-
file_extension = "jpg"
|
| 112 |
-
else:
|
| 113 |
-
file_extension = "bin" # Default to binary if can't guess
|
| 114 |
-
logger.info(f"Auto-detected file extension: {file_extension}")
|
| 115 |
-
|
| 116 |
-
file_path = os.path.join(directory, self.generate_filename(directory, filename_prefix, filename_delimiter, filename_number_padding, file_extension))
|
| 117 |
-
logger.info(f"Saving file to: {file_path}")
|
| 118 |
-
|
| 119 |
-
preview = None
|
| 120 |
-
if file_extension in ['png', 'jpg', 'jpeg']:
|
| 121 |
-
try:
|
| 122 |
-
img_data = io.BytesIO(response.content)
|
| 123 |
-
img = Image.open(img_data) # Reopen the image after verifying
|
| 124 |
-
metadata = PngInfo()
|
| 125 |
-
metadata.add_text("source_url", file_url)
|
| 126 |
-
img.save(file_path, pnginfo=metadata, compress_level=self.compression)
|
| 127 |
-
preview = torch.from_numpy(np.array(img).astype(np.float32) / 255.0).unsqueeze(0)
|
| 128 |
-
except Exception as e:
|
| 129 |
-
logger.error(f"Error processing image: {str(e)}")
|
| 130 |
-
# If image processing fails, save as binary
|
| 131 |
-
with open(file_path, 'wb') as f:
|
| 132 |
-
f.write(response.content)
|
| 133 |
-
preview = torch.zeros((1, 1, 1, 3)) # Placeholder for failed image
|
| 134 |
-
elif file_extension in ['txt', 'json']:
|
| 135 |
-
with open(file_path, 'w', encoding='utf-8') as f:
|
| 136 |
-
f.write(response.text)
|
| 137 |
-
preview = torch.zeros((1, 1, 1, 3)) # Placeholder for non-image files
|
| 138 |
-
else:
|
| 139 |
-
with open(file_path, 'wb') as f:
|
| 140 |
-
f.write(response.content)
|
| 141 |
-
preview = torch.zeros((1, 1, 1, 3)) # Placeholder for other file types
|
| 142 |
-
|
| 143 |
-
logger.info(f"File saved successfully to {file_path}")
|
| 144 |
-
|
| 145 |
-
return (file_path, preview, file_extension)
|
| 146 |
-
|
| 147 |
-
except requests.exceptions.RequestException as e:
|
| 148 |
-
logger.error(f"Error downloading file: {str(e)}")
|
| 149 |
-
except IOError as e:
|
| 150 |
-
logger.error(f"Error saving file: {str(e)}")
|
| 151 |
-
except Exception as e:
|
| 152 |
-
logger.error(f"Unexpected error: {str(e)}")
|
| 153 |
-
|
| 154 |
-
return ("", torch.zeros((1, 1, 1, 3)), "")
|
| 155 |
-
|
| 156 |
-
def generate_filename(self, directory, prefix, delimiter, number_padding, extension):
|
| 157 |
-
if number_padding == 0:
|
| 158 |
-
return f"{prefix}.{extension}"
|
| 159 |
-
|
| 160 |
-
pattern = f"{re.escape(prefix)}{re.escape(delimiter)}(\\d{{{number_padding}}})"
|
| 161 |
-
existing_files = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f))]
|
| 162 |
-
existing_counters = [
|
| 163 |
-
int(re.search(pattern, filename).group(1))
|
| 164 |
-
for filename in existing_files
|
| 165 |
-
if re.match(pattern, filename)
|
| 166 |
-
]
|
| 167 |
-
|
| 168 |
-
counter = max(existing_counters, default=0) + 1
|
| 169 |
-
return f"{prefix}{delimiter}{counter:0{number_padding}}.{extension}"
|
| 170 |
-
|
| 171 |
-
@staticmethod
|
| 172 |
-
def open_directory(path):
|
| 173 |
-
if os.name == 'nt': # Windows
|
| 174 |
-
os.startfile(path)
|
| 175 |
-
elif os.name == 'posix': # macOS and Linux
|
| 176 |
-
try:
|
| 177 |
-
subprocess.call(['open', path]) # macOS
|
| 178 |
-
except:
|
| 179 |
-
try:
|
| 180 |
-
subprocess.call(['xdg-open', path]) # Linux
|
| 181 |
-
except:
|
| 182 |
-
logger.error(f"Could not open directory: {path}")
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/rp/RunningHubInit.py
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 1 |
-
from . import validate_api_key
|
| 2 |
-
|
| 3 |
-
class RunningHubInitNode:
|
| 4 |
-
"""Node for initializing RunningHub API connection"""
|
| 5 |
-
|
| 6 |
-
@classmethod
|
| 7 |
-
def INPUT_TYPES(cls):
|
| 8 |
-
return {
|
| 9 |
-
"required": {
|
| 10 |
-
"api_key": ("STRING", {"default": "", "multiline": False}),
|
| 11 |
-
}
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
RETURN_TYPES = ("STRING",)
|
| 15 |
-
FUNCTION = "initialize"
|
| 16 |
-
CATEGORY = "🌻 Addoor/RHAPI"
|
| 17 |
-
|
| 18 |
-
def initialize(self, api_key: str):
|
| 19 |
-
if not validate_api_key(api_key):
|
| 20 |
-
raise ValueError("Invalid API key format")
|
| 21 |
-
return (api_key,)
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/rp/RunningHubNodeInfo.py
DELETED
|
@@ -1,45 +0,0 @@
|
|
| 1 |
-
class RunningHubNodeInfoNode:
|
| 2 |
-
"""Node for creating and chaining nodeInfo entries"""
|
| 3 |
-
|
| 4 |
-
@classmethod
|
| 5 |
-
def INPUT_TYPES(cls):
|
| 6 |
-
return {
|
| 7 |
-
"required": {
|
| 8 |
-
"node_id": ("STRING", {"default": ""}),
|
| 9 |
-
"field_name": ("STRING", {"default": ""}),
|
| 10 |
-
"field_value": ("STRING", {"default": "", "multiline": True}),
|
| 11 |
-
"comment": ("STRING", {"default": ""}),
|
| 12 |
-
},
|
| 13 |
-
"optional": {
|
| 14 |
-
"previous_node_info": ("NODEINFOLIST",), # 用于串联
|
| 15 |
-
}
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
RETURN_TYPES = ("NODEINFOLIST",) # 改为返回列表类型
|
| 19 |
-
FUNCTION = "create_node_info"
|
| 20 |
-
CATEGORY = "🌻 Addoor/RHAPI"
|
| 21 |
-
|
| 22 |
-
def create_node_info(self, node_id: str, field_name: str, field_value: str, comment: str, previous_node_info=None):
|
| 23 |
-
# 创建当前节点信息
|
| 24 |
-
current_node_info = {
|
| 25 |
-
"nodeId": node_id,
|
| 26 |
-
"fieldName": field_name,
|
| 27 |
-
"fieldValue": field_value,
|
| 28 |
-
"comment": comment
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
# 初始化节点信息列表
|
| 32 |
-
node_info_list = []
|
| 33 |
-
|
| 34 |
-
# 如果有前置节点信息,添加到列表
|
| 35 |
-
if previous_node_info is not None:
|
| 36 |
-
if isinstance(previous_node_info, list):
|
| 37 |
-
node_info_list.extend(previous_node_info)
|
| 38 |
-
else:
|
| 39 |
-
node_info_list.append(previous_node_info)
|
| 40 |
-
|
| 41 |
-
# 添加当前节点信息
|
| 42 |
-
node_info_list.append(current_node_info)
|
| 43 |
-
|
| 44 |
-
return (node_info_list,)
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/rp/RunningHubWorkflowExecutor.py
DELETED
|
@@ -1,151 +0,0 @@
|
|
| 1 |
-
import requests
|
| 2 |
-
import json
|
| 3 |
-
import time
|
| 4 |
-
import logging
|
| 5 |
-
from . import validate_api_key, BASE_URL
|
| 6 |
-
|
| 7 |
-
logging.basicConfig(level=logging.INFO)
|
| 8 |
-
logger = logging.getLogger(__name__)
|
| 9 |
-
|
| 10 |
-
class RunningHubWorkflowExecutorNode:
|
| 11 |
-
"""Node for executing RunningHub workflows and monitoring task status"""
|
| 12 |
-
|
| 13 |
-
@classmethod
|
| 14 |
-
def INPUT_TYPES(cls):
|
| 15 |
-
return {
|
| 16 |
-
"required": {
|
| 17 |
-
"api_key": ("STRING", {"default": ""}),
|
| 18 |
-
"workflow_id": ("STRING", {"default": ""}),
|
| 19 |
-
"node_info_list": ("NODEINFOLIST",),
|
| 20 |
-
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
| 21 |
-
"max_attempts": ("INT", {"default": 60, "min": 1, "max": 1000}),
|
| 22 |
-
"interval_seconds": ("FLOAT", {"default": 5.0, "min": 1.0, "max": 60.0}),
|
| 23 |
-
}
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
RETURN_TYPES = ("FILEURL_LIST", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING", "STRING")
|
| 27 |
-
RETURN_NAMES = ("file_urls", "task_id", "msg", "promptTips", "taskStatus", "fileType", "code", "json")
|
| 28 |
-
FUNCTION = "execute_workflow_and_monitor"
|
| 29 |
-
CATEGORY = "🌻 Addoor/RHAPI"
|
| 30 |
-
|
| 31 |
-
def execute_workflow_and_monitor(self, api_key: str, workflow_id: str, node_info_list: list, seed: int, max_attempts: int, interval_seconds: float):
|
| 32 |
-
if not validate_api_key(api_key):
|
| 33 |
-
raise ValueError("Invalid API key")
|
| 34 |
-
|
| 35 |
-
# 确保 node_info_list 是列表类型
|
| 36 |
-
if not isinstance(node_info_list, list):
|
| 37 |
-
node_info_list = [node_info_list]
|
| 38 |
-
|
| 39 |
-
# 移除可能的重复项
|
| 40 |
-
unique_node_info = {
|
| 41 |
-
(info.get('nodeId'), info.get('fieldName')): info
|
| 42 |
-
for info in node_info_list
|
| 43 |
-
}.values()
|
| 44 |
-
|
| 45 |
-
workflow_data = {
|
| 46 |
-
"workflowId": int(workflow_id),
|
| 47 |
-
"apiKey": api_key,
|
| 48 |
-
"nodeInfoList": list(unique_node_info),
|
| 49 |
-
"seed": seed
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
headers = {
|
| 53 |
-
"Authorization": f"Bearer {api_key}",
|
| 54 |
-
"Content-Type": "application/json",
|
| 55 |
-
"User-Agent": "Apifox/1.0.0 (https://apifox.com)",
|
| 56 |
-
"Accept": "*/*",
|
| 57 |
-
"Host": "www.runninghub.cn",
|
| 58 |
-
"Connection": "keep-alive"
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
workflow_endpoint = f"{BASE_URL}/task/openapi/create"
|
| 62 |
-
|
| 63 |
-
try:
|
| 64 |
-
logger.info(f"Executing workflow with ID: {workflow_id}")
|
| 65 |
-
workflow_response = requests.post(
|
| 66 |
-
workflow_endpoint,
|
| 67 |
-
headers=headers,
|
| 68 |
-
json=workflow_data
|
| 69 |
-
)
|
| 70 |
-
workflow_response.raise_for_status()
|
| 71 |
-
workflow_result = workflow_response.json()
|
| 72 |
-
|
| 73 |
-
if not workflow_result or 'data' not in workflow_result:
|
| 74 |
-
raise ValueError("Invalid response from workflow execution")
|
| 75 |
-
|
| 76 |
-
task_id = workflow_result.get('data', {}).get('taskId')
|
| 77 |
-
if not task_id:
|
| 78 |
-
raise ValueError("No task ID returned from workflow execution")
|
| 79 |
-
|
| 80 |
-
logger.info(f"Workflow executed. Task ID: {task_id}")
|
| 81 |
-
|
| 82 |
-
# Monitor task status
|
| 83 |
-
task_data = {
|
| 84 |
-
"taskId": task_id,
|
| 85 |
-
"apiKey": api_key
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
task_endpoint = f"{BASE_URL}/task/openapi/outputs"
|
| 89 |
-
|
| 90 |
-
for attempt in range(max_attempts):
|
| 91 |
-
try:
|
| 92 |
-
logger.info(f"Checking task status. Attempt {attempt + 1}/{max_attempts}")
|
| 93 |
-
task_response = requests.post(
|
| 94 |
-
task_endpoint,
|
| 95 |
-
headers=headers,
|
| 96 |
-
json=task_data
|
| 97 |
-
)
|
| 98 |
-
task_response.raise_for_status()
|
| 99 |
-
task_result = task_response.json()
|
| 100 |
-
|
| 101 |
-
if not task_result or 'data' not in task_result or not task_result['data']:
|
| 102 |
-
logger.warning("Invalid or empty response from task status check")
|
| 103 |
-
time.sleep(interval_seconds)
|
| 104 |
-
continue
|
| 105 |
-
|
| 106 |
-
task_status = task_result['data'][0].get('taskStatus', '')
|
| 107 |
-
|
| 108 |
-
if task_status in ['SUCCEDD', 'FAILED']:
|
| 109 |
-
logger.info(f"Task completed with status: {task_status}")
|
| 110 |
-
break
|
| 111 |
-
elif task_status in ['QUEUED', 'RUNNING']:
|
| 112 |
-
logger.info(f"Task status: {task_status}. Waiting...")
|
| 113 |
-
time.sleep(interval_seconds)
|
| 114 |
-
else:
|
| 115 |
-
logger.warning(f"Unknown task status: {task_status}")
|
| 116 |
-
break
|
| 117 |
-
|
| 118 |
-
except requests.exceptions.RequestException as e:
|
| 119 |
-
logger.error(f"Error checking task status: {str(e)}")
|
| 120 |
-
time.sleep(interval_seconds)
|
| 121 |
-
|
| 122 |
-
# Prepare return values
|
| 123 |
-
msg = workflow_result.get('msg', '')
|
| 124 |
-
prompt_tips = json.dumps(workflow_result.get('data', {}).get('promptTips', ''))
|
| 125 |
-
|
| 126 |
-
# 处理多个文件URL
|
| 127 |
-
file_urls = []
|
| 128 |
-
if task_result and 'data' in task_result and task_result['data']:
|
| 129 |
-
for item in task_result['data']:
|
| 130 |
-
if 'fileUrl' in item:
|
| 131 |
-
file_urls.append(item['fileUrl'])
|
| 132 |
-
|
| 133 |
-
# 如果没有文件URL,添加空字符串
|
| 134 |
-
if not file_urls:
|
| 135 |
-
file_urls = [""]
|
| 136 |
-
|
| 137 |
-
file_type = task_result['data'][0].get('fileType', '') if task_result and 'data' in task_result and task_result['data'] else ''
|
| 138 |
-
code = str(task_result.get('code', '')) if task_result else ''
|
| 139 |
-
|
| 140 |
-
return (file_urls, task_id, msg, prompt_tips, task_status, file_type, code, json.dumps(task_result))
|
| 141 |
-
|
| 142 |
-
except requests.exceptions.RequestException as e:
|
| 143 |
-
logger.error(f"API request failed: {str(e)}")
|
| 144 |
-
return ([""], "", f"Error: {str(e)}", "{}", "ERROR", "", "error", "")
|
| 145 |
-
except json.JSONDecodeError as e:
|
| 146 |
-
logger.error(f"Failed to parse JSON response: {str(e)}")
|
| 147 |
-
return ([""], "", f"Error: Invalid JSON response", "{}", "ERROR", "", "error", "")
|
| 148 |
-
except Exception as e:
|
| 149 |
-
logger.error(f"Unexpected error: {str(e)}")
|
| 150 |
-
return ([""], "", f"Error: {str(e)}", "{}", "ERROR", "", "error", "")
|
| 151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Addoor/rp/config.ini
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
; 密匙请联系微信:8617774(备注密匙)免费开通
|
| 2 |
-
|
| 3 |
-
[License]
|
| 4 |
-
COMFYUI_RHAPI_LICENSE_KEY = 1_Is-dRDrw1lbNVdN4vc2k7x4Qkg8hs2
|
| 5 |
-
COMFYUI_RHAPI_USER_ID = user123
|
| 6 |
-
COMFYUI_RHAPI_PHONE = 1234567890
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Copilot/backend/agent_factory.py
DELETED
|
@@ -1,98 +0,0 @@
|
|
| 1 |
-
'''
|
| 2 |
-
Author: ai-business-hql qingli.hql@alibaba-inc.com
|
| 3 |
-
Date: 2025-07-31 19:38:08
|
| 4 |
-
LastEditors: ai-business-hql ai.bussiness.hql@gmail.com
|
| 5 |
-
LastEditTime: 2025-11-20 17:51:33
|
| 6 |
-
FilePath: /comfyui_copilot/backend/agent_factory.py
|
| 7 |
-
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
| 8 |
-
'''
|
| 9 |
-
|
| 10 |
-
try:
|
| 11 |
-
from agents import Agent, OpenAIChatCompletionsModel, ModelSettings, Runner, set_trace_processors, set_tracing_disabled, set_default_openai_api
|
| 12 |
-
if not hasattr(__import__('agents'), 'Agent'):
|
| 13 |
-
raise ImportError
|
| 14 |
-
except Exception:
|
| 15 |
-
# Give actionable guidance without crashing obscurely
|
| 16 |
-
raise ImportError(
|
| 17 |
-
"Detected incorrect or missing 'agents' package. "
|
| 18 |
-
"Please uninstall legacy RL 'agents' (and tensorflow/gym if pulled transitively) and install openai-agents. "
|
| 19 |
-
"Commands:\n"
|
| 20 |
-
" python -m pip uninstall -y agents gym tensorflow\n"
|
| 21 |
-
" python -m pip install -U openai-agents\n\n"
|
| 22 |
-
"Alternatively, keep both by setting COMFYUI_COPILOT_PREFER_OPENAI_AGENTS=1 so this plugin prefers openai-agents."
|
| 23 |
-
)
|
| 24 |
-
from dotenv import dotenv_values
|
| 25 |
-
from .utils.globals import LLM_DEFAULT_BASE_URL, LMSTUDIO_DEFAULT_BASE_URL, get_comfyui_copilot_api_key, is_lmstudio_url
|
| 26 |
-
from openai import AsyncOpenAI
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
from agents._config import set_default_openai_api
|
| 30 |
-
from agents.tracing import set_tracing_disabled
|
| 31 |
-
import asyncio
|
| 32 |
-
# from .utils.logger import log
|
| 33 |
-
|
| 34 |
-
# def load_env_config():
|
| 35 |
-
# """Load environment variables from .env.llm file"""
|
| 36 |
-
# from dotenv import load_dotenv
|
| 37 |
-
|
| 38 |
-
# env_file_path = os.path.join(os.path.dirname(__file__), '.env.llm')
|
| 39 |
-
# if os.path.exists(env_file_path):
|
| 40 |
-
# load_dotenv(env_file_path)
|
| 41 |
-
# log.info(f"Loaded environment variables from {env_file_path}")
|
| 42 |
-
# else:
|
| 43 |
-
# log.warning(f"Warning: .env.llm not found at {env_file_path}")
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
# # Load environment configuration
|
| 47 |
-
# load_env_config()
|
| 48 |
-
|
| 49 |
-
set_default_openai_api("chat_completions")
|
| 50 |
-
set_tracing_disabled(False)
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def create_agent(**kwargs) -> Agent:
|
| 54 |
-
# 通过用户配置拿/环境变量
|
| 55 |
-
config = kwargs.pop("config") if "config" in kwargs else {}
|
| 56 |
-
# 避免将 None 写入 headers
|
| 57 |
-
session_id = (config or {}).get("session_id")
|
| 58 |
-
default_headers = {}
|
| 59 |
-
if session_id:
|
| 60 |
-
default_headers["X-Session-ID"] = session_id
|
| 61 |
-
|
| 62 |
-
# Determine base URL and API key
|
| 63 |
-
base_url = LLM_DEFAULT_BASE_URL
|
| 64 |
-
api_key = get_comfyui_copilot_api_key() or ""
|
| 65 |
-
|
| 66 |
-
if config:
|
| 67 |
-
if config.get("openai_base_url") and config.get("openai_base_url") != "":
|
| 68 |
-
base_url = config.get("openai_base_url")
|
| 69 |
-
if config.get("openai_api_key") and config.get("openai_api_key") != "":
|
| 70 |
-
api_key = config.get("openai_api_key")
|
| 71 |
-
|
| 72 |
-
# Check if this is LMStudio and adjust API key handling
|
| 73 |
-
is_lmstudio = is_lmstudio_url(base_url)
|
| 74 |
-
if is_lmstudio and not api_key:
|
| 75 |
-
# LMStudio typically doesn't require an API key, use a placeholder
|
| 76 |
-
api_key = "lmstudio-local"
|
| 77 |
-
|
| 78 |
-
client = AsyncOpenAI(
|
| 79 |
-
api_key=api_key,
|
| 80 |
-
base_url=base_url,
|
| 81 |
-
default_headers=default_headers,
|
| 82 |
-
)
|
| 83 |
-
|
| 84 |
-
# Determine model with proper precedence:
|
| 85 |
-
# 1) Explicit selection from config (model_select from frontend)
|
| 86 |
-
# 2) Explicit kwarg 'model' (call-site override)
|
| 87 |
-
model_from_config = (config or {}).get("model_select")
|
| 88 |
-
model_from_kwargs = kwargs.pop("model", None)
|
| 89 |
-
|
| 90 |
-
model_name = model_from_kwargs or model_from_config or "gemini-2.5-flash"
|
| 91 |
-
model = OpenAIChatCompletionsModel(model_name, openai_client=client)
|
| 92 |
-
|
| 93 |
-
# Safety: ensure no stray 'model' remains in kwargs to avoid duplicate kwarg errors
|
| 94 |
-
kwargs.pop("model", None)
|
| 95 |
-
|
| 96 |
-
if config.get("max_tokens"):
|
| 97 |
-
return Agent(model=model, model_settings=ModelSettings(max_tokens=config.get("max_tokens") or 8192), **kwargs)
|
| 98 |
-
return Agent(model=model, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Copilot/backend/controller/conversation_api.py
DELETED
|
@@ -1,1083 +0,0 @@
|
|
| 1 |
-
# Copyright (C) 2025 AIDC-AI
|
| 2 |
-
# Licensed under the MIT License.
|
| 3 |
-
|
| 4 |
-
import json
|
| 5 |
-
import asyncio
|
| 6 |
-
import time
|
| 7 |
-
from typing import Optional, TypedDict, List, Union
|
| 8 |
-
import threading
|
| 9 |
-
from collections import defaultdict
|
| 10 |
-
|
| 11 |
-
from sqlalchemy.orm import identity
|
| 12 |
-
|
| 13 |
-
from ..utils.globals import set_language, apply_llm_env_defaults
|
| 14 |
-
from ..utils.auth_utils import extract_and_store_api_key
|
| 15 |
-
import server
|
| 16 |
-
from aiohttp import web
|
| 17 |
-
import base64
|
| 18 |
-
|
| 19 |
-
# Import the MCP client function
|
| 20 |
-
import os
|
| 21 |
-
import shutil
|
| 22 |
-
|
| 23 |
-
from ..service.debug_agent import debug_workflow_errors
|
| 24 |
-
from ..dao.workflow_table import save_workflow_data, get_workflow_data_by_id, update_workflow_ui_by_id
|
| 25 |
-
from ..service.mcp_client import comfyui_agent_invoke
|
| 26 |
-
from ..utils.request_context import set_request_context, get_session_id
|
| 27 |
-
from ..utils.logger import log
|
| 28 |
-
from ..utils.modelscope_gateway import ModelScopeGateway
|
| 29 |
-
import folder_paths
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
def get_llm_config_from_headers(request):
|
| 33 |
-
"""Extract LLM-related configuration from request headers."""
|
| 34 |
-
return {
|
| 35 |
-
"openai_api_key": request.headers.get('Openai-Api-Key'),
|
| 36 |
-
"openai_base_url": request.headers.get('Openai-Base-Url'),
|
| 37 |
-
# Workflow LLM settings (optional, used by tools/agents that need a different LLM)
|
| 38 |
-
"workflow_llm_api_key": request.headers.get('Workflow-LLM-Api-Key'),
|
| 39 |
-
"workflow_llm_base_url": request.headers.get('Workflow-LLM-Base-Url'),
|
| 40 |
-
"workflow_llm_model": request.headers.get('Workflow-LLM-Model'),
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
# 全局下载进度存储
|
| 45 |
-
download_progress = {}
|
| 46 |
-
download_lock = threading.Lock()
|
| 47 |
-
|
| 48 |
-
# 不再使用内存存储会话消息,改为从前端传递历史消息
|
| 49 |
-
|
| 50 |
-
# 在文件开头添加
|
| 51 |
-
STATIC_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "public")
|
| 52 |
-
|
| 53 |
-
# Define types using TypedDict
|
| 54 |
-
class Node(TypedDict):
|
| 55 |
-
name: str # 节点名称
|
| 56 |
-
description: str # 节点描述
|
| 57 |
-
image: str # 节点图片url,可为空
|
| 58 |
-
github_url: str # 节点github地址
|
| 59 |
-
from_index: int # 节点在列表中的位置
|
| 60 |
-
to_index: int # 节点在列表中的位置
|
| 61 |
-
|
| 62 |
-
class NodeInfo(TypedDict):
|
| 63 |
-
existing_nodes: List[Node] # 已安装的节点
|
| 64 |
-
missing_nodes: List[Node] # 未安装的节点
|
| 65 |
-
|
| 66 |
-
class Workflow(TypedDict, total=False):
|
| 67 |
-
id: Optional[int] # 工作流id
|
| 68 |
-
name: Optional[str] # 工作流名称
|
| 69 |
-
description: Optional[str] # 工作流描述
|
| 70 |
-
image: Optional[str] # 工作流图片
|
| 71 |
-
workflow: Optional[str] # 工作流
|
| 72 |
-
|
| 73 |
-
class ExtItem(TypedDict):
|
| 74 |
-
type: str # 扩展类型
|
| 75 |
-
data: Union[dict, list] # 扩展数据
|
| 76 |
-
|
| 77 |
-
class ChatResponse(TypedDict):
|
| 78 |
-
session_id: str # 会话id
|
| 79 |
-
text: Optional[str] # 返回文本
|
| 80 |
-
finished: bool # 是否结束
|
| 81 |
-
type: str # 返回的类型
|
| 82 |
-
format: str # 返回的格式
|
| 83 |
-
ext: Optional[List[ExtItem]] # 扩展信息
|
| 84 |
-
|
| 85 |
-
# 下载进度回调类
|
| 86 |
-
class DownloadProgressCallback:
|
| 87 |
-
def __init__(self, id: str, filename: str, file_size: int, download_id: str):
|
| 88 |
-
self.id = id
|
| 89 |
-
self.filename = filename
|
| 90 |
-
self.file_size = file_size
|
| 91 |
-
self.download_id = download_id
|
| 92 |
-
self.progress = 0
|
| 93 |
-
self.status = "downloading" # downloading, completed, failed
|
| 94 |
-
self.error_message = None
|
| 95 |
-
self.start_time = time.time()
|
| 96 |
-
|
| 97 |
-
# 初始化进度记录
|
| 98 |
-
with download_lock:
|
| 99 |
-
download_progress[download_id] = {
|
| 100 |
-
"id": id,
|
| 101 |
-
"filename": filename,
|
| 102 |
-
"file_size": file_size,
|
| 103 |
-
"progress": 0,
|
| 104 |
-
"percentage": 0.0,
|
| 105 |
-
"status": "downloading",
|
| 106 |
-
"start_time": self.start_time,
|
| 107 |
-
"estimated_time": None,
|
| 108 |
-
"speed": 0.0,
|
| 109 |
-
"error_message": None
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
def update(self, size: int):
|
| 113 |
-
"""更新下载进度"""
|
| 114 |
-
self.progress += size
|
| 115 |
-
current_time = time.time()
|
| 116 |
-
elapsed_time = current_time - self.start_time
|
| 117 |
-
|
| 118 |
-
# 计算下载速度和预估时间
|
| 119 |
-
if elapsed_time > 0:
|
| 120 |
-
speed = self.progress / elapsed_time # bytes per second
|
| 121 |
-
if speed > 0 and self.progress < self.file_size:
|
| 122 |
-
remaining_bytes = self.file_size - self.progress
|
| 123 |
-
estimated_time = remaining_bytes / speed
|
| 124 |
-
else:
|
| 125 |
-
estimated_time = None
|
| 126 |
-
else:
|
| 127 |
-
speed = 0.0
|
| 128 |
-
estimated_time = None
|
| 129 |
-
|
| 130 |
-
percentage = (self.progress / self.file_size) * 100 if self.file_size > 0 else 0
|
| 131 |
-
|
| 132 |
-
# 更新全局进度
|
| 133 |
-
with download_lock:
|
| 134 |
-
if self.download_id in download_progress:
|
| 135 |
-
# 直接更新字典的值,而不是调用update方法
|
| 136 |
-
progress_dict = download_progress[self.download_id]
|
| 137 |
-
progress_dict["progress"] = self.progress
|
| 138 |
-
progress_dict["percentage"] = round(percentage, 2)
|
| 139 |
-
progress_dict["speed"] = round(speed, 2)
|
| 140 |
-
progress_dict["estimated_time"] = round(estimated_time, 2) if estimated_time else None
|
| 141 |
-
|
| 142 |
-
def end(self, success: bool = True, error_message: str = None):
|
| 143 |
-
"""下载结束回调"""
|
| 144 |
-
current_time = time.time()
|
| 145 |
-
total_time = current_time - self.start_time
|
| 146 |
-
|
| 147 |
-
if success:
|
| 148 |
-
self.status = "completed"
|
| 149 |
-
# 验证下载完整性
|
| 150 |
-
assert self.progress == self.file_size, f"Download incomplete: {self.progress}/{self.file_size}"
|
| 151 |
-
else:
|
| 152 |
-
self.status = "failed"
|
| 153 |
-
self.error_message = error_message
|
| 154 |
-
|
| 155 |
-
# 更新最终状态
|
| 156 |
-
with download_lock:
|
| 157 |
-
if self.download_id in download_progress:
|
| 158 |
-
# 直接更新字典的值,而不是调用update方法
|
| 159 |
-
progress_dict = download_progress[self.download_id]
|
| 160 |
-
progress_dict["status"] = self.status
|
| 161 |
-
progress_dict["progress"] = self.file_size if success else self.progress
|
| 162 |
-
if self.file_size > 0:
|
| 163 |
-
progress_dict["percentage"] = 100.0 if success else (self.progress / self.file_size) * 100
|
| 164 |
-
else:
|
| 165 |
-
progress_dict["percentage"] = 0.0
|
| 166 |
-
progress_dict["total_time"] = round(total_time, 2)
|
| 167 |
-
progress_dict["error_message"] = self.error_message
|
| 168 |
-
|
| 169 |
-
def fail(self, error_message: str):
|
| 170 |
-
"""下载失败回调"""
|
| 171 |
-
self.end(success=False, error_message=error_message)
|
| 172 |
-
|
| 173 |
-
# 生成唯一下载ID
|
| 174 |
-
def generate_download_id() -> str:
|
| 175 |
-
import uuid
|
| 176 |
-
return str(uuid.uuid4())
|
| 177 |
-
|
| 178 |
-
async def upload_to_oss(file_data: bytes, filename: str) -> str:
|
| 179 |
-
# TODO: Implement your OSS upload logic here
|
| 180 |
-
# For now, save locally and return a placeholder URL
|
| 181 |
-
|
| 182 |
-
try:
|
| 183 |
-
# Create uploads directory if it doesn't exist
|
| 184 |
-
uploads_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'uploads')
|
| 185 |
-
os.makedirs(uploads_dir, exist_ok=True)
|
| 186 |
-
|
| 187 |
-
# Generate unique filename to avoid conflicts
|
| 188 |
-
import uuid
|
| 189 |
-
unique_filename = f"{uuid.uuid4().hex}_{filename}"
|
| 190 |
-
file_path = os.path.join(uploads_dir, unique_filename)
|
| 191 |
-
|
| 192 |
-
# Save file locally
|
| 193 |
-
with open(file_path, 'wb') as f:
|
| 194 |
-
f.write(file_data)
|
| 195 |
-
|
| 196 |
-
# Return a local URL or base64 data URL for now
|
| 197 |
-
# In production, replace this with actual OSS URL
|
| 198 |
-
base64_data = base64.b64encode(file_data).decode('utf-8')
|
| 199 |
-
# Determine MIME type based on file extension
|
| 200 |
-
if filename.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp')):
|
| 201 |
-
mime_type = f"image/{filename.split('.')[-1].lower()}"
|
| 202 |
-
if mime_type == "image/jpg":
|
| 203 |
-
mime_type = "image/jpeg"
|
| 204 |
-
else:
|
| 205 |
-
mime_type = "image/jpeg" # Default
|
| 206 |
-
|
| 207 |
-
return f"data:{mime_type};base64,{base64_data}"
|
| 208 |
-
|
| 209 |
-
except Exception as e:
|
| 210 |
-
log.error(f"Error uploading file {filename}: {str(e)}")
|
| 211 |
-
# Return original base64 data if upload fails
|
| 212 |
-
base64_data = base64.b64encode(file_data).decode('utf-8')
|
| 213 |
-
return f"data:image/jpeg;base64,{base64_data}"
|
| 214 |
-
|
| 215 |
-
# 关联用户消息和AI响应的checkpoint信息
|
| 216 |
-
def processMessagesWithCheckpoints(messages):
|
| 217 |
-
# ... existing code ...
|
| 218 |
-
pass
|
| 219 |
-
|
| 220 |
-
@server.PromptServer.instance.routes.post("/api/chat/invoke")
|
| 221 |
-
async def invoke_chat(request):
|
| 222 |
-
log.info("Received invoke_chat request")
|
| 223 |
-
|
| 224 |
-
# Extract and store API key from Authorization header
|
| 225 |
-
extract_and_store_api_key(request)
|
| 226 |
-
|
| 227 |
-
req_json = await request.json()
|
| 228 |
-
log.info("Request JSON:", req_json)
|
| 229 |
-
|
| 230 |
-
response = web.StreamResponse(
|
| 231 |
-
status=200,
|
| 232 |
-
reason='OK',
|
| 233 |
-
headers={
|
| 234 |
-
'Content-Type': 'application/json',
|
| 235 |
-
'X-Content-Type-Options': 'nosniff'
|
| 236 |
-
}
|
| 237 |
-
)
|
| 238 |
-
await response.prepare(request)
|
| 239 |
-
|
| 240 |
-
session_id = req_json.get('session_id')
|
| 241 |
-
prompt = req_json.get('prompt')
|
| 242 |
-
images = req_json.get('images', [])
|
| 243 |
-
intent = req_json.get('intent')
|
| 244 |
-
ext = req_json.get('ext')
|
| 245 |
-
historical_messages = req_json.get('messages', [])
|
| 246 |
-
workflow_checkpoint_id = req_json.get('workflow_checkpoint_id')
|
| 247 |
-
|
| 248 |
-
# 获取当前语言
|
| 249 |
-
language = request.headers.get('Accept-Language', 'en')
|
| 250 |
-
set_language(language)
|
| 251 |
-
|
| 252 |
-
# 构建配置信息
|
| 253 |
-
config = {
|
| 254 |
-
"session_id": session_id,
|
| 255 |
-
"workflow_checkpoint_id": workflow_checkpoint_id,
|
| 256 |
-
**get_llm_config_from_headers(request),
|
| 257 |
-
"model_select": next((x['data'][0] for x in ext if x['type'] == 'model_select' and x.get('data')), None)
|
| 258 |
-
}
|
| 259 |
-
# Apply .env-based defaults for LLM-related fields (config > .env > code defaults)
|
| 260 |
-
config = apply_llm_env_defaults(config)
|
| 261 |
-
|
| 262 |
-
# 设置请求上下文 - 这里建立context隔离
|
| 263 |
-
set_request_context(session_id, workflow_checkpoint_id, config)
|
| 264 |
-
|
| 265 |
-
# 图片处理已移至前端,图片信息现在包含在历史消息的OpenAI格式中
|
| 266 |
-
# 保留空的处理逻辑以向后兼容,但实际不再使用
|
| 267 |
-
processed_images = []
|
| 268 |
-
if images and len(images) > 0:
|
| 269 |
-
log.info(f"Note: Received {len(images)} images in legacy format, but using OpenAI message format instead")
|
| 270 |
-
|
| 271 |
-
# 历史消息已经从前端传递过来,格式为OpenAI格式,直接使用
|
| 272 |
-
log.info(f"-- Received {len(historical_messages)} historical messages")
|
| 273 |
-
|
| 274 |
-
# Log workflow checkpoint ID if provided (workflow is now pre-saved before invoke)
|
| 275 |
-
if workflow_checkpoint_id:
|
| 276 |
-
log.info(f"Using workflow checkpoint ID: {workflow_checkpoint_id} for session {session_id}")
|
| 277 |
-
else:
|
| 278 |
-
log.info(f"No workflow checkpoint ID provided for session {session_id}")
|
| 279 |
-
|
| 280 |
-
# 历史消息已经从前端传递过来,包含了正确格式的OpenAI消息(包括图片)
|
| 281 |
-
# 直接使用前端传递的历史消息,无需重新构建当前消息
|
| 282 |
-
openai_messages = historical_messages
|
| 283 |
-
|
| 284 |
-
# 不再需要创建用户消息存储到后端,前端负责消息存储
|
| 285 |
-
|
| 286 |
-
try:
|
| 287 |
-
# Call the MCP client to get streaming response with historical messages and image support
|
| 288 |
-
# Pass OpenAI-formatted messages and processed images to comfyui_agent_invoke
|
| 289 |
-
accumulated_text = ""
|
| 290 |
-
ext_data = None
|
| 291 |
-
finished = True # Default to True
|
| 292 |
-
has_sent_response = False
|
| 293 |
-
previous_text_length = 0
|
| 294 |
-
|
| 295 |
-
log.info(f"config: {config}")
|
| 296 |
-
|
| 297 |
-
# Pass messages in OpenAI format (images are now included in messages)
|
| 298 |
-
# Config is now available through request context
|
| 299 |
-
async for result in comfyui_agent_invoke(openai_messages, None):
|
| 300 |
-
# The MCP client now returns tuples (text, ext_with_finished) where ext_with_finished includes finished status
|
| 301 |
-
if isinstance(result, tuple) and len(result) == 2:
|
| 302 |
-
text, ext_with_finished = result
|
| 303 |
-
if text:
|
| 304 |
-
accumulated_text = text # text from MCP is already accumulated
|
| 305 |
-
|
| 306 |
-
if ext_with_finished:
|
| 307 |
-
# Extract ext data and finished status from the structured response
|
| 308 |
-
ext_data = ext_with_finished.get("data")
|
| 309 |
-
finished = ext_with_finished.get("finished", True)
|
| 310 |
-
log.info(f"-- Received ext data: {ext_data}, finished: {finished}")
|
| 311 |
-
else:
|
| 312 |
-
# Handle single text chunk (backward compatibility)
|
| 313 |
-
text_chunk = result
|
| 314 |
-
if text_chunk:
|
| 315 |
-
accumulated_text += text_chunk
|
| 316 |
-
log.info(f"-- Received text chunk: '{text_chunk}', total length: {len(accumulated_text)}")
|
| 317 |
-
|
| 318 |
-
# Send streaming response if we have new text content
|
| 319 |
-
# Only send intermediate responses during streaming (not the final one)
|
| 320 |
-
if accumulated_text and len(accumulated_text) > previous_text_length:
|
| 321 |
-
chat_response = ChatResponse(
|
| 322 |
-
session_id=session_id,
|
| 323 |
-
text=accumulated_text,
|
| 324 |
-
finished=False, # Always false during streaming
|
| 325 |
-
type="message",
|
| 326 |
-
format="markdown",
|
| 327 |
-
ext=None # ext is only sent in final response
|
| 328 |
-
)
|
| 329 |
-
|
| 330 |
-
await response.write(json.dumps(chat_response).encode() + b"\n")
|
| 331 |
-
previous_text_length = len(accumulated_text)
|
| 332 |
-
await asyncio.sleep(0.01) # Small delay for streaming effect
|
| 333 |
-
|
| 334 |
-
# Send final response with proper finished logic from MCP client
|
| 335 |
-
log.info(f"-- Sending final response: {len(accumulated_text)} chars, ext: {bool(ext_data)}, finished: {finished}")
|
| 336 |
-
|
| 337 |
-
final_response = ChatResponse(
|
| 338 |
-
session_id=session_id,
|
| 339 |
-
text=accumulated_text,
|
| 340 |
-
finished=finished, # Use finished status from MCP client
|
| 341 |
-
type="message",
|
| 342 |
-
format="markdown",
|
| 343 |
-
ext=ext_data
|
| 344 |
-
)
|
| 345 |
-
|
| 346 |
-
await response.write(json.dumps(final_response).encode() + b"\n")
|
| 347 |
-
|
| 348 |
-
# AI响应不再存储到后端,前端负责消息存储
|
| 349 |
-
|
| 350 |
-
except Exception as e:
|
| 351 |
-
log.error(f"Error in invoke_chat: {str(e)}")
|
| 352 |
-
error_response = ChatResponse(
|
| 353 |
-
session_id=session_id,
|
| 354 |
-
text=f"I apologize, but an error occurred: {str(e)}",
|
| 355 |
-
finished=True, # Always finish on error
|
| 356 |
-
type="message",
|
| 357 |
-
format="text",
|
| 358 |
-
ext=None
|
| 359 |
-
)
|
| 360 |
-
await response.write(json.dumps(error_response).encode() + b"\n")
|
| 361 |
-
|
| 362 |
-
await response.write_eof()
|
| 363 |
-
return response
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
@server.PromptServer.instance.routes.post("/api/save-workflow-checkpoint")
|
| 367 |
-
async def save_workflow_checkpoint(request):
|
| 368 |
-
"""
|
| 369 |
-
Save workflow checkpoint for restore functionality
|
| 370 |
-
"""
|
| 371 |
-
log.info("Received save-workflow-checkpoint request")
|
| 372 |
-
req_json = await request.json()
|
| 373 |
-
|
| 374 |
-
try:
|
| 375 |
-
session_id = req_json.get('session_id')
|
| 376 |
-
workflow_api = req_json.get('workflow_api') # API format workflow
|
| 377 |
-
workflow_ui = req_json.get('workflow_ui') # UI format workflow
|
| 378 |
-
checkpoint_type = req_json.get('checkpoint_type', 'debug_start') # debug_start, debug_complete, user_message_checkpoint
|
| 379 |
-
message_id = req_json.get('message_id') # User message ID for linking (optional)
|
| 380 |
-
|
| 381 |
-
if not session_id or not workflow_api:
|
| 382 |
-
return web.json_response({
|
| 383 |
-
"success": False,
|
| 384 |
-
"message": "Missing required parameters: session_id and workflow_api"
|
| 385 |
-
})
|
| 386 |
-
|
| 387 |
-
# Save workflow with checkpoint type in attributes
|
| 388 |
-
attributes = {
|
| 389 |
-
"checkpoint_type": checkpoint_type,
|
| 390 |
-
"timestamp": time.time()
|
| 391 |
-
}
|
| 392 |
-
|
| 393 |
-
# Set description and additional attributes based on checkpoint type
|
| 394 |
-
if checkpoint_type == "user_message_checkpoint" and message_id:
|
| 395 |
-
attributes.update({
|
| 396 |
-
"description": f"Workflow checkpoint before user message {message_id}",
|
| 397 |
-
"message_id": message_id,
|
| 398 |
-
"source": "user_message_pre_invoke"
|
| 399 |
-
})
|
| 400 |
-
else:
|
| 401 |
-
attributes["description"] = f"Workflow checkpoint: {checkpoint_type}"
|
| 402 |
-
|
| 403 |
-
version_id = save_workflow_data(
|
| 404 |
-
session_id=session_id,
|
| 405 |
-
workflow_data=workflow_api,
|
| 406 |
-
workflow_data_ui=workflow_ui,
|
| 407 |
-
attributes=attributes
|
| 408 |
-
)
|
| 409 |
-
|
| 410 |
-
log.info(f"Workflow checkpoint saved with version ID: {version_id}")
|
| 411 |
-
|
| 412 |
-
# Return response format based on checkpoint type
|
| 413 |
-
response_data = {
|
| 414 |
-
"version_id": version_id,
|
| 415 |
-
"checkpoint_type": checkpoint_type
|
| 416 |
-
}
|
| 417 |
-
|
| 418 |
-
if checkpoint_type == "user_message_checkpoint" and message_id:
|
| 419 |
-
response_data.update({
|
| 420 |
-
"checkpoint_id": version_id, # Add checkpoint_id alias for user message checkpoints
|
| 421 |
-
"message_id": message_id
|
| 422 |
-
})
|
| 423 |
-
|
| 424 |
-
return web.json_response({
|
| 425 |
-
"success": True,
|
| 426 |
-
"data": response_data,
|
| 427 |
-
"message": f"Workflow checkpoint saved successfully"
|
| 428 |
-
})
|
| 429 |
-
|
| 430 |
-
except Exception as e:
|
| 431 |
-
log.error(f"Error saving workflow checkpoint: {str(e)}")
|
| 432 |
-
return web.json_response({
|
| 433 |
-
"success": False,
|
| 434 |
-
"message": f"Failed to save workflow checkpoint: {str(e)}"
|
| 435 |
-
})
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
@server.PromptServer.instance.routes.get("/api/restore-workflow-checkpoint")
|
| 440 |
-
async def restore_workflow_checkpoint(request):
|
| 441 |
-
"""
|
| 442 |
-
Restore workflow checkpoint by version ID
|
| 443 |
-
"""
|
| 444 |
-
log.info("Received restore-workflow-checkpoint request")
|
| 445 |
-
|
| 446 |
-
try:
|
| 447 |
-
version_id = request.query.get('version_id')
|
| 448 |
-
|
| 449 |
-
if not version_id:
|
| 450 |
-
return web.json_response({
|
| 451 |
-
"success": False,
|
| 452 |
-
"message": "Missing required parameter: version_id"
|
| 453 |
-
})
|
| 454 |
-
|
| 455 |
-
try:
|
| 456 |
-
version_id = int(version_id)
|
| 457 |
-
except ValueError:
|
| 458 |
-
return web.json_response({
|
| 459 |
-
"success": False,
|
| 460 |
-
"message": "Invalid version_id format"
|
| 461 |
-
})
|
| 462 |
-
|
| 463 |
-
# Get workflow data by version ID
|
| 464 |
-
workflow_version = get_workflow_data_by_id(version_id)
|
| 465 |
-
|
| 466 |
-
if not workflow_version:
|
| 467 |
-
return web.json_response({
|
| 468 |
-
"success": False,
|
| 469 |
-
"message": f"Workflow version {version_id} not found"
|
| 470 |
-
})
|
| 471 |
-
|
| 472 |
-
log.info(f"Restored workflow checkpoint version ID: {version_id}")
|
| 473 |
-
|
| 474 |
-
return web.json_response({
|
| 475 |
-
"success": True,
|
| 476 |
-
"data": {
|
| 477 |
-
"version_id": version_id,
|
| 478 |
-
"workflow_data": workflow_version.get('workflow_data'),
|
| 479 |
-
"workflow_data_ui": workflow_version.get('workflow_data_ui'),
|
| 480 |
-
"attributes": workflow_version.get('attributes'),
|
| 481 |
-
"created_at": workflow_version.get('created_at')
|
| 482 |
-
},
|
| 483 |
-
"message": f"Workflow checkpoint restored successfully"
|
| 484 |
-
})
|
| 485 |
-
|
| 486 |
-
except Exception as e:
|
| 487 |
-
log.error(f"Error restoring workflow checkpoint: {str(e)}")
|
| 488 |
-
return web.json_response({
|
| 489 |
-
"success": False,
|
| 490 |
-
"message": f"Failed to restore workflow checkpoint: {str(e)}"
|
| 491 |
-
})
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
@server.PromptServer.instance.routes.post("/api/debug-agent")
|
| 495 |
-
async def invoke_debug(request):
|
| 496 |
-
"""
|
| 497 |
-
Debug agent endpoint for analyzing ComfyUI workflow errors
|
| 498 |
-
"""
|
| 499 |
-
log.info("Received debug-agent request")
|
| 500 |
-
|
| 501 |
-
# Extract and store API key from Authorization header
|
| 502 |
-
extract_and_store_api_key(request)
|
| 503 |
-
|
| 504 |
-
req_json = await request.json()
|
| 505 |
-
|
| 506 |
-
response = web.StreamResponse(
|
| 507 |
-
status=200,
|
| 508 |
-
reason='OK',
|
| 509 |
-
headers={
|
| 510 |
-
'Content-Type': 'application/json',
|
| 511 |
-
'X-Content-Type-Options': 'nosniff'
|
| 512 |
-
}
|
| 513 |
-
)
|
| 514 |
-
await response.prepare(request)
|
| 515 |
-
|
| 516 |
-
session_id = req_json.get('session_id')
|
| 517 |
-
workflow_data = req_json.get('workflow_data')
|
| 518 |
-
|
| 519 |
-
# Get configuration from headers (OpenAI settings)
|
| 520 |
-
config = {
|
| 521 |
-
"session_id": session_id,
|
| 522 |
-
"model": "gemini-2.5-flash", # Default model for debug agents
|
| 523 |
-
**get_llm_config_from_headers(request),
|
| 524 |
-
}
|
| 525 |
-
# Apply .env-based defaults for LLM-related fields (config > .env > code defaults)
|
| 526 |
-
config = apply_llm_env_defaults(config)
|
| 527 |
-
|
| 528 |
-
# 获取当前语言
|
| 529 |
-
language = request.headers.get('Accept-Language', 'en')
|
| 530 |
-
set_language(language)
|
| 531 |
-
|
| 532 |
-
# 设置请求上下文 - 为debug请求建立context隔离
|
| 533 |
-
set_request_context(session_id, None, config)
|
| 534 |
-
|
| 535 |
-
log.info(f"Debug agent config: {config}")
|
| 536 |
-
log.info(f"Session ID: {session_id}")
|
| 537 |
-
log.info(f"Workflow nodes: {list(workflow_data.keys()) if workflow_data else 'None'}")
|
| 538 |
-
|
| 539 |
-
try:
|
| 540 |
-
# Call the debug agent with streaming response
|
| 541 |
-
accumulated_text = ""
|
| 542 |
-
final_ext_data = None
|
| 543 |
-
finished = False
|
| 544 |
-
|
| 545 |
-
async for result in debug_workflow_errors(workflow_data):
|
| 546 |
-
# Stream the response
|
| 547 |
-
if isinstance(result, tuple) and len(result) == 2:
|
| 548 |
-
text, ext = result
|
| 549 |
-
if text:
|
| 550 |
-
accumulated_text = text # text is already accumulated from debug agent
|
| 551 |
-
|
| 552 |
-
# Handle new ext format from debug_agent matching mcp-client
|
| 553 |
-
if ext:
|
| 554 |
-
if isinstance(ext, dict) and "data" in ext and "finished" in ext:
|
| 555 |
-
# New format: {"data": ext, "finished": finished}
|
| 556 |
-
final_ext_data = ext["data"]
|
| 557 |
-
finished = ext["finished"]
|
| 558 |
-
|
| 559 |
-
# 检查是否包含需要实时发送的workflow_update或param_update
|
| 560 |
-
has_realtime_ext = False
|
| 561 |
-
if final_ext_data:
|
| 562 |
-
for ext_item in final_ext_data:
|
| 563 |
-
if ext_item.get("type") in ["workflow_update", "param_update"]:
|
| 564 |
-
has_realtime_ext = True
|
| 565 |
-
break
|
| 566 |
-
|
| 567 |
-
# 如果包含实时ext数据或者已完成,则发送响应
|
| 568 |
-
if has_realtime_ext or finished:
|
| 569 |
-
chat_response = ChatResponse(
|
| 570 |
-
session_id=session_id,
|
| 571 |
-
text=accumulated_text,
|
| 572 |
-
finished=finished,
|
| 573 |
-
type="message",
|
| 574 |
-
format="markdown",
|
| 575 |
-
ext=final_ext_data # 发送ext数据
|
| 576 |
-
)
|
| 577 |
-
await response.write(json.dumps(chat_response).encode() + b"\n")
|
| 578 |
-
elif not finished:
|
| 579 |
-
# 只有文本更新,不发送ext数据
|
| 580 |
-
chat_response = ChatResponse(
|
| 581 |
-
session_id=session_id,
|
| 582 |
-
text=accumulated_text,
|
| 583 |
-
finished=False,
|
| 584 |
-
type="message",
|
| 585 |
-
format="markdown",
|
| 586 |
-
ext=None
|
| 587 |
-
)
|
| 588 |
-
await response.write(json.dumps(chat_response).encode() + b"\n")
|
| 589 |
-
else:
|
| 590 |
-
# Legacy format: direct ext data (for backward compatibility)
|
| 591 |
-
final_ext_data = ext
|
| 592 |
-
finished = False
|
| 593 |
-
|
| 594 |
-
# Create streaming response
|
| 595 |
-
chat_response = ChatResponse(
|
| 596 |
-
session_id=session_id,
|
| 597 |
-
text=accumulated_text,
|
| 598 |
-
finished=False,
|
| 599 |
-
type="message",
|
| 600 |
-
format="markdown",
|
| 601 |
-
ext=ext
|
| 602 |
-
)
|
| 603 |
-
await response.write(json.dumps(chat_response).encode() + b"\n")
|
| 604 |
-
else:
|
| 605 |
-
# No ext data, just text streaming
|
| 606 |
-
chat_response = ChatResponse(
|
| 607 |
-
session_id=session_id,
|
| 608 |
-
text=accumulated_text,
|
| 609 |
-
finished=False,
|
| 610 |
-
type="message",
|
| 611 |
-
format="markdown",
|
| 612 |
-
ext=None
|
| 613 |
-
)
|
| 614 |
-
await response.write(json.dumps(chat_response).encode() + b"\n")
|
| 615 |
-
|
| 616 |
-
await asyncio.sleep(0.01) # Small delay for streaming effect
|
| 617 |
-
|
| 618 |
-
# Send final response
|
| 619 |
-
final_response = ChatResponse(
|
| 620 |
-
session_id=session_id,
|
| 621 |
-
text=accumulated_text,
|
| 622 |
-
finished=True,
|
| 623 |
-
type="message",
|
| 624 |
-
format="markdown",
|
| 625 |
-
ext=final_ext_data if final_ext_data else [{"type": "debug_complete", "data": {"status": "completed"}}]
|
| 626 |
-
)
|
| 627 |
-
|
| 628 |
-
# Save workflow checkpoint after debug completion if we have workflow_data
|
| 629 |
-
if workflow_data and accumulated_text:
|
| 630 |
-
try:
|
| 631 |
-
current_session_id = get_session_id()
|
| 632 |
-
checkpoint_id = save_workflow_data(
|
| 633 |
-
session_id=current_session_id,
|
| 634 |
-
workflow_data=workflow_data,
|
| 635 |
-
workflow_data_ui=None, # UI format not available in debug agent
|
| 636 |
-
attributes={
|
| 637 |
-
"checkpoint_type": "debug_complete",
|
| 638 |
-
"description": "Workflow state after debug completion",
|
| 639 |
-
"timestamp": time.time()
|
| 640 |
-
}
|
| 641 |
-
)
|
| 642 |
-
|
| 643 |
-
# Add checkpoint info to ext data
|
| 644 |
-
if final_response["ext"]:
|
| 645 |
-
final_response["ext"].append({
|
| 646 |
-
"type": "debug_checkpoint",
|
| 647 |
-
"data": {
|
| 648 |
-
"checkpoint_id": checkpoint_id,
|
| 649 |
-
"checkpoint_type": "debug_complete"
|
| 650 |
-
}
|
| 651 |
-
})
|
| 652 |
-
else:
|
| 653 |
-
final_response["ext"] = [{
|
| 654 |
-
"type": "debug_checkpoint",
|
| 655 |
-
"data": {
|
| 656 |
-
"checkpoint_id": checkpoint_id,
|
| 657 |
-
"checkpoint_type": "debug_complete"
|
| 658 |
-
}
|
| 659 |
-
}]
|
| 660 |
-
|
| 661 |
-
log.info(f"Debug completion checkpoint saved with ID: {checkpoint_id}")
|
| 662 |
-
except Exception as checkpoint_error:
|
| 663 |
-
log.error(f"Failed to save debug completion checkpoint: {checkpoint_error}")
|
| 664 |
-
|
| 665 |
-
await response.write(json.dumps(final_response).encode() + b"\n")
|
| 666 |
-
log.info("Debug agent processing complete")
|
| 667 |
-
|
| 668 |
-
except Exception as e:
|
| 669 |
-
log.error(f"Error in debug agent: {str(e)}")
|
| 670 |
-
import traceback
|
| 671 |
-
traceback.print_exc()
|
| 672 |
-
|
| 673 |
-
error_response = ChatResponse(
|
| 674 |
-
session_id=session_id,
|
| 675 |
-
text=f"❌ Debug agent error: {str(e)}",
|
| 676 |
-
finished=True,
|
| 677 |
-
type="message",
|
| 678 |
-
format="text",
|
| 679 |
-
ext=[{"type": "error", "data": {"error": str(e)}}]
|
| 680 |
-
)
|
| 681 |
-
await response.write(json.dumps(error_response).encode() + b"\n")
|
| 682 |
-
|
| 683 |
-
await response.write_eof()
|
| 684 |
-
return response
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
@server.PromptServer.instance.routes.post("/api/update-workflow-ui")
|
| 688 |
-
async def update_workflow_ui(request):
|
| 689 |
-
"""
|
| 690 |
-
Update workflow_data_ui field for a specific checkpoint without affecting other fields
|
| 691 |
-
"""
|
| 692 |
-
log.info("Received update-workflow-ui request")
|
| 693 |
-
req_json = await request.json()
|
| 694 |
-
|
| 695 |
-
try:
|
| 696 |
-
checkpoint_id = req_json.get('checkpoint_id')
|
| 697 |
-
workflow_data_ui = req_json.get('workflow_data_ui')
|
| 698 |
-
|
| 699 |
-
if not checkpoint_id or not workflow_data_ui:
|
| 700 |
-
return web.json_response({
|
| 701 |
-
"success": False,
|
| 702 |
-
"message": "Missing required parameters: checkpoint_id and workflow_data_ui"
|
| 703 |
-
})
|
| 704 |
-
|
| 705 |
-
try:
|
| 706 |
-
checkpoint_id = int(checkpoint_id)
|
| 707 |
-
except ValueError:
|
| 708 |
-
return web.json_response({
|
| 709 |
-
"success": False,
|
| 710 |
-
"message": "Invalid checkpoint_id format"
|
| 711 |
-
})
|
| 712 |
-
|
| 713 |
-
# Update only the workflow_data_ui field
|
| 714 |
-
success = update_workflow_ui_by_id(checkpoint_id, workflow_data_ui)
|
| 715 |
-
|
| 716 |
-
if success:
|
| 717 |
-
log.info(f"Successfully updated workflow_data_ui for checkpoint ID: {checkpoint_id}")
|
| 718 |
-
return web.json_response({
|
| 719 |
-
"success": True,
|
| 720 |
-
"message": f"Workflow UI data updated successfully for checkpoint {checkpoint_id}"
|
| 721 |
-
})
|
| 722 |
-
else:
|
| 723 |
-
return web.json_response({
|
| 724 |
-
"success": False,
|
| 725 |
-
"message": f"Checkpoint {checkpoint_id} not found"
|
| 726 |
-
})
|
| 727 |
-
|
| 728 |
-
except Exception as e:
|
| 729 |
-
log.error(f"Error updating workflow UI data: {str(e)}")
|
| 730 |
-
return web.json_response({
|
| 731 |
-
"success": False,
|
| 732 |
-
"message": f"Failed to update workflow UI data: {str(e)}"
|
| 733 |
-
})
|
| 734 |
-
|
| 735 |
-
@server.PromptServer.instance.routes.post("/api/download-model")
|
| 736 |
-
async def download_model(request):
|
| 737 |
-
"""
|
| 738 |
-
Download model from ModelScope using SDK
|
| 739 |
-
"""
|
| 740 |
-
log.info("Received download-model request")
|
| 741 |
-
req_json = await request.json()
|
| 742 |
-
|
| 743 |
-
try:
|
| 744 |
-
id = req_json.get('id')
|
| 745 |
-
model_id = req_json.get('model_id')
|
| 746 |
-
model_type = req_json.get('model_type')
|
| 747 |
-
dest_dir = req_json.get('dest_dir')
|
| 748 |
-
|
| 749 |
-
# 验证必需参数
|
| 750 |
-
if not id:
|
| 751 |
-
return web.json_response({
|
| 752 |
-
"success": False,
|
| 753 |
-
"message": "Missing required parameter: id"
|
| 754 |
-
})
|
| 755 |
-
|
| 756 |
-
if not model_id:
|
| 757 |
-
return web.json_response({
|
| 758 |
-
"success": False,
|
| 759 |
-
"message": "Missing required parameter: model_id"
|
| 760 |
-
})
|
| 761 |
-
|
| 762 |
-
if not model_type:
|
| 763 |
-
return web.json_response({
|
| 764 |
-
"success": False,
|
| 765 |
-
"message": "Missing required parameter: model_type"
|
| 766 |
-
})
|
| 767 |
-
|
| 768 |
-
log.info(f"Downloading model: {model_id} (type: {model_type})")
|
| 769 |
-
|
| 770 |
-
# 生成下载ID
|
| 771 |
-
download_id = generate_download_id()
|
| 772 |
-
|
| 773 |
-
# 计算目标目录:优先使用传入的dest_dir,否则使用ComfyUI的models目录下对应类型
|
| 774 |
-
resolved_dest_dir = None
|
| 775 |
-
if dest_dir:
|
| 776 |
-
resolved_dest_dir = os.path.abspath(os.path.expanduser(f"models/{dest_dir}"))
|
| 777 |
-
else:
|
| 778 |
-
try:
|
| 779 |
-
model_type_paths = folder_paths.get_folder_paths(model_type)
|
| 780 |
-
resolved_dest_dir = model_type_paths[0] if model_type_paths else os.path.join(folder_paths.models_dir, model_type)
|
| 781 |
-
except Exception:
|
| 782 |
-
resolved_dest_dir = os.path.join(folder_paths.models_dir, model_type)
|
| 783 |
-
|
| 784 |
-
# 创建进度回调
|
| 785 |
-
progress_callback = DownloadProgressCallback(
|
| 786 |
-
id=id,
|
| 787 |
-
filename=f"{model_id}.{model_type}",
|
| 788 |
-
file_size=0, # 实际大小会在下载过程中获取
|
| 789 |
-
download_id=download_id
|
| 790 |
-
)
|
| 791 |
-
|
| 792 |
-
# 启动下载任务(异步执行)
|
| 793 |
-
async def download_task():
|
| 794 |
-
try:
|
| 795 |
-
# 调用下载方法 - 使用snapshot_download
|
| 796 |
-
from modelscope import snapshot_download
|
| 797 |
-
|
| 798 |
-
# 创建进度回调包装器 - 实现ModelScope期望的接口
|
| 799 |
-
class ProgressWrapper:
|
| 800 |
-
"""Factory that returns a per-file progress object with update/end."""
|
| 801 |
-
def __init__(self, download_id: str):
|
| 802 |
-
self.download_id = download_id
|
| 803 |
-
|
| 804 |
-
def __call__(self, file_name: str, file_size: int):
|
| 805 |
-
# Create a per-file progress tracker expected by ModelScope
|
| 806 |
-
download_id = self.download_id
|
| 807 |
-
|
| 808 |
-
class _PerFileProgress:
|
| 809 |
-
def __init__(self, fname: str, fsize: int):
|
| 810 |
-
self.file_name = fname
|
| 811 |
-
self.file_size = max(int(fsize or 0), 0)
|
| 812 |
-
self.progress = 0
|
| 813 |
-
self.last_update_time = time.time()
|
| 814 |
-
self.last_downloaded = 0
|
| 815 |
-
with download_lock:
|
| 816 |
-
if download_id in download_progress:
|
| 817 |
-
# If unknown size, keep 0 to avoid div-by-zero
|
| 818 |
-
download_progress[download_id]["file_size"] = self.file_size
|
| 819 |
-
|
| 820 |
-
def update(self, size: int):
|
| 821 |
-
try:
|
| 822 |
-
self.progress += int(size or 0)
|
| 823 |
-
now = time.time()
|
| 824 |
-
# Update global progress
|
| 825 |
-
with download_lock:
|
| 826 |
-
if download_id in download_progress:
|
| 827 |
-
dp = download_progress[download_id]
|
| 828 |
-
dp["progress"] = self.progress
|
| 829 |
-
if self.file_size > 0:
|
| 830 |
-
dp["percentage"] = round(self.progress * 100.0 / self.file_size, 2)
|
| 831 |
-
# speed
|
| 832 |
-
elapsed = max(now - self.last_update_time, 1e-6)
|
| 833 |
-
speed = (self.progress - self.last_downloaded) / elapsed
|
| 834 |
-
dp["speed"] = round(speed, 2)
|
| 835 |
-
self.last_update_time = now
|
| 836 |
-
self.last_downloaded = self.progress
|
| 837 |
-
except Exception as e:
|
| 838 |
-
log.error(f"Error in progress update: {e}")
|
| 839 |
-
|
| 840 |
-
def end(self):
|
| 841 |
-
# Called by modelscope when a file finishes
|
| 842 |
-
with download_lock:
|
| 843 |
-
if download_id in download_progress:
|
| 844 |
-
dp = download_progress[download_id]
|
| 845 |
-
if self.file_size > 0:
|
| 846 |
-
dp["progress"] = self.file_size
|
| 847 |
-
dp["percentage"] = 100.0
|
| 848 |
-
|
| 849 |
-
return _PerFileProgress(file_name, file_size)
|
| 850 |
-
|
| 851 |
-
progress_wrapper = ProgressWrapper(download_id)
|
| 852 |
-
|
| 853 |
-
# 添加调试日志
|
| 854 |
-
log.info(f"Starting download with progress wrapper: {download_id}")
|
| 855 |
-
|
| 856 |
-
# 在线程中执行阻塞的下载,避免阻塞事件循环
|
| 857 |
-
from functools import partial
|
| 858 |
-
local_dir = await asyncio.to_thread(
|
| 859 |
-
partial(
|
| 860 |
-
snapshot_download,
|
| 861 |
-
model_id=model_id,
|
| 862 |
-
cache_dir=resolved_dest_dir,
|
| 863 |
-
progress_callbacks=[progress_wrapper]
|
| 864 |
-
)
|
| 865 |
-
)
|
| 866 |
-
|
| 867 |
-
# 下载完成
|
| 868 |
-
progress_callback.end(success=True)
|
| 869 |
-
log.info(f"Model downloaded successfully to: {local_dir}")
|
| 870 |
-
|
| 871 |
-
# 下载后遍历目录,将所有重要权重/资源文件移动到最外层(与目录同级,即 resolved_dest_dir)
|
| 872 |
-
try:
|
| 873 |
-
moved_count = 0
|
| 874 |
-
allowed_exts = {
|
| 875 |
-
".safetensors", ".ckpt", ".pt", ".pth", ".bin"
|
| 876 |
-
# ".msgpack", ".json", ".yaml", ".yml", ".toml", ".png", ".onnx"
|
| 877 |
-
}
|
| 878 |
-
for root, dirs, files in os.walk(local_dir):
|
| 879 |
-
for name in files:
|
| 880 |
-
ext = os.path.splitext(name)[1].lower()
|
| 881 |
-
if ext in allowed_exts:
|
| 882 |
-
src_path = os.path.join(root, name)
|
| 883 |
-
target_dir = resolved_dest_dir
|
| 884 |
-
os.makedirs(target_dir, exist_ok=True)
|
| 885 |
-
target_path = os.path.join(target_dir, name)
|
| 886 |
-
# 如果已经在目标目录则跳过
|
| 887 |
-
if os.path.abspath(os.path.dirname(src_path)) == os.path.abspath(target_dir):
|
| 888 |
-
continue
|
| 889 |
-
# 处理重名情况:自动追加 _1, _2 ...
|
| 890 |
-
if os.path.exists(target_path):
|
| 891 |
-
base, ext_real = os.path.splitext(name)
|
| 892 |
-
idx = 1
|
| 893 |
-
while True:
|
| 894 |
-
candidate = f"{base}_{idx}{ext_real}"
|
| 895 |
-
candidate_path = os.path.join(target_dir, candidate)
|
| 896 |
-
if not os.path.exists(candidate_path):
|
| 897 |
-
target_path = candidate_path
|
| 898 |
-
break
|
| 899 |
-
idx += 1
|
| 900 |
-
shutil.move(src_path, target_path)
|
| 901 |
-
moved_count += 1
|
| 902 |
-
log.info(f"Moved {moved_count} files with extensions {sorted(list(allowed_exts))} to: {resolved_dest_dir}")
|
| 903 |
-
except Exception as move_err:
|
| 904 |
-
log.error(f"Post-download move failed: {move_err}")
|
| 905 |
-
|
| 906 |
-
except Exception as e:
|
| 907 |
-
progress_callback.fail(str(e))
|
| 908 |
-
log.error(f"Download failed: {str(e)}")
|
| 909 |
-
|
| 910 |
-
# 启动异步下载任务
|
| 911 |
-
asyncio.create_task(download_task())
|
| 912 |
-
|
| 913 |
-
return web.json_response({
|
| 914 |
-
"success": True,
|
| 915 |
-
"data": {
|
| 916 |
-
"download_id": download_id,
|
| 917 |
-
"id": id,
|
| 918 |
-
"model_id": model_id,
|
| 919 |
-
"model_type": model_type,
|
| 920 |
-
"dest_dir": resolved_dest_dir,
|
| 921 |
-
"status": "started"
|
| 922 |
-
},
|
| 923 |
-
"message": f"Download started for model '{model_id}'"
|
| 924 |
-
})
|
| 925 |
-
|
| 926 |
-
except ImportError as e:
|
| 927 |
-
log.error(f"ModelScope SDK not installed: {str(e)}")
|
| 928 |
-
return web.json_response({
|
| 929 |
-
"success": False,
|
| 930 |
-
"message": "ModelScope SDK not installed. Please install with: pip install modelscope"
|
| 931 |
-
})
|
| 932 |
-
|
| 933 |
-
except Exception as e:
|
| 934 |
-
log.error(f"Error starting download: {str(e)}")
|
| 935 |
-
import traceback
|
| 936 |
-
traceback.print_exc()
|
| 937 |
-
return web.json_response({
|
| 938 |
-
"success": False,
|
| 939 |
-
"message": f"Failed to start download: {str(e)}"
|
| 940 |
-
})
|
| 941 |
-
|
| 942 |
-
@server.PromptServer.instance.routes.get("/api/download-progress/{download_id}")
|
| 943 |
-
async def get_download_progress(request):
|
| 944 |
-
"""
|
| 945 |
-
Get download progress by download ID
|
| 946 |
-
"""
|
| 947 |
-
download_id = request.match_info.get('download_id')
|
| 948 |
-
|
| 949 |
-
if not download_id:
|
| 950 |
-
return web.json_response({
|
| 951 |
-
"success": False,
|
| 952 |
-
"message": "Missing download_id parameter"
|
| 953 |
-
})
|
| 954 |
-
|
| 955 |
-
with download_lock:
|
| 956 |
-
progress_info = download_progress.get(download_id)
|
| 957 |
-
|
| 958 |
-
if not progress_info:
|
| 959 |
-
return web.json_response({
|
| 960 |
-
"success": False,
|
| 961 |
-
"message": f"Download ID {download_id} not found"
|
| 962 |
-
})
|
| 963 |
-
|
| 964 |
-
return web.json_response({
|
| 965 |
-
"success": True,
|
| 966 |
-
"data": progress_info,
|
| 967 |
-
"message": "Download progress retrieved successfully"
|
| 968 |
-
})
|
| 969 |
-
|
| 970 |
-
@server.PromptServer.instance.routes.get("/api/download-progress")
|
| 971 |
-
async def list_downloads(request):
|
| 972 |
-
"""
|
| 973 |
-
List all active downloads
|
| 974 |
-
"""
|
| 975 |
-
with download_lock:
|
| 976 |
-
downloads = list(download_progress.keys())
|
| 977 |
-
|
| 978 |
-
return web.json_response({
|
| 979 |
-
"success": True,
|
| 980 |
-
"data": {
|
| 981 |
-
"downloads": downloads,
|
| 982 |
-
"count": len(downloads)
|
| 983 |
-
},
|
| 984 |
-
"message": "Download list retrieved successfully"
|
| 985 |
-
})
|
| 986 |
-
|
| 987 |
-
@server.PromptServer.instance.routes.delete("/api/download-progress/{download_id}")
|
| 988 |
-
async def clear_download_progress(request):
|
| 989 |
-
"""
|
| 990 |
-
Clear download progress record (for cleanup)
|
| 991 |
-
"""
|
| 992 |
-
download_id = request.match_info.get('download_id')
|
| 993 |
-
|
| 994 |
-
if not download_id:
|
| 995 |
-
return web.json_response({
|
| 996 |
-
"success": False,
|
| 997 |
-
"message": "Missing download_id parameter"
|
| 998 |
-
})
|
| 999 |
-
|
| 1000 |
-
with download_lock:
|
| 1001 |
-
if download_id in download_progress:
|
| 1002 |
-
del download_progress[download_id]
|
| 1003 |
-
return web.json_response({
|
| 1004 |
-
"success": True,
|
| 1005 |
-
"message": f"Download progress {download_id} cleared successfully"
|
| 1006 |
-
})
|
| 1007 |
-
else:
|
| 1008 |
-
return web.json_response({
|
| 1009 |
-
"success": False,
|
| 1010 |
-
"message": f"Download ID {download_id} not found"
|
| 1011 |
-
})
|
| 1012 |
-
|
| 1013 |
-
@server.PromptServer.instance.routes.get("/api/model-searchs")
|
| 1014 |
-
async def model_suggests(request):
|
| 1015 |
-
"""
|
| 1016 |
-
Get model search list by keyword
|
| 1017 |
-
"""
|
| 1018 |
-
log.info("Received model-search request")
|
| 1019 |
-
try:
|
| 1020 |
-
keyword = request.query.get('keyword')
|
| 1021 |
-
|
| 1022 |
-
if not keyword:
|
| 1023 |
-
return web.json_response({
|
| 1024 |
-
"success": False,
|
| 1025 |
-
"message": "Missing required parameter: keyword"
|
| 1026 |
-
})
|
| 1027 |
-
|
| 1028 |
-
# 创建ModelScope网关实例
|
| 1029 |
-
gateway = ModelScopeGateway()
|
| 1030 |
-
|
| 1031 |
-
suggests = gateway.search(name=keyword)
|
| 1032 |
-
|
| 1033 |
-
list = suggests["data"] if suggests.get("data") else []
|
| 1034 |
-
|
| 1035 |
-
return web.json_response({
|
| 1036 |
-
"success": True,
|
| 1037 |
-
"data": {
|
| 1038 |
-
"searchs": list,
|
| 1039 |
-
"total": len(list)
|
| 1040 |
-
},
|
| 1041 |
-
"message": f"Get searchs successfully"
|
| 1042 |
-
})
|
| 1043 |
-
|
| 1044 |
-
except ImportError as e:
|
| 1045 |
-
log.error(f"ModelScope SDK not installed: {str(e)}")
|
| 1046 |
-
return web.json_response({
|
| 1047 |
-
"success": False,
|
| 1048 |
-
"message": "ModelScope SDK not installed. Please install with: pip install modelscope"
|
| 1049 |
-
})
|
| 1050 |
-
|
| 1051 |
-
except Exception as e:
|
| 1052 |
-
log.error(f"Error get model searchs: {str(e)}")
|
| 1053 |
-
import traceback
|
| 1054 |
-
traceback.print_exc()
|
| 1055 |
-
return web.json_response({
|
| 1056 |
-
"success": False,
|
| 1057 |
-
"message": f"Get model searchs failed: {str(e)}"
|
| 1058 |
-
})
|
| 1059 |
-
|
| 1060 |
-
@server.PromptServer.instance.routes.get("/api/model-paths")
|
| 1061 |
-
async def model_paths(request):
|
| 1062 |
-
"""
|
| 1063 |
-
Get model paths by type
|
| 1064 |
-
"""
|
| 1065 |
-
log.info("Received model-paths request")
|
| 1066 |
-
try:
|
| 1067 |
-
model_paths = list(folder_paths.folder_names_and_paths.keys())
|
| 1068 |
-
return web.json_response({
|
| 1069 |
-
"success": True,
|
| 1070 |
-
"data": {
|
| 1071 |
-
"paths": model_paths,
|
| 1072 |
-
},
|
| 1073 |
-
"message": f"Get paths successfully"
|
| 1074 |
-
})
|
| 1075 |
-
|
| 1076 |
-
except Exception as e:
|
| 1077 |
-
log.error(f"Error get model path: {str(e)}")
|
| 1078 |
-
import traceback
|
| 1079 |
-
traceback.print_exc()
|
| 1080 |
-
return web.json_response({
|
| 1081 |
-
"success": False,
|
| 1082 |
-
"message": f"Get model failed: {str(e)}"
|
| 1083 |
-
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Copilot/backend/controller/expert_api.py
DELETED
|
@@ -1,192 +0,0 @@
|
|
| 1 |
-
from server import PromptServer
|
| 2 |
-
from aiohttp import web
|
| 3 |
-
from typing import Dict, Any
|
| 4 |
-
import logging
|
| 5 |
-
from ..dao.expert_table import (
|
| 6 |
-
create_rewrite_expert,
|
| 7 |
-
get_rewrite_expert,
|
| 8 |
-
list_rewrite_experts,
|
| 9 |
-
update_rewrite_expert_by_id,
|
| 10 |
-
delete_rewrite_expert_by_id
|
| 11 |
-
)
|
| 12 |
-
|
| 13 |
-
# 配置日志
|
| 14 |
-
logger = logging.getLogger(__name__)
|
| 15 |
-
|
| 16 |
-
def validate_expert_data(data: Dict[str, Any]) -> tuple[bool, str, Dict[str, Any]]:
|
| 17 |
-
"""验证专家数据"""
|
| 18 |
-
if not data:
|
| 19 |
-
return False, "请求数据不能为空", {}
|
| 20 |
-
|
| 21 |
-
# 验证必填字段
|
| 22 |
-
if 'name' not in data or not data['name']:
|
| 23 |
-
return False, "名称不能为空", {}
|
| 24 |
-
|
| 25 |
-
# 验证字段类型和长度
|
| 26 |
-
if not isinstance(data['name'], str) or len(data['name']) > 255:
|
| 27 |
-
return False, "名称必须是字符串且长度不超过255个字符", {}
|
| 28 |
-
|
| 29 |
-
# 验证可选字段
|
| 30 |
-
validated_data = {
|
| 31 |
-
'name': data['name'].strip(),
|
| 32 |
-
'description': data.get('description'),
|
| 33 |
-
'content': data.get('content')
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
return True, "", validated_data
|
| 37 |
-
|
| 38 |
-
@PromptServer.instance.routes.post("/api/expert/experts")
|
| 39 |
-
async def create_expert(request):
|
| 40 |
-
"""创建新的专家记录"""
|
| 41 |
-
try:
|
| 42 |
-
data = await request.json()
|
| 43 |
-
if not data:
|
| 44 |
-
return web.json_response({"success": False, "message": "请求数据格式错误", "data": None}, status=400)
|
| 45 |
-
|
| 46 |
-
# 验证数据
|
| 47 |
-
is_valid, error_msg, validated_data = validate_expert_data(data)
|
| 48 |
-
if not is_valid:
|
| 49 |
-
return web.json_response({"success": False, "message": error_msg, "data": None}, status=400)
|
| 50 |
-
|
| 51 |
-
# 创建专家记录
|
| 52 |
-
expert_id = create_rewrite_expert(
|
| 53 |
-
name=validated_data['name'],
|
| 54 |
-
description=validated_data['description'],
|
| 55 |
-
content=validated_data['content']
|
| 56 |
-
)
|
| 57 |
-
|
| 58 |
-
logger.info(f"成功创建专家记录,ID: {expert_id}")
|
| 59 |
-
|
| 60 |
-
return web.json_response({"success": True, "message": "创建成功", "data": {"id": expert_id}}, status=200)
|
| 61 |
-
|
| 62 |
-
except Exception as e:
|
| 63 |
-
logger.error(f"创建专家记录失败: {str(e)}")
|
| 64 |
-
return web.json_response({"success": False, "message": f"创建失败: {str(e)}", "data": None}, status=500)
|
| 65 |
-
|
| 66 |
-
@PromptServer.instance.routes.get("/api/expert/experts")
|
| 67 |
-
async def get_experts(request):
|
| 68 |
-
"""获取所有专家记录列表"""
|
| 69 |
-
try:
|
| 70 |
-
experts = list_rewrite_experts()
|
| 71 |
-
|
| 72 |
-
logger.info(f"成功获取专家记录列表,共 {len(experts)} 条")
|
| 73 |
-
|
| 74 |
-
return web.json_response({"success": True, "message": "获取列表成功", "data": {"total": len(experts), "experts": experts}}, status=200)
|
| 75 |
-
|
| 76 |
-
except Exception as e:
|
| 77 |
-
logger.error(f"获取专家记录列表失败: {str(e)}")
|
| 78 |
-
return web.json_response({"success": False, "message": f"获取列表失败: {str(e)}", "data": None}, status=500)
|
| 79 |
-
|
| 80 |
-
@PromptServer.instance.routes.get("/api/expert/experts/{expert_id}")
|
| 81 |
-
async def get_expert_by_id(request):
|
| 82 |
-
"""根据ID获取专家记录"""
|
| 83 |
-
try:
|
| 84 |
-
expert_id = int(request.match_info['expert_id'])
|
| 85 |
-
expert = get_rewrite_expert(expert_id)
|
| 86 |
-
|
| 87 |
-
if not expert:
|
| 88 |
-
return web.json_response({"success": False, "message": "ID不存在", "data": None}, status=404)
|
| 89 |
-
|
| 90 |
-
logger.info(f"成功获取专家记录,ID: {expert_id}")
|
| 91 |
-
|
| 92 |
-
return web.json_response({"success": True, "message": "获取记录成功", "data": expert}, status=200)
|
| 93 |
-
|
| 94 |
-
except Exception as e:
|
| 95 |
-
logger.error(f"获取专家记录失败,ID: {expert_id}, 错误: {str(e)}")
|
| 96 |
-
return web.json_response({"success": False, "message": f"获取记录失败: {str(e)}", "data": None}, status=500)
|
| 97 |
-
|
| 98 |
-
@PromptServer.instance.routes.put("/api/expert/experts/{expert_id}")
|
| 99 |
-
async def update_expert(request):
|
| 100 |
-
"""更新专家记录"""
|
| 101 |
-
try:
|
| 102 |
-
expert_id = int(request.match_info['expert_id'])
|
| 103 |
-
data = await request.json()
|
| 104 |
-
if not data:
|
| 105 |
-
return web.json_response({"success": False, "message": "请求数据格式错误", "data": None}, status=400)
|
| 106 |
-
|
| 107 |
-
# 验证数据
|
| 108 |
-
is_valid, error_msg, validated_data = validate_expert_data(data)
|
| 109 |
-
if not is_valid:
|
| 110 |
-
return web.json_response({"success": False, "message": error_msg, "data": None}, status=400)
|
| 111 |
-
|
| 112 |
-
# 更新专家记录
|
| 113 |
-
success = update_rewrite_expert_by_id(
|
| 114 |
-
expert_id=expert_id,
|
| 115 |
-
name=validated_data.get('name'),
|
| 116 |
-
description=validated_data.get('description'),
|
| 117 |
-
content=validated_data.get('content')
|
| 118 |
-
)
|
| 119 |
-
|
| 120 |
-
if not success:
|
| 121 |
-
return web.json_response({"success": False, "message": "ID不存在", "data": None}, status=404)
|
| 122 |
-
|
| 123 |
-
logger.info(f"成功更新专家记录,ID: {expert_id}")
|
| 124 |
-
|
| 125 |
-
return web.json_response({"success": True, "message": "更新成功", "data": {"id": expert_id}}, status=200)
|
| 126 |
-
|
| 127 |
-
except Exception as e:
|
| 128 |
-
logger.error(f"更新专家记录失败,ID: {expert_id}, 错误: {str(e)}")
|
| 129 |
-
return web.json_response({"success": False, "message": f"更新失败: {str(e)}", "data": None}, status=500)
|
| 130 |
-
|
| 131 |
-
@PromptServer.instance.routes.delete("/api/expert/experts/{expert_id}")
|
| 132 |
-
async def delete_expert(request):
|
| 133 |
-
"""删除专家记录"""
|
| 134 |
-
try:
|
| 135 |
-
expert_id = int(request.match_info['expert_id'])
|
| 136 |
-
success = delete_rewrite_expert_by_id(expert_id)
|
| 137 |
-
|
| 138 |
-
if not success:
|
| 139 |
-
return web.json_response({"success": False, "message": "ID不存在", "data": None}, status=404)
|
| 140 |
-
|
| 141 |
-
logger.info(f"成功删除专家记录,ID: {expert_id}")
|
| 142 |
-
|
| 143 |
-
return web.json_response({"success": True, "message": "删除成功", "data": {"id": expert_id}}, status=200)
|
| 144 |
-
|
| 145 |
-
except Exception as e:
|
| 146 |
-
logger.error(f"删除专家记录失败,ID: {expert_id}, 错误: {str(e)}")
|
| 147 |
-
return web.json_response({"success": False, "message": f"删除失败: {str(e)}", "data": None}, status=500)
|
| 148 |
-
|
| 149 |
-
@PromptServer.instance.routes.patch("/api/expert/experts/{expert_id}")
|
| 150 |
-
async def partial_update_expert(request):
|
| 151 |
-
"""部分更新专家记录"""
|
| 152 |
-
try:
|
| 153 |
-
expert_id = int(request.match_info['expert_id'])
|
| 154 |
-
data = await request.json()
|
| 155 |
-
if not data:
|
| 156 |
-
return web.json_response({"success": False, "message": "请求数据格式错误", "data": None}, status=400)
|
| 157 |
-
|
| 158 |
-
# 验证字段
|
| 159 |
-
update_data = {}
|
| 160 |
-
if 'name' in data:
|
| 161 |
-
if not isinstance(data['name'], str) or len(data['name']) > 255:
|
| 162 |
-
return web.json_response({"success": False, "message": "名称必须是字符串且长度不超过255个字符", "data": None}, status=400)
|
| 163 |
-
update_data['name'] = data['name'].strip()
|
| 164 |
-
|
| 165 |
-
if 'description' in data:
|
| 166 |
-
update_data['description'] = data['description']
|
| 167 |
-
|
| 168 |
-
if 'content' in data:
|
| 169 |
-
update_data['content'] = data['content']
|
| 170 |
-
|
| 171 |
-
if not update_data:
|
| 172 |
-
return web.json_response({"success": False, "message": "没有提供要更新的字段", "data": None}, status=400)
|
| 173 |
-
|
| 174 |
-
# 更新专家记录
|
| 175 |
-
success = update_rewrite_expert_by_id(
|
| 176 |
-
expert_id=expert_id,
|
| 177 |
-
**update_data
|
| 178 |
-
)
|
| 179 |
-
|
| 180 |
-
if not success:
|
| 181 |
-
return web.json_response({"success": False, "message": "ID不存在", "data": None}, status=404)
|
| 182 |
-
|
| 183 |
-
logger.info(f"成功部分更新专家记录,ID: {expert_id}")
|
| 184 |
-
|
| 185 |
-
return web.json_response({"success": True, "message": "更新成功", "data": {"id": expert_id}}, status=200)
|
| 186 |
-
|
| 187 |
-
except Exception as e:
|
| 188 |
-
logger.error(f"部分更新专家记录失败,ID: {expert_id}, 错误: {str(e)}")
|
| 189 |
-
return web.json_response({"success": False, "message": f"更新失败: {str(e)}", "data": None}, status=500)
|
| 190 |
-
|
| 191 |
-
# 路由配置函数
|
| 192 |
-
# 所有路由已通过装饰器自动注册到 ComfyUI 的 PromptServer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Copilot/backend/core.py
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
|
| 3 |
-
from agents._config import set_default_openai_api
|
| 4 |
-
from agents.tracing import set_tracing_disabled
|
| 5 |
-
# from .utils.logger import log
|
| 6 |
-
|
| 7 |
-
# def load_env_config():
|
| 8 |
-
# """Load environment variables from .env.llm file"""
|
| 9 |
-
# from dotenv import load_dotenv
|
| 10 |
-
|
| 11 |
-
# env_file_path = os.path.join(os.path.dirname(__file__), '.env.llm')
|
| 12 |
-
# if os.path.exists(env_file_path):
|
| 13 |
-
# load_dotenv(env_file_path)
|
| 14 |
-
# log.info(f"Loaded environment variables from {env_file_path}")
|
| 15 |
-
# else:
|
| 16 |
-
# log.warning(f"Warning: .env.llm not found at {env_file_path}")
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
# # Load environment configuration
|
| 20 |
-
# load_env_config()
|
| 21 |
-
|
| 22 |
-
set_default_openai_api("chat_completions")
|
| 23 |
-
set_tracing_disabled(True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Copilot/backend/dao/__init__.py
DELETED
|
File without changes
|
custom_nodes/ComfyUI-Copilot/backend/dao/workflow_table.py
DELETED
|
@@ -1,183 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import json
|
| 3 |
-
from typing import Dict, Any, Optional
|
| 4 |
-
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text
|
| 5 |
-
from sqlalchemy.ext.declarative import declarative_base
|
| 6 |
-
from sqlalchemy.orm import sessionmaker
|
| 7 |
-
from datetime import datetime
|
| 8 |
-
|
| 9 |
-
# 创建数据库基类
|
| 10 |
-
Base = declarative_base()
|
| 11 |
-
|
| 12 |
-
# 定义workflow_version表模型
|
| 13 |
-
class WorkflowVersion(Base):
|
| 14 |
-
__tablename__ = 'workflow_version'
|
| 15 |
-
|
| 16 |
-
id = Column(Integer, primary_key=True, autoincrement=True)
|
| 17 |
-
session_id = Column(String(255), nullable=False)
|
| 18 |
-
workflow_data = Column(Text, nullable=False) # JSON字符串 api格式
|
| 19 |
-
workflow_data_ui = Column(Text, nullable=True) # JSON字符串 ui格式
|
| 20 |
-
attributes = Column(Text, nullable=True) # JSON字符串,存储额外属性
|
| 21 |
-
created_at = Column(DateTime, default=datetime.utcnow)
|
| 22 |
-
|
| 23 |
-
def to_dict(self):
|
| 24 |
-
return {
|
| 25 |
-
'id': self.id,
|
| 26 |
-
'session_id': self.session_id,
|
| 27 |
-
'workflow_data': json.loads(self.workflow_data) if self.workflow_data else None,
|
| 28 |
-
'attributes': json.loads(self.attributes) if self.attributes else None,
|
| 29 |
-
'created_at': self.created_at.isoformat() if self.created_at else None
|
| 30 |
-
}
|
| 31 |
-
|
| 32 |
-
class DatabaseManager:
|
| 33 |
-
"""数据库管理器"""
|
| 34 |
-
|
| 35 |
-
def __init__(self, db_path: str = None):
|
| 36 |
-
if db_path is None:
|
| 37 |
-
# 默认数据库路径
|
| 38 |
-
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 39 |
-
db_dir = os.path.join(current_dir, '..', 'data')
|
| 40 |
-
os.makedirs(db_dir, exist_ok=True)
|
| 41 |
-
db_path = os.path.join(db_dir, 'workflow_debug.db')
|
| 42 |
-
|
| 43 |
-
self.db_path = db_path
|
| 44 |
-
self.engine = create_engine(f'sqlite:///{db_path}', echo=False)
|
| 45 |
-
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
|
| 46 |
-
|
| 47 |
-
# 创建表
|
| 48 |
-
Base.metadata.create_all(bind=self.engine)
|
| 49 |
-
|
| 50 |
-
def get_session(self):
|
| 51 |
-
"""获取数据库会话"""
|
| 52 |
-
return self.SessionLocal()
|
| 53 |
-
|
| 54 |
-
def save_workflow_version(self, session_id: str, workflow_data: Dict[str, Any], workflow_data_ui: Dict[str, Any] = None, attributes: Optional[Dict[str, Any]] = None) -> int:
|
| 55 |
-
"""保存工作流版本,返回新版本的ID"""
|
| 56 |
-
session = self.get_session()
|
| 57 |
-
try:
|
| 58 |
-
workflow_version = WorkflowVersion(
|
| 59 |
-
session_id=session_id,
|
| 60 |
-
workflow_data=json.dumps(workflow_data),
|
| 61 |
-
workflow_data_ui=json.dumps(workflow_data_ui) if workflow_data_ui else None,
|
| 62 |
-
attributes=json.dumps(attributes) if attributes else None
|
| 63 |
-
)
|
| 64 |
-
session.add(workflow_version)
|
| 65 |
-
session.commit()
|
| 66 |
-
session.refresh(workflow_version)
|
| 67 |
-
return workflow_version.id
|
| 68 |
-
except Exception as e:
|
| 69 |
-
session.rollback()
|
| 70 |
-
raise e
|
| 71 |
-
finally:
|
| 72 |
-
session.close()
|
| 73 |
-
|
| 74 |
-
def get_current_workflow_data(self, session_id: str) -> Optional[Dict[str, Any]]:
|
| 75 |
-
"""获取当前session的最新工作流数据(最大ID版本)"""
|
| 76 |
-
session = self.get_session()
|
| 77 |
-
try:
|
| 78 |
-
latest_version = session.query(WorkflowVersion)\
|
| 79 |
-
.filter(WorkflowVersion.session_id == session_id)\
|
| 80 |
-
.order_by(WorkflowVersion.id.desc())\
|
| 81 |
-
.first()
|
| 82 |
-
|
| 83 |
-
if latest_version:
|
| 84 |
-
return json.loads(latest_version.workflow_data)
|
| 85 |
-
return None
|
| 86 |
-
finally:
|
| 87 |
-
session.close()
|
| 88 |
-
|
| 89 |
-
def get_current_workflow_data_ui(self, session_id: str) -> Optional[Dict[str, Any]]:
|
| 90 |
-
"""获取当前session的最新工作流数据(最大ID版本)"""
|
| 91 |
-
session = self.get_session()
|
| 92 |
-
try:
|
| 93 |
-
latest_version = session.query(WorkflowVersion)\
|
| 94 |
-
.filter(WorkflowVersion.session_id == session_id)\
|
| 95 |
-
.order_by(WorkflowVersion.id.desc())\
|
| 96 |
-
.first()
|
| 97 |
-
|
| 98 |
-
if latest_version:
|
| 99 |
-
return json.loads(latest_version.workflow_data_ui)
|
| 100 |
-
return None
|
| 101 |
-
finally:
|
| 102 |
-
session.close()
|
| 103 |
-
|
| 104 |
-
def get_workflow_version_by_id(self, version_id: int) -> Optional[Dict[str, Any]]:
|
| 105 |
-
"""根据版本ID获取工作流数据"""
|
| 106 |
-
session = self.get_session()
|
| 107 |
-
try:
|
| 108 |
-
version = session.query(WorkflowVersion)\
|
| 109 |
-
.filter(WorkflowVersion.id == version_id)\
|
| 110 |
-
.first()
|
| 111 |
-
|
| 112 |
-
if version:
|
| 113 |
-
result = version.to_dict()
|
| 114 |
-
# 添加UI格式的工作流数据
|
| 115 |
-
if version.workflow_data_ui:
|
| 116 |
-
result['workflow_data_ui'] = json.loads(version.workflow_data_ui)
|
| 117 |
-
return result
|
| 118 |
-
return None
|
| 119 |
-
finally:
|
| 120 |
-
session.close()
|
| 121 |
-
|
| 122 |
-
def update_workflow_version(self, version_id: int, workflow_data: Dict[str, Any], attributes: Optional[Dict[str, Any]] = None) -> bool:
|
| 123 |
-
"""更新指定版本的工作流数据"""
|
| 124 |
-
session = self.get_session()
|
| 125 |
-
try:
|
| 126 |
-
version = session.query(WorkflowVersion)\
|
| 127 |
-
.filter(WorkflowVersion.id == version_id)\
|
| 128 |
-
.first()
|
| 129 |
-
|
| 130 |
-
if version:
|
| 131 |
-
version.workflow_data = json.dumps(workflow_data)
|
| 132 |
-
if attributes:
|
| 133 |
-
version.attributes = json.dumps(attributes)
|
| 134 |
-
session.commit()
|
| 135 |
-
return True
|
| 136 |
-
return False
|
| 137 |
-
except Exception as e:
|
| 138 |
-
session.rollback()
|
| 139 |
-
raise e
|
| 140 |
-
finally:
|
| 141 |
-
session.close()
|
| 142 |
-
|
| 143 |
-
def update_workflow_ui(self, version_id: int, workflow_data_ui: Dict[str, Any]) -> bool:
|
| 144 |
-
"""只更新指定版本的workflow_data_ui字段,不影响其他字段"""
|
| 145 |
-
session = self.get_session()
|
| 146 |
-
try:
|
| 147 |
-
version = session.query(WorkflowVersion)\
|
| 148 |
-
.filter(WorkflowVersion.id == version_id)\
|
| 149 |
-
.first()
|
| 150 |
-
|
| 151 |
-
if version:
|
| 152 |
-
version.workflow_data_ui = json.dumps(workflow_data_ui)
|
| 153 |
-
session.commit()
|
| 154 |
-
return True
|
| 155 |
-
return False
|
| 156 |
-
except Exception as e:
|
| 157 |
-
session.rollback()
|
| 158 |
-
raise e
|
| 159 |
-
finally:
|
| 160 |
-
session.close()
|
| 161 |
-
|
| 162 |
-
# 全局数据库管理器实例
|
| 163 |
-
db_manager = DatabaseManager()
|
| 164 |
-
|
| 165 |
-
def get_workflow_data(session_id: str) -> Optional[Dict[str, Any]]:
|
| 166 |
-
"""获取当前session的工作流数据的便捷函数"""
|
| 167 |
-
return db_manager.get_current_workflow_data(session_id)
|
| 168 |
-
|
| 169 |
-
def get_workflow_data_ui(session_id: str) -> Optional[Dict[str, Any]]:
|
| 170 |
-
"""获取当前session的工作流数据的便捷函数"""
|
| 171 |
-
return db_manager.get_current_workflow_data_ui(session_id)
|
| 172 |
-
|
| 173 |
-
def save_workflow_data(session_id: str, workflow_data: Dict[str, Any], workflow_data_ui: Dict[str, Any] = None, attributes: Optional[Dict[str, Any]] = None) -> int:
|
| 174 |
-
"""保存工作流数据的便捷函数"""
|
| 175 |
-
return db_manager.save_workflow_version(session_id, workflow_data, workflow_data_ui, attributes)
|
| 176 |
-
|
| 177 |
-
def get_workflow_data_by_id(version_id: int) -> Optional[Dict[str, Any]]:
|
| 178 |
-
"""根据版本ID获取工作流数据的便捷函数"""
|
| 179 |
-
return db_manager.get_workflow_version_by_id(version_id)
|
| 180 |
-
|
| 181 |
-
def update_workflow_ui_by_id(version_id: int, workflow_data_ui: Dict[str, Any]) -> bool:
|
| 182 |
-
"""只更新指定版本的workflow_data_ui字段的便捷函数"""
|
| 183 |
-
return db_manager.update_workflow_ui(version_id, workflow_data_ui)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
custom_nodes/ComfyUI-Copilot/backend/logs/comfyui_copilot.log
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
custom_nodes/ComfyUI-Copilot/backend/service/__init__.py
DELETED
|
File without changes
|
custom_nodes/ComfyUI-Copilot/backend/service/link_agent_tools.py
DELETED
|
@@ -1,467 +0,0 @@
|
|
| 1 |
-
# Link Agent Tools for ComfyUI Workflow Connection Analysis
|
| 2 |
-
|
| 3 |
-
import json
|
| 4 |
-
import time
|
| 5 |
-
from typing import Dict, Optional, List
|
| 6 |
-
|
| 7 |
-
from agents.tool import function_tool
|
| 8 |
-
from ..utils.request_context import get_session_id
|
| 9 |
-
from ..dao.workflow_table import get_workflow_data, save_workflow_data
|
| 10 |
-
from ..utils.comfy_gateway import get_object_info
|
| 11 |
-
from ..utils.logger import log
|
| 12 |
-
|
| 13 |
-
@function_tool
|
| 14 |
-
async def analyze_missing_connections() -> str:
|
| 15 |
-
"""
|
| 16 |
-
分析工作流中缺失的连接,枚举所有可能的连接选项和所需的新节点。
|
| 17 |
-
|
| 18 |
-
返回格式说明:
|
| 19 |
-
- missing_connections: 缺失连接的详细列表,包含节点ID、输入名称、需要的数据类型等(仅包含required输入)
|
| 20 |
-
- possible_connections: 现有节点可以提供的连接选项
|
| 21 |
-
- universal_inputs: 可以接受任意输出类型的通用输入端口
|
| 22 |
-
- optional_unconnected_inputs: 未连接的可选输入列表,包含节点ID、输入名称、配置信息等
|
| 23 |
-
- required_new_nodes: 需要创建的新节点类型列表
|
| 24 |
-
- connection_summary: 连接分析的统计摘要(包含optional输入的统计)
|
| 25 |
-
"""
|
| 26 |
-
try:
|
| 27 |
-
session_id = get_session_id()
|
| 28 |
-
if not session_id:
|
| 29 |
-
log.error("analyze_missing_connections: No session_id found in context")
|
| 30 |
-
return json.dumps({"error": "No session_id found in context"})
|
| 31 |
-
|
| 32 |
-
workflow_data = get_workflow_data(session_id)
|
| 33 |
-
if not workflow_data:
|
| 34 |
-
return json.dumps({"error": "No workflow data found for this session"})
|
| 35 |
-
|
| 36 |
-
object_info = await get_object_info()
|
| 37 |
-
|
| 38 |
-
analysis_result = {
|
| 39 |
-
"missing_connections": [],
|
| 40 |
-
"possible_connections": [],
|
| 41 |
-
"universal_inputs": [], # 可以连接任意输出的输入端口
|
| 42 |
-
"optional_unconnected_inputs": [], # 未连接的optional输入
|
| 43 |
-
"required_new_nodes": [],
|
| 44 |
-
"connection_summary": {
|
| 45 |
-
"total_missing": 0,
|
| 46 |
-
"auto_fixable": 0,
|
| 47 |
-
"requires_new_nodes": 0,
|
| 48 |
-
"universal_inputs_count": 0,
|
| 49 |
-
"optional_unconnected_count": 0
|
| 50 |
-
}
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
# 构建现有节点的输出映射
|
| 54 |
-
available_outputs = {}
|
| 55 |
-
for node_id, node_data in workflow_data.items():
|
| 56 |
-
node_class = node_data.get("class_type")
|
| 57 |
-
if node_class in object_info and "output" in object_info[node_class]:
|
| 58 |
-
outputs = object_info[node_class]["output"]
|
| 59 |
-
available_outputs[node_id] = {
|
| 60 |
-
"class_type": node_class,
|
| 61 |
-
"outputs": [(i, output_type) for i, output_type in enumerate(outputs)]
|
| 62 |
-
}
|
| 63 |
-
|
| 64 |
-
# 分析每个节点的缺失连接
|
| 65 |
-
for node_id, node_data in workflow_data.items():
|
| 66 |
-
node_class = node_data.get("class_type")
|
| 67 |
-
if node_class not in object_info:
|
| 68 |
-
continue
|
| 69 |
-
|
| 70 |
-
node_info = object_info[node_class]
|
| 71 |
-
if "input" not in node_info:
|
| 72 |
-
continue
|
| 73 |
-
|
| 74 |
-
required_inputs = node_info["input"].get("required", {})
|
| 75 |
-
current_inputs = node_data.get("inputs", {})
|
| 76 |
-
|
| 77 |
-
# 检查每个required input
|
| 78 |
-
for input_name, input_config in required_inputs.items():
|
| 79 |
-
if input_name not in current_inputs:
|
| 80 |
-
# 发现缺失的连接
|
| 81 |
-
missing_connection = {
|
| 82 |
-
"node_id": node_id,
|
| 83 |
-
"node_class": node_class,
|
| 84 |
-
"input_name": input_name,
|
| 85 |
-
"input_config": input_config,
|
| 86 |
-
"required": True
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
# 分析输入类型
|
| 90 |
-
if isinstance(input_config, (list, tuple)) and len(input_config) > 0:
|
| 91 |
-
expected_types = input_config[0] if isinstance(input_config[0], list) else [input_config[0]]
|
| 92 |
-
|
| 93 |
-
missing_connection["expected_types"] = expected_types
|
| 94 |
-
|
| 95 |
-
# 检查是否是通用输入(可以接受任意类型)
|
| 96 |
-
is_universal_input = "*" in expected_types
|
| 97 |
-
|
| 98 |
-
if is_universal_input:
|
| 99 |
-
# 这是一个通用输入端口,可以连接任意输出
|
| 100 |
-
universal_input = {
|
| 101 |
-
"node_id": node_id,
|
| 102 |
-
"node_class": node_class,
|
| 103 |
-
"input_name": input_name,
|
| 104 |
-
"input_config": input_config,
|
| 105 |
-
"can_connect_any_output": True
|
| 106 |
-
}
|
| 107 |
-
analysis_result["universal_inputs"].append(universal_input)
|
| 108 |
-
analysis_result["connection_summary"]["universal_inputs_count"] += 1
|
| 109 |
-
analysis_result["connection_summary"]["auto_fixable"] += 1
|
| 110 |
-
|
| 111 |
-
# 对于通用输入,我们不列出所有可能的连接,而是标记为通用
|
| 112 |
-
missing_connection["possible_matches"] = "universal"
|
| 113 |
-
missing_connection["is_universal"] = True
|
| 114 |
-
else:
|
| 115 |
-
# 查找具体类型匹配的连接
|
| 116 |
-
possible_matches = []
|
| 117 |
-
for source_node_id, source_info in available_outputs.items():
|
| 118 |
-
if source_node_id == node_id: # 不能连接自己
|
| 119 |
-
continue
|
| 120 |
-
|
| 121 |
-
for output_index, output_type in source_info["outputs"]:
|
| 122 |
-
# 检查类型匹配(排除通用类型的情况)
|
| 123 |
-
type_match = (output_type in expected_types or output_type == "*")
|
| 124 |
-
|
| 125 |
-
if type_match:
|
| 126 |
-
possible_matches.append({
|
| 127 |
-
"source_node_id": source_node_id,
|
| 128 |
-
"source_class": source_info["class_type"],
|
| 129 |
-
"output_index": output_index,
|
| 130 |
-
"output_type": output_type,
|
| 131 |
-
"match_confidence": "high" if output_type in expected_types else "medium"
|
| 132 |
-
})
|
| 133 |
-
|
| 134 |
-
missing_connection["possible_matches"] = possible_matches
|
| 135 |
-
missing_connection["is_universal"] = False
|
| 136 |
-
|
| 137 |
-
# 如果有可能的匹配,添加到possible_connections
|
| 138 |
-
if possible_matches:
|
| 139 |
-
analysis_result["possible_connections"].extend([
|
| 140 |
-
{
|
| 141 |
-
"target_node_id": node_id,
|
| 142 |
-
"target_input": input_name,
|
| 143 |
-
"source_node_id": match["source_node_id"],
|
| 144 |
-
"source_output_index": match["output_index"],
|
| 145 |
-
"connection": [match["source_node_id"], match["output_index"]],
|
| 146 |
-
"confidence": match["match_confidence"],
|
| 147 |
-
"types": {
|
| 148 |
-
"expected": expected_types,
|
| 149 |
-
"provided": match["output_type"]
|
| 150 |
-
}
|
| 151 |
-
} for match in possible_matches
|
| 152 |
-
])
|
| 153 |
-
analysis_result["connection_summary"]["auto_fixable"] += 1
|
| 154 |
-
else:
|
| 155 |
-
# 没有匹配的输出,需要新节点
|
| 156 |
-
analysis_result["connection_summary"]["requires_new_nodes"] += 1
|
| 157 |
-
|
| 158 |
-
# 分析需要什么类型的节点
|
| 159 |
-
required_node_types = analyze_required_node_types(expected_types, object_info)
|
| 160 |
-
analysis_result["required_new_nodes"].extend([{
|
| 161 |
-
"for_node": node_id,
|
| 162 |
-
"for_input": input_name,
|
| 163 |
-
"expected_types": expected_types,
|
| 164 |
-
"suggested_node_types": required_node_types
|
| 165 |
-
}])
|
| 166 |
-
|
| 167 |
-
analysis_result["missing_connections"].append(missing_connection)
|
| 168 |
-
|
| 169 |
-
# 检查optional inputs (未连接的可选输入)
|
| 170 |
-
optional_inputs = node_info["input"].get("optional", {})
|
| 171 |
-
for input_name, input_config in optional_inputs.items():
|
| 172 |
-
if input_name not in current_inputs:
|
| 173 |
-
# 发现未连接的optional输入
|
| 174 |
-
optional_unconnected = {
|
| 175 |
-
"node_id": node_id,
|
| 176 |
-
"node_class": node_class,
|
| 177 |
-
"input_name": input_name,
|
| 178 |
-
"input_config": input_config,
|
| 179 |
-
"required": False
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
# 分析输入类型
|
| 183 |
-
if isinstance(input_config, (list, tuple)) and len(input_config) > 0:
|
| 184 |
-
expected_types = input_config[0] if isinstance(input_config[0], list) else [input_config[0]]
|
| 185 |
-
else:
|
| 186 |
-
expected_types = ["*"] # 默认为通用类型
|
| 187 |
-
|
| 188 |
-
optional_unconnected["expected_types"] = expected_types
|
| 189 |
-
|
| 190 |
-
# 检查是否是通用输入(可以接受任意类型)
|
| 191 |
-
is_universal_input = "*" in expected_types
|
| 192 |
-
optional_unconnected["is_universal"] = is_universal_input
|
| 193 |
-
|
| 194 |
-
analysis_result["optional_unconnected_inputs"].append(optional_unconnected)
|
| 195 |
-
|
| 196 |
-
# 更新统计信息
|
| 197 |
-
analysis_result["connection_summary"]["total_missing"] = len(analysis_result["missing_connections"])
|
| 198 |
-
analysis_result["connection_summary"]["optional_unconnected_count"] = len(analysis_result["optional_unconnected_inputs"])
|
| 199 |
-
|
| 200 |
-
log.info(f"analysis_result: {json.dumps(analysis_result, ensure_ascii=False)}")
|
| 201 |
-
return json.dumps(analysis_result)
|
| 202 |
-
|
| 203 |
-
except Exception as e:
|
| 204 |
-
return json.dumps({"error": f"Failed to analyze missing connections: {str(e)}"})
|
| 205 |
-
|
| 206 |
-
def analyze_required_node_types(expected_types: List[str], object_info: Dict) -> List[Dict]:
|
| 207 |
-
"""分析需要什么类型的节点来提供指定的输出类型"""
|
| 208 |
-
suggested_nodes = []
|
| 209 |
-
|
| 210 |
-
# 常见类型到节点的映射
|
| 211 |
-
type_to_nodes = {
|
| 212 |
-
"MODEL": ["CheckpointLoaderSimple", "CheckpointLoader", "UNETLoader"],
|
| 213 |
-
"CLIP": ["CheckpointLoaderSimple", "CheckpointLoader", "CLIPLoader"],
|
| 214 |
-
"VAE": ["CheckpointLoaderSimple", "CheckpointLoader", "VAELoader"],
|
| 215 |
-
"CONDITIONING": ["CLIPTextEncode"],
|
| 216 |
-
"LATENT": ["EmptyLatentImage", "VAEEncode"],
|
| 217 |
-
"IMAGE": ["LoadImage", "VAEDecode"],
|
| 218 |
-
"MASK": ["LoadImageMask"],
|
| 219 |
-
"CONTROL_NET": ["ControlNetLoader"],
|
| 220 |
-
"LORA": ["LoraLoader"],
|
| 221 |
-
"IPADAPTER": ["IPAdapterModelLoader"]
|
| 222 |
-
}
|
| 223 |
-
|
| 224 |
-
for expected_type in expected_types:
|
| 225 |
-
if expected_type in type_to_nodes:
|
| 226 |
-
for node_class in type_to_nodes[expected_type]:
|
| 227 |
-
if node_class in object_info:
|
| 228 |
-
suggested_nodes.append({
|
| 229 |
-
"node_class": node_class,
|
| 230 |
-
"output_type": expected_type,
|
| 231 |
-
"confidence": "high",
|
| 232 |
-
"description": f"{node_class} can provide {expected_type}"
|
| 233 |
-
})
|
| 234 |
-
else:
|
| 235 |
-
# 搜索所有能提供该类型输出的节点
|
| 236 |
-
for node_class, node_info in object_info.items():
|
| 237 |
-
if "output" in node_info:
|
| 238 |
-
outputs = node_info["output"]
|
| 239 |
-
if expected_type in outputs:
|
| 240 |
-
suggested_nodes.append({
|
| 241 |
-
"node_class": node_class,
|
| 242 |
-
"output_type": expected_type,
|
| 243 |
-
"confidence": "medium",
|
| 244 |
-
"description": f"{node_class} can provide {expected_type}"
|
| 245 |
-
})
|
| 246 |
-
|
| 247 |
-
# 去重并排序
|
| 248 |
-
unique_nodes = {}
|
| 249 |
-
for node in suggested_nodes:
|
| 250 |
-
key = node["node_class"]
|
| 251 |
-
if key not in unique_nodes or unique_nodes[key]["confidence"] == "medium":
|
| 252 |
-
unique_nodes[key] = node
|
| 253 |
-
|
| 254 |
-
return list(unique_nodes.values())
|
| 255 |
-
|
| 256 |
-
def save_checkpoint_before_link_modification(session_id: str, action_description: str) -> Optional[int]:
|
| 257 |
-
"""在连接修改前保存checkpoint"""
|
| 258 |
-
try:
|
| 259 |
-
current_workflow = get_workflow_data(session_id)
|
| 260 |
-
if not current_workflow:
|
| 261 |
-
return None
|
| 262 |
-
|
| 263 |
-
checkpoint_id = save_workflow_data(
|
| 264 |
-
session_id,
|
| 265 |
-
current_workflow,
|
| 266 |
-
workflow_data_ui=None,
|
| 267 |
-
attributes={
|
| 268 |
-
"checkpoint_type": "link_agent_start",
|
| 269 |
-
"description": f"Checkpoint before {action_description}",
|
| 270 |
-
"action": "link_agent_checkpoint",
|
| 271 |
-
"timestamp": time.time()
|
| 272 |
-
}
|
| 273 |
-
)
|
| 274 |
-
log.info(f"Saved link agent checkpoint with ID: {checkpoint_id}")
|
| 275 |
-
return checkpoint_id
|
| 276 |
-
except Exception as e:
|
| 277 |
-
log.error(f"Failed to save checkpoint before link modification: {str(e)}")
|
| 278 |
-
return None
|
| 279 |
-
|
| 280 |
-
@function_tool
|
| 281 |
-
def apply_connection_fixes(fixes_json: str) -> str:
|
| 282 |
-
"""批量应用连接修复,fixes_json应为包含修复指令的JSON字符串"""
|
| 283 |
-
try:
|
| 284 |
-
session_id = get_session_id()
|
| 285 |
-
if not session_id:
|
| 286 |
-
log.error("apply_connection_fixes: No session_id found in context")
|
| 287 |
-
return json.dumps({"error": "No session_id found in context"})
|
| 288 |
-
|
| 289 |
-
# 在修改前保存checkpoint
|
| 290 |
-
checkpoint_id = save_checkpoint_before_link_modification(session_id, "batch connection fixes")
|
| 291 |
-
|
| 292 |
-
# 解析修复指令
|
| 293 |
-
fixes = json.loads(fixes_json) if isinstance(fixes_json, str) else fixes_json
|
| 294 |
-
|
| 295 |
-
workflow_data = get_workflow_data(session_id)
|
| 296 |
-
if not workflow_data:
|
| 297 |
-
return json.dumps({"error": "No workflow data found for this session"})
|
| 298 |
-
|
| 299 |
-
applied_fixes = []
|
| 300 |
-
failed_fixes = []
|
| 301 |
-
|
| 302 |
-
# 处理连接修复
|
| 303 |
-
connections_to_add = fixes.get("connections", [])
|
| 304 |
-
for conn_fix in connections_to_add:
|
| 305 |
-
try:
|
| 306 |
-
target_node_id = conn_fix["target_node_id"]
|
| 307 |
-
target_input = conn_fix["target_input"]
|
| 308 |
-
source_node_id = conn_fix["source_node_id"]
|
| 309 |
-
source_output_index = conn_fix["source_output_index"]
|
| 310 |
-
|
| 311 |
-
if target_node_id not in workflow_data:
|
| 312 |
-
failed_fixes.append({
|
| 313 |
-
"type": "connection",
|
| 314 |
-
"target": f"{target_node_id}.{target_input}",
|
| 315 |
-
"error": f"Target node {target_node_id} not found"
|
| 316 |
-
})
|
| 317 |
-
continue
|
| 318 |
-
|
| 319 |
-
if source_node_id not in workflow_data:
|
| 320 |
-
failed_fixes.append({
|
| 321 |
-
"type": "connection",
|
| 322 |
-
"target": f"{target_node_id}.{target_input}",
|
| 323 |
-
"error": f"Source node {source_node_id} not found"
|
| 324 |
-
})
|
| 325 |
-
continue
|
| 326 |
-
|
| 327 |
-
# 应用连接
|
| 328 |
-
if "inputs" not in workflow_data[target_node_id]:
|
| 329 |
-
workflow_data[target_node_id]["inputs"] = {}
|
| 330 |
-
|
| 331 |
-
old_value = workflow_data[target_node_id]["inputs"].get(target_input, "not connected")
|
| 332 |
-
new_connection = [source_node_id, source_output_index]
|
| 333 |
-
workflow_data[target_node_id]["inputs"][target_input] = new_connection
|
| 334 |
-
|
| 335 |
-
applied_fixes.append({
|
| 336 |
-
"type": "connection",
|
| 337 |
-
"target": f"{target_node_id}.{target_input}",
|
| 338 |
-
"source": f"{source_node_id}[{source_output_index}]",
|
| 339 |
-
"old_value": old_value,
|
| 340 |
-
"new_value": new_connection
|
| 341 |
-
})
|
| 342 |
-
|
| 343 |
-
except Exception as e:
|
| 344 |
-
failed_fixes.append({
|
| 345 |
-
"type": "connection",
|
| 346 |
-
"target": f"{conn_fix.get('target_node_id', 'unknown')}.{conn_fix.get('target_input', 'unknown')}",
|
| 347 |
-
"error": str(e)
|
| 348 |
-
})
|
| 349 |
-
|
| 350 |
-
# 处理新节点添加
|
| 351 |
-
nodes_to_add = fixes.get("new_nodes", [])
|
| 352 |
-
for node_spec in nodes_to_add:
|
| 353 |
-
try:
|
| 354 |
-
node_class = node_spec["node_class"]
|
| 355 |
-
node_id = node_spec.get("node_id", "")
|
| 356 |
-
inputs = node_spec.get("inputs", {})
|
| 357 |
-
|
| 358 |
-
# 生成节点ID
|
| 359 |
-
if not node_id:
|
| 360 |
-
existing_ids = set(workflow_data.keys())
|
| 361 |
-
node_id = "1"
|
| 362 |
-
while node_id in existing_ids:
|
| 363 |
-
node_id = str(int(node_id) + 1)
|
| 364 |
-
|
| 365 |
-
# 创建新节点
|
| 366 |
-
new_node = {
|
| 367 |
-
"class_type": node_class,
|
| 368 |
-
"inputs": inputs,
|
| 369 |
-
"_meta": {"title": node_class}
|
| 370 |
-
}
|
| 371 |
-
|
| 372 |
-
workflow_data[node_id] = new_node
|
| 373 |
-
|
| 374 |
-
applied_fixes.append({
|
| 375 |
-
"type": "add_node",
|
| 376 |
-
"node_id": node_id,
|
| 377 |
-
"node_class": node_class,
|
| 378 |
-
"inputs": inputs
|
| 379 |
-
})
|
| 380 |
-
|
| 381 |
-
# 如果指定了要自动连接的目标,进行连接
|
| 382 |
-
auto_connect = node_spec.get("auto_connect", [])
|
| 383 |
-
for auto_conn in auto_connect:
|
| 384 |
-
target_node_id = auto_conn["target_node_id"]
|
| 385 |
-
target_input = auto_conn["target_input"]
|
| 386 |
-
output_index = auto_conn.get("output_index", 0)
|
| 387 |
-
|
| 388 |
-
if target_node_id in workflow_data:
|
| 389 |
-
if "inputs" not in workflow_data[target_node_id]:
|
| 390 |
-
workflow_data[target_node_id]["inputs"] = {}
|
| 391 |
-
|
| 392 |
-
workflow_data[target_node_id]["inputs"][target_input] = [node_id, output_index]
|
| 393 |
-
|
| 394 |
-
applied_fixes.append({
|
| 395 |
-
"type": "auto_connection",
|
| 396 |
-
"from_new_node": node_id,
|
| 397 |
-
"to": f"{target_node_id}.{target_input}",
|
| 398 |
-
"output_index": output_index
|
| 399 |
-
})
|
| 400 |
-
|
| 401 |
-
except Exception as e:
|
| 402 |
-
failed_fixes.append({
|
| 403 |
-
"type": "add_node",
|
| 404 |
-
"node_class": node_spec.get("node_class", "unknown"),
|
| 405 |
-
"error": str(e)
|
| 406 |
-
})
|
| 407 |
-
|
| 408 |
-
# 保存更新的工作流
|
| 409 |
-
version_id = save_workflow_data(
|
| 410 |
-
session_id,
|
| 411 |
-
workflow_data,
|
| 412 |
-
attributes={
|
| 413 |
-
"action": "link_agent_batch_fix",
|
| 414 |
-
"description": f"Applied {len(applied_fixes)} connection fixes",
|
| 415 |
-
"fixes_applied": applied_fixes,
|
| 416 |
-
"fixes_failed": failed_fixes
|
| 417 |
-
}
|
| 418 |
-
)
|
| 419 |
-
|
| 420 |
-
# 构建返回数据,包含checkpoint信息
|
| 421 |
-
ext_data = [{
|
| 422 |
-
"type": "workflow_update",
|
| 423 |
-
"data": {
|
| 424 |
-
"workflow_data": workflow_data,
|
| 425 |
-
"changes": {
|
| 426 |
-
"applied_fixes": applied_fixes,
|
| 427 |
-
"failed_fixes": failed_fixes
|
| 428 |
-
}
|
| 429 |
-
}
|
| 430 |
-
}]
|
| 431 |
-
|
| 432 |
-
# 如果成功保存了checkpoint,添加修改前的checkpoint信息
|
| 433 |
-
if checkpoint_id:
|
| 434 |
-
ext_data.append({
|
| 435 |
-
"type": "link_agent_checkpoint",
|
| 436 |
-
"data": {
|
| 437 |
-
"checkpoint_id": checkpoint_id,
|
| 438 |
-
"checkpoint_type": "link_agent_start"
|
| 439 |
-
}
|
| 440 |
-
})
|
| 441 |
-
|
| 442 |
-
# 添加修改后的版本信息
|
| 443 |
-
ext_data.append({
|
| 444 |
-
"type": "link_agent_complete",
|
| 445 |
-
"data": {
|
| 446 |
-
"version_id": version_id,
|
| 447 |
-
"checkpoint_type": "link_agent_complete"
|
| 448 |
-
}
|
| 449 |
-
})
|
| 450 |
-
|
| 451 |
-
return json.dumps({
|
| 452 |
-
"success": True,
|
| 453 |
-
"version_id": version_id,
|
| 454 |
-
"applied_fixes": applied_fixes,
|
| 455 |
-
"failed_fixes": failed_fixes,
|
| 456 |
-
"summary": {
|
| 457 |
-
"total_fixes": len(applied_fixes),
|
| 458 |
-
"failed_fixes": len(failed_fixes),
|
| 459 |
-
"connections_added": len([f for f in applied_fixes if f["type"] == "connection"]),
|
| 460 |
-
"nodes_added": len([f for f in applied_fixes if f["type"] == "add_node"])
|
| 461 |
-
},
|
| 462 |
-
"message": f"Applied {len(applied_fixes)} fixes successfully, {len(failed_fixes)} failed",
|
| 463 |
-
"ext": ext_data
|
| 464 |
-
})
|
| 465 |
-
|
| 466 |
-
except Exception as e:
|
| 467 |
-
return json.dumps({"error": f"Failed to apply connection fixes: {str(e)}"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|