kimwint commited on
Commit
ac5a1a1
·
verified ·
1 Parent(s): f9c0117

Delete custom_nodes

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. custom_nodes/Cheny_custom_nodes/yolov8_person_detect.py +0 -188
  2. custom_nodes/ComfyUI-AK-Pack/.gitignore +0 -18
  3. custom_nodes/ComfyUI-AK-Pack/README.md +0 -183
  4. custom_nodes/ComfyUI-AK-Pack/__init__.py +0 -109
  5. custom_nodes/ComfyUI-AK-Pack/icon.jpg +0 -0
  6. custom_nodes/ComfyUI-AK-Pack/nodes/AKBase.py +0 -248
  7. custom_nodes/ComfyUI-AK-Pack/nodes/AKCLIPEncodeMultiple.py +0 -243
  8. custom_nodes/ComfyUI-AK-Pack/nodes/AKContrastAndSaturateImage.py +0 -281
  9. custom_nodes/ComfyUI-AK-Pack/nodes/AKControlMultipleKSamplers.py +0 -46
  10. custom_nodes/ComfyUI-AK-Pack/nodes/AKIndexMultiple.py +0 -72
  11. custom_nodes/ComfyUI-AK-Pack/nodes/AKKSamplerSettings.py +0 -152
  12. custom_nodes/ComfyUI-AK-Pack/nodes/AKPipeLoop.py +0 -184
  13. custom_nodes/ComfyUI-AK-Pack/nodes/AKProjectSettingsOut.py +0 -178
  14. custom_nodes/ComfyUI-AK-Pack/nodes/AKReplaceAlphaWithColor.py +0 -201
  15. custom_nodes/ComfyUI-AK-Pack/nodes/AKReplaceColorWithAlpha.py +0 -153
  16. custom_nodes/ComfyUI-AK-Pack/nodes/CLIPTextEncodeAndCombineCached.py +0 -68
  17. custom_nodes/ComfyUI-AK-Pack/nodes/CLIPTextEncodeCached.py +0 -50
  18. custom_nodes/ComfyUI-AK-Pack/nodes/Getter.py +0 -38
  19. custom_nodes/ComfyUI-AK-Pack/nodes/IsOneOfGroupsActive.py +0 -43
  20. custom_nodes/ComfyUI-AK-Pack/nodes/PreviewRawText.py +0 -99
  21. custom_nodes/ComfyUI-AK-Pack/nodes/RepeatGroupState.py +0 -35
  22. custom_nodes/ComfyUI-AK-Pack/nodes/Setter.py +0 -136
  23. custom_nodes/ComfyUI-AK-Pack/pyproject.toml +0 -28
  24. custom_nodes/ComfyUI-Addoor/classes/AD_AnyFileList.py +0 -88
  25. custom_nodes/ComfyUI-Addoor/classes/AD_BatchImageLoadFromDir.py +0 -52
  26. custom_nodes/ComfyUI-Addoor/classes/AD_LoadImageAdvanced.py +0 -87
  27. custom_nodes/ComfyUI-Addoor/classes/AD_PromptReplace.py +0 -59
  28. custom_nodes/ComfyUI-Addoor/example_workflow/RunningHub API.json +0 -500
  29. custom_nodes/ComfyUI-Addoor/nodes/AddPaddingAdvanced.py +0 -435
  30. custom_nodes/ComfyUI-Addoor/nodes/AddPaddingBase.py +0 -99
  31. custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-FofrToolkit.py +0 -126
  32. custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-imageResize.py +0 -172
  33. custom_nodes/ComfyUI-Addoor/nodes/ComfyUI-textAppend.py +0 -48
  34. custom_nodes/ComfyUI-Addoor/nodes/__init__.py +0 -62
  35. custom_nodes/ComfyUI-Addoor/nodes/imagecreatemask.py +0 -109
  36. custom_nodes/ComfyUI-Addoor/nodes/multiline_string.py +0 -42
  37. custom_nodes/ComfyUI-Addoor/rp/RunningHubFileSaver.py +0 -183
  38. custom_nodes/ComfyUI-Addoor/rp/RunningHubInit.py +0 -22
  39. custom_nodes/ComfyUI-Addoor/rp/RunningHubNodeInfo.py +0 -45
  40. custom_nodes/ComfyUI-Addoor/rp/RunningHubWorkflowExecutor.py +0 -151
  41. custom_nodes/ComfyUI-Addoor/rp/config.ini +0 -6
  42. custom_nodes/ComfyUI-Copilot/backend/agent_factory.py +0 -98
  43. custom_nodes/ComfyUI-Copilot/backend/controller/conversation_api.py +0 -1083
  44. custom_nodes/ComfyUI-Copilot/backend/controller/expert_api.py +0 -192
  45. custom_nodes/ComfyUI-Copilot/backend/core.py +0 -23
  46. custom_nodes/ComfyUI-Copilot/backend/dao/__init__.py +0 -0
  47. custom_nodes/ComfyUI-Copilot/backend/dao/workflow_table.py +0 -183
  48. custom_nodes/ComfyUI-Copilot/backend/logs/comfyui_copilot.log +0 -0
  49. custom_nodes/ComfyUI-Copilot/backend/service/__init__.py +0 -0
  50. 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/MSamplers.jpg) | [![](./img/MSamplersSettings.jpg)](./img/MSamplersSettings.jpg) | [![](./img/MSamplersChooser.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/ProjectSettings.jpg) | [![](./img/ProjectSettingsOptions.jpg)](./img/ProjectSettingsOptions.jpg) | [![](./img/AKProjectSettingsOut.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/AKBase.jpg) | [![](./img/AKBaseGallery.jpg)](./img/AKBaseGallery.jpg) | [![](./img/AKBasePIP.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)}"})